Kotlin coroutine - coroutine dispatcher CoroutineDispatcher

1. Concept

The coroutine must run on a thread, so a scheduler must be specified. It is an abstract class. Dispatcher is a helper class in the standard library that helps us encapsulate switching threads. It can schedule the coroutine to be executed on which type of thread. When creating a coroutine, if the context does not specify or inherit a scheduler, a default scheduler will be added (the scheduler is implemented through the ContinuationInterceptor continuation body interceptor). Scheduling is done through Dispatchers instead of Thread because the thread is not simply specified.

2. Mode

  • Since the child coroutine will inherit the context of the parent coroutine, after specifying the scheduler mode on the parent coroutine, the child coroutine will use this mode by default.
  • IO and DEFAULT modes share the same thread pool and reuse threads for optimization (DEFAULT switches to IO with a high probability of staying on the same thread). The two have independent limits on the number of threads and will not starve each other. If used together to the maximum extent, the default number of threads active at the same time is 64 + the number of CPUs.
Dispatcher.Main Runs on the main thread, which is the UI thread in Android, and is used to handle some lightweight tasks of UI interaction.

Call the suspend function

Call UI function

Update LiveData

Dispatcher.Main.immediate Scheduling of coroutines has costs. When we are already in the main thread, if we start a sub-coroutine scheduled to the main thread, we will experience suspension and wait for recovery. This is unnecessary overhead, and even a long queue will cause data loss. Delayed display (for example, ViewModelScope is in the default main thread of Android, so the scheduler in the context uses this). If it is designated as immediate, it will only be scheduled when needed, otherwise it will be executed directly. Functions wrapped withContext are used when running on Dispatcher.Main.
Dispatcher.IO

Runs in a thread pool and is optimized for IO blocking tasks. The maximum number of threads is 64. As long as it does not exceed and there are no idle threads, new threads can always be opened to perform new tasks.

database

File reading and writing

network processing

Dispatcher.Default

Runs on a thread pool and is optimized for CPU-intensive computing tasks. The maximum number of threads is the number of CPU cores (but not less than 2). If all are busy, new tasks cannot be executed.

Array sort

Json parsing

Handling Discrepancy Judgments

Calculate Bitmap

Dispatcher.Unconfined Do not change the thread, start its thread execution, and resume its thread execution. Scheduling has the lowest cost and the best performance, but there is a risk of calling blocking operations on the main thread. Used when you don't need to care about which thread the coroutine is suspended on.

3. Limit the number of threads limitedParallelism()

Introduced in version 1.6.

  • For Default mode: When there is a very expensive task, which may cause other coroutines using the same scheduler to be unable to grab the thread execution rights, this time it can be used to limit the number of threads used by the coroutine.
  • For IO mode: When there is a very expensive task, it may cause too many threads to be blocked and other tasks to pause and wait, breaking through the default limit of 64 threads to accelerate execution (not significant).
  • The parameter transfer limits the thread to 1 to solve the synchronization problem of multiple threads modifying data concurrently. But if it is blocked, other operations will have to wait.
public open fun limitedParallelism(parallelism: Int): CoroutineDispatcher
suspend fun main(): Unit = coroutineScope {
    //使用默认IO模式
    launch {
        printTime(Dispatchers.IO)   //打印:Dispatchers.IO 花费了: 2038/
    }
    //使用limitedParallelism增加线程
    launch {
        val dispatcher = Dispatchers.IO.limitedParallelism(100)
        printTime(dispatcher)   //打印:LimitedDispatcher@1cc12797 花费了: 1037
    }
}

suspend fun printTime(dispatcher: CoroutineDispatcher) {
    val time = measureTimeMillis {
        coroutineScope {
            repeat(100) {
                launch(dispatcher) {
                    Thread.sleep(1000)
                }
            }
        }
    }
    println("$dispatcher 花费了: $time")
}

4. Multi-thread concurrency issues

Create 10 coroutines, each coroutine executes i++ 1000 times, and the expected result is i=10000.

4.1 Avoid using shared variables

fun main() = runBlocking {
    val deferreds = mutableListOf<Deferred<Int>>()
    repeat(10) {
        val deferred = async(Dispatchers.Default) {
            var i = 0
            repeat(1000) { i++ }
            return@async i
        }
        deferreds.add(deferred)
    }
    var result = 0
    deferreds.forEach {
        result += it.await()
    }
    println("i = $result")
}
打印:i = 10000,耗时:77

4.2 Using Java methods (not recommended)

Synchronized, Lock, Atomic can be used. Because it is a blocking method under the thread model, calling the suspension function is not supported, which will affect the coroutine suspension feature.

4.2.1 Using synchronization lock

fun main() = runBlocking {
    val start = System.currentTimeMillis()
    var i = 0
    val jobs = mutableListOf<Job>()

    @Synchronized
    fun add() { i++ }

    repeat(10) {
        val job = launch(Dispatchers.Default) {
            repeat(1000) { add() }
        }
        jobs.add(job)
    }
    jobs.joinAll()
    println("i = $i,耗时:${System.currentTimeMillis() - start}")
}
//打印:i = 10000,耗时:71

