Асинхронното и едновременното програмиране играе важна роля в настоящия свят на уеб API и микроуслуги, където значителна част от нашия код е за организиране на мрежови взаимодействия. Използването на традиционни синхронни модели, при които нишките се блокират, докато чакат външни отговори, не е подходящо за платформи.

Работата с нишките, синхронизацията и блокировките би било последното нещо, което човек би искал, докато пише асинхронни приложения.

Какво предлага Kotlin, за да улесни живота ни?

Kotlin предлага Coroutines. Технически съпрограмата е екземпляр на изчисление с възможност за спиране. Съпрограмите не са обвързани с определена нишка и могат да спрат изпълнението в една нишка и да продължат в друга.

Kotlin Coroutines опростяват асинхронното програмиране, като поставят усложненията в библиотеки. Логиката на програмата може да бъде изразена последователно в съпрограма и основната библиотека ще разбере асинхронността вместо нас.

Въведение в Coroutines

Какво е спиращо изчисление или спираща функция?

Суспендиращата функция на Kotlin е тази, която може да спре сърутина, като се върне незабавно към нейния повикващ и някак си „освободи“ хостинг нишката и възобнови нейното изпълнение в бъдещ момент, потенциално в друга нишка .

По какво се различава функцията за спиране от нормалната функция?

Декларацията на функцията за спиране има префикс с ключова дума спиране. Останалият подпис обаче остава непроменен. Функция за спиране може да бъде извикана само от друга функция за спиране или конструктор на съпрограма.

suspend fun findAll(): List<User> {
  ....
}

Какво е конструктор на сърутина?

Конструктор на съпрограма е функция, която взема спираща ламбда, създава съпрограма и може да върне резултат. Kotlin предоставя различни конструктори на съпрограми - runBlocking, async и launch и др.

Как бихте извикали горната функция findAll() с помощта наконструктор на съпрограми?

Бих използвал конструктор на съпрограми, например async, за да извикам функцията findAll(), както е споменато по-долу-

fun main(args: Array<String>) {
   println("coroutine example")         ---------- (1)
   async {                              ---------- (2)  
      println("invoking findAll()")     ---------- (3)
      findAll()
   }
   println("main thread done")          ---------- (4) 
}

Какъв е потокът на изпълнение за горната функция main()?

  • основната функция се изпълнява от Main Thread и изпълнява оператор (1)
  • async в Statement (2) създава коррутина, която е присвоена на нишка от ForkJoinPool [проверете Coroutine контекст и диспечери] и изпълнява Statement (3), ако нишката преминава в работещо състояние. Тъй като findAll е спряна функция, съпрограмата може да бъде спряна
  • Основната нишка не е блокирана и продължава с изпълнението и изпълнява оператор (4)
  • В този пример, тъй като основната нишка не изчаква завършването на съпрограмата, тя може да завърши преди завършването на съпрограмата

Какво имате предвид, когато казвате „коррутина може да бъде спряна“?

Ако съпрограмата включва суспендирана функция, която включва IO операция или трябва да изчака нейното изпълнение, съпрограмата ще бъде спряна. Библиотеката може да реши да продължи без спиране, ако резултатът за въпросното повикване вече е наличен.

Как да накарам главната нишка да чака завършване на съпрограмата?

fun main(args: Array<String>) = runBlocking<Unit> {  ------ (1)
    val job = launch {                               ------ (2)  
        delay(1000L)
        println("World!")                            ------ (5)
    }
    println("Hello,")                                ------ (3)
    job.join()                                       ------ (4) 
}
  • Основната нишка, която извиква runBlocking в израз (1), блокирадокато съпрограмата вътре в runBlocking завърши
  • Изявление (2) създава нова съпрограма във фонов режим и връща екземпляр на Job
  • Основната съпрограма продължава с израз (3)
  • Изявление (4) изчаква работата да приключи. join() е функция с възможност за спиране и трябва да се изпълнява в конструктор на сърутина, който в този случай е runBlocking
  • Съпрограмата, създадена в израз (2), започва в различна нишка, намира функция на библиотеката, наречена delay()която може да бъде суспендирана и води до суспендиране на съпрограмата. След спиране може да възобнови изпълнението си в друга нишка и да завърши с оператор (5)

Как да получа резултата от функция за спиране?

Kotlin предоставя друг създател на сърутина, наречен async, който връща екземпляр на Deferred‹T›. Извикването на await на екземпляр на Deferred блокира съпрограмата до завършване (не нишката).

