So I have this experimental code:
class WorkLoader : Runnable {
private val id : Int
private val listener : Listener?
private val lock : ReentrantLock
private val condition : Condition
private val counter : Counter?
private var isFinished : Boolean
constructor(counter: Counter? = null, listener: Listener? = null) {
id = IdGenerator.getId()
isFinished = false
lock = ReentrantLock()
condition = lock.newCondition()
this.counter = counter
this.listener = listener
}
interface Listener {
fun onWorkStarted(id : Int)
fun onWorkFinished(id : Int, s : String, elapsed : Long)
}
override fun run() {
listener?.onWorkStarted(id)
val startTime = System.currentTimeMillis()
//The loop below just simply loads the CPU with useless stuff, it does nothing important
var s = ""
for (i in 1 .. 10_000_000) {
counter?.add()
val c : Char = (i % 95 + 32).toChar()
s += c
if (s.length > 200) {
s = s.substring(1)
}
}
val elapsedTime = System.currentTimeMillis() - startTime
listener?.onWorkFinished(id, s, elapsedTime)
lock.lock()
isFinished = true
condition.signal()
lock.unlock()
}
fun waitTillFinished() {
lock.lock()
while (!isFinished) {
condition.await()
}
lock.unlock()
}
}
And the main function that runs simultaneously 6 instances of WorkLoader in 6 separate threads:
fun main(arguments: Array<String>) {
println("Hello World!")
val workListener = WorkLoaderListener()
val workers = ArrayList<WorkLoader>()
for (i in 1..6) {
val workLoader = WorkLoader(counter = null, workListener)
workers.add(workLoader)
val thread = Thread(workLoader)
thread.start()
}
for (worker in workers) {
worker.waitTillFinished()
}
println("End of main thread")
}
class WorkLoaderListener : WorkLoader.Listener {
override fun onWorkStarted(id: Int) {
println("Work started, id:$id ${getFormattedTime()}")
}
override fun onWorkFinished(id: Int, s: String, elapsed : Long) {
println("Work ENDED, id:$id ${getFormattedTime()}, in ${elapsed/1000} s")
}
}
It takes 8s to get all 6 threads to finish execution. Here is the output:
Hello World!
Work started, id:1 21:12:26.577
Work started, id:0 21:12:26.577
Work started, id:2 21:12:26.577
Work started, id:4 21:12:26.577
Work started, id:5 21:12:26.577
Work started, id:3 21:12:26.577
Work ENDED, id:2 21:12:35.137, in 8 s
Work ENDED, id:1 21:12:35.137, in 8 s
Work ENDED, id:3 21:12:35.215, in 8 s
Work ENDED, id:0 21:12:35.215, in 8 s
Work ENDED, id:5 21:12:35.215, in 8 s
Work ENDED, id:4 21:12:35.231, in 8 s
End of main thread
However!!! only 1 instance of WorkLoader in a separate thread executes in just 1 second. Which makes it more efficient to run those threads one by one and not lunch them simultaneously. Like this:
for (i in 1..6) {
val workLoader = WorkLoader(counter = null, workListener)
workers.add(workLoader)
val thread = Thread(workLoader)
thread.start()
//just one extra line to wait for the termination before starting another workLoader
workLoader.waitTillFinished() //I understand that the workLoader thread might still be running when this method returns,
// but it doesn't matter, the thread is about to die anyway
}
output:
Hello World!
Work started, id:0 21:23:33.622
Work ENDED, id:0 21:23:35.411, in 1 s
Work started, id:1 21:23:35.411
Work ENDED, id:1 21:23:36.545, in 1 s
Work started, id:2 21:23:36.545
Work ENDED, id:2 21:23:37.576, in 1 s
Work started, id:3 21:23:37.576
Work ENDED, id:3 21:23:38.647, in 1 s
Work started, id:4 21:23:38.647
Work ENDED, id:4 21:23:39.687, in 1 s
Work started, id:5 21:23:39.687
Work ENDED, id:5 21:23:40.726, in 1 s
End of main thread
So in this case the execution of the whole program ended in like 6 or 7 seconds. I have a 6 core intel CPU with 12 logical threads. So I'm expecting to have all 6 threads executed in like 2 seconds at most (when launched all at once). In first case (all threads at once) the CPU spiked to 100% utilization and it stayed there for the entire time of execution. In the second case (one thread at a time) the CPU spiked to 47% for a brief moment and the whole execution went slightly faster.
So what's the point of multithreading? Why is this happening? It feels like there is no point to have more then 1 worker thread, since that any additional threads will make all other threads slower, regardless of how many CPU cores you have at your disposal. And if one single thread is able to use all the cores of the CPU then why didn't my CPU spike to 100% load in the second case?