Home > other >  Why multithreading doesn't make the execution faster
Why multithreading doesn't make the execution faster

Time:09-27

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?

CodePudding user response:

@Tenfour04, Thank you! your comment directed me to the right answer. The purpose of multithreading is SAVED! So, apparently my String manipulations where indeed not multithreading friendly, don't know why. So I changed my CPU loading code to this:

val cArr = arrayOfNulls<Char>(200)
for (i in 1..20_000_000) {
    val cValue: Char = (i % 95   32).toChar()
    if (cArr[0] == null) {
        cArr[0] = cValue
    } else {
        var tempC = cValue
        for (ci in cArr.indices) {
            val temp = cArr[ci]
            cArr[ci] = tempC
            if (temp == null) {
                break
            }
            tempC = temp
        }
    }
}

Now this code, executes in 3 seconds on 1 thread. As you can see in the output below:

Hello World!
All threads initiated
Work started, id:0 02:16:58.000
Work ENDED,   id:0 02:17:01.189, in 3 s
End of main thread

Now 6 threads:

Hello World!
All threads initiated
Work started, id:0 02:18:48.830
Work started, id:2 02:18:48.830
Work started, id:1 02:18:48.830
Work started, id:5 02:18:48.830
Work started, id:4 02:18:48.830
Work started, id:3 02:18:48.830
Work ENDED,   id:1 02:18:53.090, in 4 s
Work ENDED,   id:0 02:18:53.168, in 4 s
Work ENDED,   id:3 02:18:53.230, in 4 s
Work ENDED,   id:4 02:18:53.246, in 4 s
Work ENDED,   id:2 02:18:53.340, in 4 s
Work ENDED,   id:5 02:18:53.340, in 4 s
End of main thread

6 times the amount of work, but it adds just 1 second to the total execution and wait, my CPU has 12 logical threads, so...

Hello World!
All threads initiated
Work started, id:1 02:22:43.299
Work started, id:8 02:22:43.299
Work started, id:3 02:22:43.299
Work started, id:5 02:22:43.299
Work started, id:7 02:22:43.299
Work started, id:9 02:22:43.299
Work started, id:11 02:22:43.299
Work started, id:10 02:22:43.299
Work started, id:0 02:22:43.299
Work started, id:2 02:22:43.299
Work started, id:4 02:22:43.299
Work started, id:6 02:22:43.299
Work ENDED,   id:4 02:22:50.115, in 6 s
Work ENDED,   id:11 02:22:50.132, in 6 s
Work ENDED,   id:7 02:22:50.148, in 6 s
Work ENDED,   id:1 02:22:50.148, in 6 s
Work ENDED,   id:6 02:22:50.148, in 6 s
Work ENDED,   id:9 02:22:50.148, in 6 s
Work ENDED,   id:10 02:22:50.163, in 6 s
Work ENDED,   id:5 02:22:50.163, in 6 s
Work ENDED,   id:0 02:22:50.179, in 6 s
Work ENDED,   id:8 02:22:50.195, in 6 s
Work ENDED,   id:2 02:22:50.195, in 6 s
Work ENDED,   id:3 02:22:50.210, in 6 s
End of main thread

Now if I go over 12 threads which is more then the amount of logical threads of my CPU, the time increases considerably and it becomes inefficient. I won't post the output on this one, too many lines, so just believe me.

  • Related