Kotlin Multiplatform. Kami bekerja dengan multithreading dalam praktiknya. Bagian 2

Selamat siang untuk semuanya! Saya bersama Anda, Anna Zharkova, pengembang seluler terkemuka di Usetech.

Pada artikel sebelumnya, saya telah membahas salah satu cara mengimplementasikan multithreading di aplikasi Multiplatform Kotlin. Hari ini kami akan mempertimbangkan situasi alternatif saat kami mengimplementasikan aplikasi dengan kode umum yang paling banyak dibagikan, mentransfer semua pekerjaan dengan utas ke dalam logika umum.





Dalam contoh sebelumnya, kami dibantu oleh pustaka Ktor, yang mengambil alih semua pekerjaan utama menyediakan asinkron di klien jaringan. Ini menyelamatkan kami dari keharusan menggunakan DispatchQueue di iOS dalam kasus khusus itu, tetapi di kasus lain kami harus menggunakan pekerjaan antrian run untuk menjalankan logika bisnis dan menangani respons. Di sisi Android, kami menggunakan MainScope untuk memanggil fungsi yang ditangguhkan.



Jadi, jika kita ingin mengimplementasikan pekerjaan seragam dengan multithreading dalam proyek umum, maka kita perlu mengkonfigurasi dengan benar cakupan dan konteks coroutine yang akan dieksekusi.

Mari kita mulai dengan sederhana. Mari buat mediator arsitektural kita yang akan memanggil metode layanan pada cakupannya, yang diperoleh dari konteks coroutine:

class PresenterCoroutineScope(context: CoroutineContext) : CoroutineScope {
    private var onViewDetachJob = Job()
    override val coroutineContext: CoroutineContext = context + onViewDetachJob

    fun viewDetached() {
        onViewDetachJob.cancel()
    }
}

//   
abstract class BasePresenter(private val coroutineContext: CoroutineContext) {
    protected var view: T? = null
    protected lateinit var scope: PresenterCoroutineScope

    fun attachView(view: T) {
        scope = PresenterCoroutineScope(coroutineContext)
        this.view = view
        onViewAttached(view)
    }
}

      
      





Kami memanggil layanan dalam metode mediator dan meneruskannya ke UI kami:

class MoviesPresenter:BasePresenter(defaultDispatcher){
    var view: IMoviesListView? = null

