An overview of the

In the last article Kotlin coroutine in-depth understanding of the working principle of coroutine from the source point of view of the Kotlin coroutine working principle, this article records the basic use of Kotlin coroutine, familiar with the development of coroutine students can ignore. If the content of the article is wrong, welcome to point out, common progress! Leave a “like” if you think it’s good

  • Link to blog
  • Basic use of Kotlin coroutines
  • In-depth understanding of how Kotlin coroutines work
  • Coroutine cancellation and exception handling of Kotlin coroutines

At Google I/O 2019, Kotlin was announced as an increasing priority for Android development. Kotlin is an expressive and concise programming language that reduces common code errors and can be easily integrated into existing applications. Those of you who have used Kotlin will probably think it smells good. The basic usage of Kotlin can be found in the official Kotlin documentation and the Basic Kotlin Notes.

In general, Coroutines are lightweight threads that do not need to switch from user to kernel mode. Coroutines are compilers, processes and threads are operating system levels. Coroutines are not directly associated with the operating system, but they also run in threads, which can be single-threaded. It can also be multithreaded. Designed to solve concurrency problems and make collaborative multitasking easier, coroutines can effectively eliminate callback hell.

Kotlin provides a convenient thread framework for switching threads multiple times within the same block of code — like Java Executors and Android Handler — so you can write asynchronous code in a way that looks synchronous. Non-blocking suspension.

Benefits of coroutines: Avoid callback hell (also available via RxJava or CompletableFuture) :

/ / callback
api.getAvatar(id) { avatar ->
    api.getName(id) { name ->
        show(getLabel(avatar, name))
    }
}

/ / coroutines
coroutineScope.launch(Dispatchers.Main) {
    val avatar = async { api.getAvatar(id) }
    val name = async { api.getName(id) }
    val label = getLabelSuspend(avatar.await(), name.await())
    show(label)
}
Copy the code

Dependencies need to be added before use:

def kotlin_coroutines = `1.39.`

// Rely on the coroutine core library
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
// Depends on the platform library corresponding to the current platform
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines"
Copy the code

Coroutines can be started in one of the following ways:

// Thread blocking, suitable for unit testing scenarios
runBlocking { getName(id) }

// Does not block threads, but is not recommended in Android because its lifecycle is the same as the app's
GlobalScope.launch { getName(id) }

// It is recommended to use the CoroutineContext parameter to manage and control the coroutine life cycle
// Example: context = dispatchers. Default + EmptyCoroutineContext
val coroutineScope = CoroutineScope(context)
coroutineScope.launch { getName(id) }

// Async starts a Job of type Deferred and can return the result, which is obtained with await method
// public suspend fun await(): T
val id = coroutineScope.async { getName(id) }
id.await()
Copy the code

To start a coroutine, you need three things: the context (CoroutineContext), the launch mode (CoroutineStart), and the coroutine body.

The basic concept

suspend

Suspend means to suspend, and the object it suspends is a coroutine.

  • When a thread reaches the suspend function of the coroutine, it stops executing the coroutine code. It jumps out of the coroutine block, and the thread does whatever it needs to do.
  • When a coroutine executes to the suspend function, the coroutine is “suspend”, that is, suspended from the current thread. In other words, the coroutine detaches from the thread executing it. The thread’s code is pinchedwhen it reaches the suspend function, and the coroutine proceeds from the suspend function, but in the thread specified by the suspend function.

Then, after the suspend function completes, the coroutine automatically cuts the thread back and resumes the code that follows it. The resume function is specific to the coroutine, so the suspend function must be called either in the coroutine or in another suspend function.

Suspend is non-blocking when it means that it can write non-blocking operations in code that looks blocking. A single coroutine can be non-blocking because it can use the suspend function to switch threads, which are essentially multithreading, but written to look like two lines of code in a row are blocking.

CoroutineScope

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}
Copy the code

GlobeScope

GlobeScope launches a coroutine that is a separate scope and does not inherit the scope of the upper coroutine, and its internal child coroutines follow the default scope rules.

