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…