    fun loadData() {
        //  
        scope.launch {
            service.getMoviesList{
                val result = it
                if (result.errorResponse == null) {
                    data = arrayListOf()
                    data.addAll(result.content?.articles ?: arrayListOf())
                    withContext(uiDispatcher){
                    view?.setupItems(data)
                   }
                }
            }
        }

//IMoviesListView - /,    UIViewController  Activity. 
interface IMoviesListView  {
  fun setupItems(items: List<MovieItem>)
}
class MoviesVC: UIViewController, IMoviesListView {
private lazy var presenter: IMoviesPresenter? = {
       let presenter = MoviesPresenter()
        presenter.attachView(view: self)
        return presenter
    }()

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        presenter?.attachView(view: self)
        self.loadMovies()
    }

    func loadMovies() {
        self.presenter?.loadMovies()
    }

   func setupItems(items: List<MovieItem>){}
//....

class MainActivity : AppCompatActivity(), IMoviesListView {
    val presenter: IMoviesPresenter = MoviesPresenter()

    override fun onResume() {
        super.onResume()
        presenter.attachView(this)
        presenter.loadMovies()
    }

   fun  setupItems(items: List<MovieItem>){}
//...

      
      







Untuk membuat cakupan dari konteks coroutine dengan benar, kita perlu menyiapkan dispatcher coroutine.

Logika ini bergantung pada platform, jadi kami menggunakan penyesuaian dengan ekspektasi / aktual.

expect val defaultDispatcher: CoroutineContext

expect val uiDispatcher: CoroutineContext

      
      





uiDispatcher akan bertanggung jawab untuk mengerjakan thread UI. defaultDispatcher akan digunakan untuk bekerja di luar thread UI.

Cara termudah untuk membuatnya adalah di androidMain, karena Kotlin JVM memiliki implementasi yang sudah jadi untuk dispatcher coroutine. Untuk mengakses aliran yang sesuai, kami menggunakan CoroutineDispatchers Main (aliran UI) dan Default (standar untuk Coroutine):

actual val uiDispatcher: CoroutineContext
    get() = Dispatchers.Main

actual val defaultDispatcher: CoroutineContext
    get() = Dispatchers.Default
      
      







MainDispatcher dipilih untuk platform di bawah kap CoroutineDispatcher menggunakan pabrik dispatcher MainDispatcherLoader:



internal object MainDispatcherLoader {

    private val FAST_SERVICE_LOADER_ENABLED = systemProp(FAST_SERVICE_LOADER_PROPERTY_NAME, true)

    @JvmField
    val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()

    private fun loadMainDispatcher(): MainCoroutineDispatcher {
        return try {
            val factories = if (FAST_SERVICE_LOADER_ENABLED) {
                FastServiceLoader.loadMainDispatcherFactory()
            } else {
                // We are explicitly using the
                // `ServiceLoader.load(MyClass::class.java, MyClass::class.java.classLoader).iterator()`
                // form of the ServiceLoader call to enable R8 optimization when compiled on Android.
                ServiceLoader.load(
                        MainDispatcherFactory::class.java,
                        MainDispatcherFactory::class.java.classLoader
                ).iterator().asSequence().toList()
            }
            @Suppress("ConstantConditionIf")
            factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
                ?: createMissingDispatcher()
        } catch (e: Throwable) {
            // Service loader can throw an exception as well
            createMissingDispatcher(e)
        }
    }
}

      
      







Ini sama dengan Default:

internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
    val IO: CoroutineDispatcher = LimitingDispatcher(
        this,
        systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)),
        "Dispatchers.IO",
        TASK_PROBABLY_BLOCKING
    )

    override fun close() {
        throw UnsupportedOperationException("$DEFAULT_DISPATCHER_NAME cannot be closed")
    }

    override fun toString(): String = DEFAULT_DISPATCHER_NAME

    @InternalCoroutinesApi
    @Suppress("UNUSED")
    public fun toDebugString(): String = super.toString()
}

      
      







Namun, tidak semua platform memiliki implementasi dispatcher coroutine. Misalnya, untuk iOS, yang berfungsi dengan Kotlin / Native, bukan Kotlin / JVM.

Jika kita mencoba menggunakan kode tersebut, seperti di Android, kita akan mendapatkan error:





Mari kita lihat apa yang kita lakukan.



Masalah 470 dari GitHub Kotlin Coroutines berisi informasi bahwa dispatcher khusus belum diterapkan untuk iOS:





Masalah 462 , 470 bergantung, masih sama dalam status Terbuka:





Solusi yang disarankan adalah membuat dispatcher Anda sendiri untuk iOS:

actual val defaultDispatcher: CoroutineContext
get() = IODispatcher

actual val uiDispatcher: CoroutineContext
get() = MainDispatcher

private object MainDispatcher: CoroutineDispatcher(){
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        dispatch_async(dispatch_get_main_queue()) {
            try {
                block.run()
            }catch (err: Throwable) {
                throw err
            }
        }
    }
}

private object IODispatcher: CoroutineDispatcher(){
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong(),
0.toULong())) {
            try {
                block.run()
            }catch (err: Throwable) {
                throw err
            }
        }
    }

      
      







Kami akan mendapatkan kesalahan yang sama saat memulai.

Pertama, kita tidak dapat menggunakan dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong (), 0.toULong ())), karena tidak terikat ke utas apa pun di Kotlin / Native:



Kedua, Kotlin / Native tidak seperti Kotlin / JVM tidak dapat meraba-raba coroutine di antara utas. Dan juga objek yang bisa berubah.

Oleh karena itu, kami menggunakan MainDispatcher dalam kedua kasus:

actual val ioDispatcher: CoroutineContext
get() = MainDispatcher

actual val uiDispatcher: CoroutineContext
get() = MainDispatcher