coroutineScope

The coroutine cancel initiated by coroutineScope cancels all child coroutines as well as the parent coroutine, and any exceptions not caught by the child coroutine are passed up to the parent coroutine.

supervisorScope

The coroutine cancel and passing exception that is launched by the container is only propagated unidirectionally from the parent coroutine to the child coroutine. The MainScope is the container scope.

CoroutineContext

Job, CoroutineDispatcher, and ContinuationInterceptor are subclasses of CoroutineContext, that is, they are coroutine contexts. CoroutineContext has a plus method that overloads the (+) operator and aggregates elements such as Job and CoroutineDispatcher to represent a coroutine scenario.

public interface CoroutineContext {
    // Override the [] operator
    public operator fun <E : Element> get(key: Key<E>): E?

    public fun <R> fold(initial: R, operation: (R.Element) - >R): R

    // Override the + operator
    public operator fun plus(context: CoroutineContext): CoroutineContext = / *... * /

    public fun minusKey(key: Key< * >): CoroutineContext

    public interface Key<E : Element>

    public interface Element : CoroutineContext {
        // ...}}public interface ContinuationInterceptor : CoroutineContext.Element {
    // ...
}

public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    // ...
}
Copy the code

CoroutineStart

The startup modes are as follows:

public enum class CoroutineStart {
    DEFAULT,
    LAZY,
    ATOMIC,
    UNDISPATCHED; // Execute the coroutine body in the current thread immediately until the first suspend call
}
Copy the code

DEFAULT

DEFAULT is a hanhan-type startup. After launch is invoked, it will enter the state to be scheduled immediately and can be executed once the scheduler is OK.

LAZY

LAZY is a LAZY start. There is no scheduling behavior after launch, and the coroutine body does not execute until it is required to execute (calling job.start/join, etc.).

ATOMIC

@ExperimentalCoroutinesApi. This is similar to [DEFAULT], but the coroutine cannot be cancelled before it starts executing.

UNDISPATCHED

@ExperimentalCoroutinesApi. This is similar to [ATOMIC] in the sense that coroutine starts executing even if it was already cancelled, but the difference is that it starts executing in the same thread.

Dispatchers

The coroutine scheduler is used to specify which thread the coroutine body will execute in. Kotlin provides several:

Default

Default option to specify that the coroutine body is executed in the thread pool:

GlobalScope.launch(Dispatchers.Default) {
    println("1: ${Thread.currentThread().name}")
    launch(Dispatchers.Default) {
        println("2: ${Thread.currentThread().name}")
    }
    println("3.${Thread.currentThread().name}")
}

-->output
1: DefaultDispatcher-worker-1
3: DefaultDispatcher-worker-1
2: DefaultDispatcher-worker-2
Copy the code

Main

Specifies that the coroutine body is executed in the main thread.

Unconfined

The coroutine body runs in the thread in which the parent coroutine resides:

GlobalScope.launch(Dispatchers.Default) {
    println("1: ${Thread.currentThread().name}")
    launch(Dispatchers.Unconfined) {
        println("2: ${Thread.currentThread().name}")
    }
    println("3.${Thread.currentThread().name}")
}

-->output
1: DefaultDispatcher-worker-1
2: DefaultDispatcher-worker-1
3: DefaultDispatcher-worker-1
Copy the code

IO

Designed for offloading blocking IO tasks, so switching from Default to IO does not trigger a thread switch:

GlobalScope.launch(Dispatchers.Default) {
    println("1: ${Thread.currentThread().name}")
    launch(Dispatchers.IO) {
        println("2: ${Thread.currentThread().name}")
    }
    println("3.${Thread.currentThread().name}")
}

-->output
1: DefaultDispatcher-worker-1
3: DefaultDispatcher-worker-1
2: DefaultDispatcher-worker-1
Copy the code

Job&Deferred

A Job can be cancelled and has a simple life cycle, which has three states:

State isActive isCompleted isCancelled
New (optional initial state) false false false
Active (default initial state) true false false
Completing (optional transient state) true false false
Cancelling (optional transient state) false false true
Cancelled (final state) false true true
Completed (final state) false true false