4.2.2 Using synchronized code blocks

fun main() = runBlocking {
    val start = System.currentTimeMillis()
    val lock = Any()
    var i = 0
    val jobs = mutableListOf<Job>()
    repeat(10) {
        val job = launch(Dispatchers.Default) {
            repeat(1000) {
                synchronized(lock) { i++ }
            }
        }
        jobs.add(job)
    }
    jobs.joinAll()
    println("i = $i,耗时:${System.currentTimeMillis() - start}")
}
//打印:i = 10000,耗时:73

4.2.3 Using reentrant lock ReenTrantLock

fun main() = runBlocking {
    val start = System.currentTimeMillis()
    val lock = ReentrantLock()
    var i = 0
    val jobs = mutableListOf<Job>()
    repeat(10) {
        val job = launch(Dispatchers.Default) {
            repeat(1000) {
                lock.lock()
                i++
                lock.unlock()
            }
        }
        jobs.add(job)
    }
    jobs.joinAll()
    println("i = $i,耗时:${System.currentTimeMillis() - start}")
}
//打印:i = 10000,耗时:83

4.2.4 Use AtomicInteger to ensure atomicity

fun main() = runBlocking {
    val start = System.currentTimeMillis()
    var i = AtomicInteger(0)
    val jobs = mutableListOf<Job>()
    repeat(10) {
        val job = launch(Dispatchers.Default) {
            repeat(1000) { i.incrementAndGet() }
        }
        jobs.add(job)
    }
    jobs.joinAll()
    println("i = $i,耗时:${System.currentTimeMillis() - start}")
}
//打印:i = 10000,耗时:89

4.3 Use single thread (not recommended)

This was done before version 1.6 without limitedParallelism(). The problem with this approach is that it is easy to forget to use close() to close, and it may offset the use of the thread pool (keeping unused threads active but not with other services share these threads).

fun main() = runBlocking {
    val start = System.currentTimeMillis()
    val mySingleDispatcher = Executors.newSingleThreadExecutor {
        Thread(it, "我的线程").apply { isDaemon = true }
    }.asCoroutineDispatcher()
    var i = 0
    val jobs = mutableListOf<Job>()
    repeat(10) {
        val job = launch(mySingleDispatcher) {
            repeat(1000) { i++ }
        }
        jobs.add(job)
    }
    jobs.joinAll()
    println("i = $i,耗时:${System.currentTimeMillis() - start}")
}
//打印:i = 10000,耗时:64

4.4 Using Mutex

The Java method does not support calling suspension functions. The synchronization lock is blocking and will affect the coroutine characteristics. For this reason, Kotlin provides a non-blocking lock Mutex. Multi-thread synchronization can be achieved by using mutex.lock() and mutex.unlock() to wrap the calculation logic that needs to be synchronized. However, due to possible exceptions in the package content, unlock() cannot be executed. It will be very difficult to write it in finally{}. It is cumbersome, so the extension function mutex.withLock{ } is provided. The essence is to call unlock() in finally{ }.

public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action: () -> T): T {     lock(owner)     try {         return action()     } finally { // Note that there is no catch here code block, so the exception will not be caught         unlock(owner)     } }






fun main() = runBlocking {
    val start = System.currentTimeMillis()
    var i = 0
    val mutex = Mutex()
    //使用方式一
    mutex.lock()
//    try {
//        repeat(10000) { i++ }
//    } catch (e: Exception) {
//        e.printStackTrace()
//    } finally {
//        mutex.unlock()
//    }
    //使用方式二
    mutex.withLock {
        try {
            repeat(10000) { i++ }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
    println("i = $i,耗时:${System.currentTimeMillis() - start}")
}
//方式一打印:i = 10000,耗时:17
//方式二打印:i = 10000,耗时:17

 4.5 Using Actors

Actor is a concurrent synchronization model, which is essentially implemented based on Channel pipeline messages.

sealed class Msg {
    object AddMsg : Msg()
    class ResultMsg(val result: CompletableDeferred<Int>) : Msg()
}

@OptIn(ObsoleteCoroutinesApi::class)
fun main() = runBlocking {
    val start = System.currentTimeMillis()
    val actor = actor<Msg> {
        var i = 0
        for (msg in channel) {
            when (msg) {
                is Msg.AddMsg -> i++
                is Msg.ResultMsg -> msg.result.complete(i)
            }
        }
    }
    val jobs = mutableListOf<Job>()
    repeat(10) {
        val job = launch {
            repeat(1000) {
                actor.send(Msg.AddMsg)
            }
        }
        jobs.add(job)
    }
    jobs.joinAll()
    val deferred = CompletableDeferred<Int>()
    actor.send(Msg.ResultMsg(deferred))
    val result = deferred.await()
    actor.close()
    println("i = $result,耗时:${System.currentTimeMillis() - start}")
}
//打印:i = 10000,耗时:167

 4.6 Using Semaphore

Semaphore is a semaphore in a coroutine. Specifying the number of passes as 1 can ensure that the number of concurrency is 1.

Guess you like

Origin blog.csdn.net/HugMua/article/details/132797687