@ThreadLocal
private object MainDispatcher: CoroutineDispatcher(){
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        dispatch_async(dispatch_get_main_queue()) {
            try {
                block.run().freeze()
            }catch (err: Throwable) {
                throw err
            }
        }
    }

      
      







Agar kita dapat mentransfer blok kode dan objek yang bisa berubah antar utas, kita perlu membekukannya sebelum mentransfernya menggunakan perintah freeze ():



Namun, jika kita mencoba membekukan objek yang sudah dibekukan, misalnya, singletons, yang dianggap dibekukan secara default, kita dapatkan FreezingException.

Untuk mencegah hal ini terjadi, kami menandai lajang dengan anotasi @ThreadLocal, dan variabel global @SharedImmutable:

/**
 * Marks a top level property with a backing field or an object as thread local.
 * The object remains mutable and it is possible to change its state,
 * but every thread will have a distinct copy of this object,
 * so changes in one thread are not reflected in another.
 *
 * The annotation has effect only in Kotlin/Native platform.
 *
 * PLEASE NOTE THAT THIS ANNOTATION MAY GO AWAY IN UPCOMING RELEASES.
 */
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
public actual annotation class ThreadLocal

/**
 * Marks a top level property with a backing field as immutable.
 * It is possible to share the value of such property between multiple threads, but it becomes deeply frozen,
 * so no changes can be made to its state or the state of objects it refers to.
 *
 * The annotation has effect only in Kotlin/Native platform.
 *
 * PLEASE NOTE THAT THIS ANNOTATION MAY GO AWAY IN UPCOMING RELEASES.
 */
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
public actual annotation class SharedImmutable

      
      







Menggunakan MainDispatcher dalam kedua kasus tidak masalah saat bekerja dengan Ktor. Jika kita ingin permintaan berat kita disimpan di latar belakang, maka kita dapat mengirimnya ke GlobalScope dengan dispatcher utama Dispatchers.Main / MainDispatcher sebagai konteksnya:

iOS

actual fun ktorScope(block: suspend () -> Unit) {
    GlobalScope.launch(MainDispatcher) { block() }
}

      
      







Android:

actual fun ktorScope(block: suspend () -> Unit) {
           GlobalScope.launch(Dispatchers.Main) { block() }
       }

      
      





Panggilan dan perubahan konteks kemudian akan ada di layanan kami:

suspend fun loadMovies(callback:(MoviesList?)->Unit) {
       ktorScope {
            val url =
                "http://api.themoviedb.org/3/discover/movie?api_key=KEY&page=1&sort_by=popularity.desc"
            val result = networkService.loadData<MoviesList>(url)
            delay(1000)
           withContext(uiDispatcher) {
               callback(result)
           }
        }
    }

      
      





Dan bahkan jika Anda tidak hanya memanggil fungsionalitas Ktor di sana, semuanya akan berfungsi.



Anda juga dapat menerapkan di iOS panggilan blok dengan transfer ke DispatchQueue latar belakang seperti ini:

//  ,  ,   
actual fun callFreeze(callback: (Response)->Unit) {
    val block = {
      //     ,    
        callback(Response("from ios").freeze())
    }
    block.freeze()
    dispatch_async {
        queue = dispath_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND.toLong, 
            0.toULong())
        block = block     
    }
}

      
      







Tentu saja, Anda harus menambahkan callFreeze yang sebenarnya menyenangkan (...) di sisi Android juga, tetapi hanya meneruskan respons Anda ke callback.



Hasilnya, setelah melakukan semua pengeditan, kami mendapatkan aplikasi yang berfungsi sama di kedua platform:





Contoh sumber github.com/anioutkazharkova/movies_kmp

Ada contoh serupa, tetapi tidak di bawah Kotlin 1.4

github.com/anioutkazharkova/kmp_news_sample



tproger.ru/articles/creating-an -app-untuk-kotlin-multiplatform

github.com/JetBrains/kotlin-native

github.com/JetBrains/kotlin-native/blob/master/IMMUTABILITY.md

github.com/Kotlin/kotlinx.coroutines/issues/462

helw.net / 2020/04/16 / multithreading-in-kotlin-multiplatform-apps






All Articles