Public Interface Deferred

: Job Public Interface Deferred

: Job Public Interface Deferred

: Job


The launch method returns a Job type:

public interface Job : CoroutineContext.Element {
    public companion object Key : CoroutineContext.Key<Job> {
        // ...
    }

    public val isActive: Boolean
    public val isCompleted: Boolean
    public val isCancelled: Boolean
    public fun start(a): Boolean
    public fun cancel(a): Unit = cancel(null)
    public suspend fun join(a)

    // ...
}
Copy the code

The async method returns a Deferred:

public interface Deferred<out T> : Job {
    // This method returns the value
    public suspend fun await(a): T
    // ...
}
Copy the code

Example:

fun main(a) = runBlocking {
    val job = async {
        delay(500)
        "Hello"
    }
    println("${job.await()}, World")}Copy the code

Android-Kotlin coroutine use

MainScope

GlobalScope is generally not recommended in Android because it creates a top-level coroutine that needs to keep all references to the coroutines GlobalScope launches and then cancel them during scenes such as Activity DeStory. Otherwise, it will cause problems such as memory leaks. MainScope can be used:

class CoroutineActivity : AppCompatActivity() {
    private val mainScope = MainScope()

    fun request1(a) {
        mainScope.launch {
            // ...}}// request2, 3, ...

    override fun onDestroy(a) {
        super.onDestroy()
        mainScope.cancel()
    }
}
Copy the code

MainScope definition:

public fun MainScope(a): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
Copy the code

Lifecycle coroutines

For information about Lifecycle, see Lifecycle in the Android-Jetpack component.

Add dependencies:

implementation "Androidx. Lifecycle: lifecycle - runtime - KTX: 2.2.0." "
Copy the code

The source code is as follows:

val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = mInternalScopeRef.get(a)as LifecycleCoroutineScopeImpl?
            if(existing ! =null) {
                return existing
            }
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            if (mInternalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }

abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope {
    internal abstract val lifecycle: Lifecycle

    // The coroutine body is executed when the activity is created
    fun launchWhenCreated(block: suspend CoroutineScope. () - >Unit): Job = launch {
        lifecycle.whenCreated(block)
    }

    // // Executes the coroutine body when the activity started
    fun launchWhenStarted(block: suspend CoroutineScope. () - >Unit): Job = launch {
        lifecycle.whenStarted(block)
    }

    // // Executes the coroutine body when the activity is resumed
    fun launchWhenResumed(block: suspend CoroutineScope. () - >Unit): Job = launch {
        lifecycle.whenResumed(block)
    }
}
Copy the code

Use:

AppCompatActivity implements the LifecycleOwner interface
class MainActivity : AppCompatActivity() {

    fun test(a) {
        lifecycleScope.launchWhenCreated {
            // ...}}}Copy the code

LiveData coroutines

For LiveData, see the LiveData-viewModel of the Android-Jetpack component.

Add dependencies:

implementation "Androidx. Lifecycle: lifecycle - livedata - KTX: 2.2.0." "
Copy the code

The source code is as follows:

fun <T> liveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT,
    @BuilderInference block: suspend LiveDataScope<T>. () - >Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)

internal class CoroutineLiveData<T>(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT,
    block: Block<T>
) : MediatorLiveData<T>() {
    private var blockRunner: BlockRunner<T>?
    private var emittedSource: EmittedSource? = null

    init {
        val supervisorJob = SupervisorJob(context[Job])
        val scope = CoroutineScope(Dispatchers.Main.immediate + context + supervisorJob)
        blockRunner = BlockRunner(
            liveData = this,
            block = block,
            timeoutInMs = timeoutInMs,
            scope = scope
        ) {
            blockRunner = null}}internal suspend fun emitSource(source: LiveData<T>): DisposableHandle {
        clearSource()
        val newSource = addDisposableSource(source)
        emittedSource = newSource
        return newSource
    }

    internal suspend fun clearSource(a){ emittedSource? .disposeNow() emittedSource =null
    }

    // Start the coroutine
    override fun onActive(a) {
        super.onActive() blockRunner? .maybeRun() }// get the cancel algorithm
    override fun onInactive(a) {
        super.onInactive() blockRunner? .cancel() } }Copy the code

Use:

AppCompatActivity implements the LifecycleOwner interface
class MainActivity : AppCompatActivity() {

    fun test(a) {
        liveData {
            try {
                // ...
                emit("success")}catch(e: Exception) {
                emit("error")
            }
        }.observe(this, Observer {
            Log.d("LLL", it)
        })
    }
}
Copy the code

The ViewModel coroutines

For ViewModel, see liveData-viewModel of the Android-Jetpack component.

Add dependencies:

implementation "Androidx. Lifecycle: lifecycle - viewmodel - KTX: 2.2.0." "
Copy the code

The source code is as follows:

val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if(scope ! =null) {
                return scope
            }
            return setTagIfAbsent(JOB_KEY,
                CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
        }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close(a) {
        coroutineContext.cancel()
    }
}
Copy the code

Concurrency of coroutines

Start a hundred coroutines, each doing the same operation a thousand times, and measure their completion time for further comparison:

suspend fun massiveRun(action: suspend() - >Unit) {
    val n = 100  // The number of coroutines started
    val k = 1000 // The number of times each coroutine repeats the same action
    val time = measureTimeMillis {
        coroutineScope { // Scope of coroutines
            repeat(n) {
                launch {
                    repeat(k) { action() }
                }
            }
        }
    }
    println("Completed ${n * k} actions in $time ms")}var counter = 0

fun main(a) = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun {
            counter++
        }
    }
    println("Counter = $counter")}// output
Completed 100000 actions in 55 ms
Counter = 92805
Copy the code

Because a hundred coroutines incrementing counters in multiple threads at the same time but not doing concurrent processing, it is unlikely to output 100000. Using volatile does not solve this problem because volatile does not guarantee atomicity, only memory visibility. Can be solved in an inferior way:

Incrementing using the atomic class AtomicInteger

Thread restriction at a fine-grained level: All access to a particular shared state is restricted to a single thread

val counterContext = newSingleThreadContext("CounterContext")
var counter = 0

// Run slowly
fun main(a) = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun {
            // Limit each increment to a single-thread context
            withContext(counterContext) {
                counter++
            }
        }
    }
    println("Counter = $counter")}// output