fun main(args: Array<String>) {
    println("starting: ${Thread.currentThread().name}")
    val job: Deferred<Int> = async {
        println("coroutine: ${Thread.currentThread().name}")
        value()
    }

    runBlocking {
        val value = job.await()
        println("value is: $value")
    }

    println("done ${Thread.currentThread().name}")
}

suspend fun value() : Int {
    delay(200)
    return 2
}

Горният код произвежда следния изход-

starting: main
coroutine: ForkJoinPool.commonPool-worker-1
value is: 2
done main

Корутините на Kotlin леки ли са?

Да те са. Съпрограмата не се съпоставя към родна нишка. Той не свързва никакви собствени ресурси и в най-простия случай използва сравнително малки обекти в JVM heap. Тъй като съпрограмата е потребителска абстракция, превключването между съпрограмите изобщо не включва ядрото на ОС. Може да бъде толкова евтино, колкото извикването на обикновена функция. Следователно можете да имате много съпрограми, които могат да бъдат изпълнени от малък брой нишки.

runBlocking<Unit> {
    val jobs = (1..100_000).map {
        launch {
            delay(1000L)
            print(".")
        }
    }
    jobs.forEach { it.join() }
}

Горният код създава 100K съпрограми. Кодът по-горе отнема около 1,1 секунди, за да завърши на MacBook Pro, 2,8 GHz Intel Core i7 с 16GB 1600MHz DDR3.

Контекст на сърутина и диспечери

Coroutines винаги се изпълняват в някакъв контекст, който е представен от стойността на типа CoroutineContext, дефиниран в стандартната библиотека на Kotlin.

Контекстът на съпрограма включва диспечер на съпрограма, който определя коя нишка или нишки използва съответната съпрограма за своето изпълнение. Диспетчерът на съпрограми може да ограничи изпълнението на съпрограмата до конкретна нишка, да я изпрати в пул от нишки или да я остави да работи неограничено.

Диспечерът по подразбиране, който използвахме в предишните раздели, е представен от DefaultDispatcher, който е равен на CommonPool в текущата реализация на Kotlin.

И така, стартирането { … } е същото като launch(DefaultDispatcher) { … }, което е същото като launch(CommonPool) { … }.

fun main(args: Array<String>) = runBlocking<Unit> {
    val jobs = arrayListOf<Job>()
    jobs += launch(coroutineContext) { 
        println("'coroutineContext':${Thread.currentThread().name}")
    }
    jobs += launch(CommonPool) {       
        println("'CommonPool': ${Thread.currentThread().name}")
    }
    jobs += launch(newSingleThreadContext("MyOwnThread")) { 
        println("'newSTC': ${Thread.currentThread().name}")
    }
    jobs += launch(newFixedThreadPoolContext(3,"MyPool")) {
         println("'newFixedPool': ${Thread.currentThread().name}")
    }    
    jobs.forEach { it.join() }
}

произвежда следния изход -

'CommonPool': ForkJoinPool.commonPool-worker-1
'newSTC': MyOwnThread
'newFixedPool': MyPool-1
'coroutineContext':main

Това означава ли, че мога да използвам различен сърутинен диспечер за различни задачи?

Да, можеш. Ако приемем, че една от съпрограмите включва IO интензивни задачи, може да използва пул от нишки, запазен за IO задачи, а другата съпрограма може да използва пул от нишки за задачи с интензивно изчисление.

fun main(args: Array<String>) = runBlocking<Unit> {
    val job = launch(coroutineContext) {  ----- (1)
        println("Running under: ${Thread.currentThread().name}")
        val one = async(CommonPool) {     ----- (2)
            delay(1000);
            println("Running(1): ${Thread.currentThread().name}")
            1
        }
        val two = async(newFixedThreadPoolContext(4, "pool")) { -(3)
            delay(1000);
            println("Running(2):${Thread.currentThread().name}")
            2
        }
        println("The answer is: ${one.await() + two.await()}")
    }
    job.join()
}

Кодът по-горе използва различни групи от нишки за различни съпрограми. Той произвежда следния изход -

Running under: main
Running(2): pool-2
Running(1): ForkJoinPool.commonPool-worker-1
The answer is: 3
  • Изявление (1) създава съпрограма, която се изпълнява от Main Thread
  • Изявление (2) създава съпрограма, която се изпълнява от нишка от Common Pool =› ForkJoinPool
  • Изявление (3) създава съпрограма, която се изпълнява от нишка от нашия персонализиран набор от фиксирани нишки

Как Kotlin внедри Coroutines?

Kotlin прилага стил, наречен „стил на предаване на продължение“. Спряната съпрограма може да се съхранява и предава като обект, който запазва суспендираното си състояние и локалните. Следователно функциите за спиране приемат допълнителен параметър от тип Continuation под капака.

Препратки