Completed 100000 actions in 1652 ms
Counter = 100000
Copy the code

Coarse-grained threading: Threads are restricted to execute in large chunks of code

val counterContext = newSingleThreadContext("CounterContext")
var counter = 0

fun main(a) = runBlocking {
    // Restrict everything to a single-threaded context
    withContext(counterContext) {
        massiveRun {
            counter++
        }
    }
    println("Counter = $counter")}// output
Completed 100000 actions in 40 ms
Counter = 100000
Copy the code

Mutex: In addition to Java’s existing Mutex methods such as Lock, Kotlin provides the Mutex class, which has Lock and unlock methods. Lock is a suspend function that does not block a thread. You can use the withLock extension function instead of the usualmutex.lock(); try { …… } finally { mutex.unlock() }

val mutex = Mutex()
var counter = 0

// The locks in this example are fine-grained and therefore incur some cost.
fun main(a) = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun {
            // Protect each increment with a lock
            mutex.withLock {
                counter++
            }
        }
    }
    println("Counter = $counter")}// output
Completed 100000 actions in 640 ms
Counter = 100000
Copy the code

conclusion

This article mainly records the basic use of Kotlin coroutines. If you are interested in the working principle of Kotlin coroutines, you can read this article for an in-depth understanding of the working principle of Kotlin coroutines. It mainly tells that there are three layers of packaging in Kotlin coroutines, and the work of each layer is shown as follows:

Reference:

  • www.kotlincn.net/docs/refere…
  • Juejin. Cn/post / 684490…
  • Kaixue. IO/kotlin – coro…