kotlin协程的理解

  • Post author:
  • Post category:其他


*   协程到底是什么?

*   为什么需要 Kotlin Coroutines 提供的解决方案?

*   关于如何在 Android 中实现 Kotlin 协程的分步指南。

*   Kotlin 协程中的作用域是什么?

*   Kotlin 协程中的异常处理。

*   [通过示例学习 Kotlin Coroutines for Android 的项目](https://github.com/MindorksOpenSource/Kotlin-Coroutines-Android-Examples)。

协程,一个非常高效和完整的框架,以更高效和更简单的方式管理并发

什么是协程?

在这里,Co表示合作,Routines表示功能。

这意味着当函数相互协作时,我们称之为协程。

3bbc033e259eda4fba36c2a21ac161b1.png

让我们通过一个例子来理解这一点。为了便于理解,我以不同的方式编写了以下代码。假设我们有两个函数functionA和functionB。

函数A如下:

fun functionA(case: Int) {
    when (case) {
        1 -> {
            taskA1()
            functionB(1)
        }
        2 -> {
            taskA2()
            functionB(2)
        }
        3 -> {
            taskA3()
            functionB(3)
        }
        4 -> {
            taskA4()
            functionB(4)
        }
    }
}

功能B如下:

fun functionB(case: Int) {
    when (case) {
        1 -> {
            taskB1()
            functionA(2)
        }
        2 -> {
            taskB2()
            functionA(3)
        }
        3 -> {
            taskB3()
            functionA(4)
        }
        4 -> {
            taskB4()
        }
    }
}

然后,我们可以如下调用函数A:

functionA(1)

在这里,functionA将执行taskA1并将控制权交给functionB以执行taskB1。

然后,函数B将执行任务B1并将控制权交还给函数A以执行任务A2,依此类推。

重要的是functionA和functionB相互合作。

使用 Kotlin Coroutines 可以非常轻松地完成上述协作,而无需使用我在上面的示例中为了便于理解而使用的when或switch case 。

现在,我们已经了解了函数之间的协作是什么。由于功能的协作性,存在无限的可能性。

一些可能性如下:

它可以执行几行函数A,然后执行几行函数B,然后再执行几行函数A,依此类推。当线程闲置不做任何事情时,这将很有帮助,在这种情况下,它可以执行几行另一个函数。这样,它可以充分利用线程。最终,合作有助于多任务处理。

它将能够以同步方式编写异步代码。我们将在本文后面讨论这个问题。

总体而言,协程使多任务处理变得非常容易。

所以,我们可以说协程和线程都是多任务的。但不同的是,线程由操作系统管理,协程由用户管理,因为它可以利用协作执行几行功能。

它是一个在实际线程上编写的优化框架,利用函数的协作特性使其轻巧而强大。所以,我们可以说协程是轻量级线程。轻量级线程意味着它不会映射到本机线程,因此它不需要处理器上的上下文切换,因此它们更快。

Kotlin 实现了无堆栈协程——这意味着协程没有自己的堆栈,因此它们不会映射到本机线程上

Kotlin 实现了无堆栈协程——这意味着协程没有自己的堆栈,因此它们不会映射到本机线程上

##为什么需要 Kotlin 协程?

让我们以非常标准的 Android 应用程序用例为例,如下所示:

从服务器获取用户。

在 UI 中显示用户

fun fetchUser(): User {
    // make network call
    // return user
}


fun showUser(user: User) {
    // show user
}


fun fetchAndShowUser() {
    val user = fetchUser()
    showUser(user)
}

当我们调用fetchAndShowUser函数时,它会抛出NetworkOnMainThreadException,因为主线程上不允许网络调用。

有很多方法可以解决这个问题。其中一些如下:

1.使用回调:在这里,我们在后台线程中运行 fetchUser,并通过回调传递结果。

fun fetchAndShowUser() {
    fetchUser { user ->
        showUser(user)
    }
}

2.使用 RxJava:反应式世界方法。这样我们就可以摆脱嵌套回调。

fetchUser()
        .subscribeOn(Schedulers.io())
        .observerOn(AndroidSchedulers.mainThread())
        .subscribe { user ->
            showUser(user)
        }

3.使用协程:是的,协程。

suspend fun fetchAndShowUser() {
     val user = fetchUser() // fetch on IO thread
     showUser(user) // back on UI thread
}

在这里,上面的代码看起来是同步的,但实际上是异步的。我们将看看这怎么可能。

Android 中 Kotlin 协程的实现

在 Android 项目中添加 Kotlin Coroutines 依赖项,如下所示:

dependencies {
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

现在,我们的函数fetchUser将如下所示:

suspend fun fetchUser(): User {
    return GlobalScope.async(Dispatchers.IO) {
        // make network call
        // return user
    }.await()
}

逐步学习suspend、GlobalScope、async、await和Dispatchers.IO。

fetchAndShowUser 如下所示:

suspend fun fetchAndShowUser() {
    val user = fetchUser() // fetch on IO thread
    showUser(user) // back on UI thread
}

1.调度程序:调度程序帮助协程决定必须在哪个线程上完成工作。主要有三种类型的 Dispatcher,分别是IO、Default 和 Main。IO dispatcher 用来做网络和磁盘相关的工作。默认用于执行 CPU 密集型工作。Main 是 Android 的 UI 线程。为了使用这些,我们需要将工作包装在async函数下。异步函数如下所示。

suspend fun async() //

挂起:挂起函数是一个可以启动、暂停和恢复的函数。

8a7260408952ac0da9f07ef0ef870951.png

挂起函数只允许从协程或另一个挂起函数中调用。您可以看到包含关键字suspend的async函数。所以,为了使用它,我们需要让我们的函数也暂停。

因此,fetchAndShowUser只能从另一个挂起函数或协程调用。我们不能让Activity的 onCreate 函数挂起,所以我们需要从协程中调用它,如下所示:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    GlobalScope.launch(Dispatchers.Main) {
        fetchAndShowUser()
    }
    
}

这实际上是

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)




    GlobalScope.launch(Dispatchers.Main) {
        val user = fetchUser() // fetch on IO thread
        showUser(user) // back on UI thread
    }
    
}

howUser 将在 UI 线程上运行,因为我们使用了Dispatchers.Main来启动它。

Kotlin 中有两个函数可以启动协程,如下所示:

launch{}
async{}

Kotlin 协程中的启动与异步

不同之处在于launch{}不返回任何内容

async{}返回 的实例Deferred<T>,该实例具有await()返回协程结果的函数,就像我们在 Java 中使用 future.get() 来获取结果一样。

换一种说法:

launch:

async: 执行任务并返回结果

我们举个例子来学习launch和async。

我们有一个 fetchUserAndSaveInDatabase 函数,如下所示:

suspend fun fetchUserAndSaveInDatabase() {
    // fetch user from network
    // save user in database
    // and do not return anything
}

现在,我们可以使用如下启动:

GlobalScope.launch(Dispatchers.Main) {
    fetchUserAndSaveInDatabase() // do on IO thread
}

由于fetchUserAndSaveInDatabase不返回任何内容,我们可以使用启动来完成该任务,然后在主线程上执行一些操作。

但是当我们需要返回结果时,我们需要使用async。

我们有两个返回 User 的函数,如下所示:

suspend fun fetchFirstUser(): User {
    // make network call
    // return user
}


suspend fun fetchSecondUser(): User {
    // make network call
    // return user
}

现在,我们可以像下面这样使用异步:

GlobalScope.launch(Dispatchers.Main) {
    val userOne = async(Dispatchers.IO) { fetchFirstUser() }
    val userTwo = async(Dispatchers.IO) { fetchSecondUser() }
    showUsers(userOne.await(), userTwo.await()) // back on UI thread
}

有一种叫做withContext的东西。

suspend fun fetchUser(): User {
    return GlobalScope.async(Dispatchers.IO) {
        // make network call
        // return user
    }.await()
}
with

Context 只不过是另一种编写异步的方式,我们不必编写await()。

suspend fun fetchUser(): User {
    return withContext(Dispatchers.IO) {
        // make network call
        // return user
    }
}
withC

ontext但是关于和,我们还应该知道更多的事情await。

现在,让withContext我们在我们的async示例中并行使用fetchFirstUserand 。fetchSecondUser

GlobalScope.launch(Dispatchers.Main) {
    val userOne = withContext(Dispatchers.IO) { fetchFirstUser() }
    val userTwo = withContext(Dispatchers.IO) { fetchSecondUser() }
    showUsers(userOne, userTwo) // back on UI thread
}

当我们使用withContext时,它将串联而不是并行运行。这是一个主要区别。

规则:

withContext当您不需要并行执行时使用。

async仅在需要并行执行时使用。

两者withContext和async都可用于获得launch.

用于withContext返回单个任务的结果。

用于async并行运行的多个任务的结果


Kotlin 协程中的作用域

假设我们的活动是范围,一旦活动被销毁,后台任务应该被取消。

在活动中,我们需要实现 CoroutineScope

lass MainActivity :

AppCompatActivity(), CoroutineScope {


    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job


    private lateinit var job: Job


}

在 onCreate 和 onDestroy 函数中。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    job = Job() // create the Job
}


override fun onDestroy() {
    job.cancel() // cancel the Job
    super.onDestroy()
}

现在,只需使用如下启动:

launch {
    val userOne = async(Dispatchers.IO) { fetchFirstUser() }
    val userTwo = async(Dispatchers.IO) { fetchSecondUser() }
    showUsers(userOne.await(), userTwo.await())
}

一旦活动被销毁,如果它正在运行,任务将被取消,因为我们已经定义了范围。

当我们需要全局范围,即我们的应用范围,而不是活动范围时,我们可以使用 GlobalScope,如下所示:

GlobalScope.launch(Dispatchers.Main) {
    val userOne = async(Dispatchers.IO) { fetchFirstUser() }
    val userTwo = async(Dispatchers.IO) { fetchSecondUser() }
}

在这里,即使活动被破坏, fetchUser 函数将继续运行,因为我们使用了GlobalScope。

如果其中一个网络调用失败,它将直接进入catch区块。

但是假设,我们想为失败的网络调用返回一个空列表,并继续来自另一个网络调用的响应。我们可以将try-catch 块添加到单个网络调用中,如下所示:

launch {
    try {
        val users = try {
            getUsers()
        } catch (e: Exception) {
            emptyList<User>()
        }
        val moreUsers = try {
            getMoreUsers()
        } catch (e: Exception) {
            emptyList<User>()
        }
    } catch (exception: Exception) {
        Log.d(TAG, "$exception handled !")
    }
}

这样,如果出现任何错误,它将继续使用空列表。

现在,如果我们想并行进行网络调用怎么办。我们可以使用async.

launch {
    try {
        val usersDeferred = async {  getUsers() }
        val moreUsersDeferred = async { getMoreUsers() }
        val users = usersDeferred.await()
        val moreUsers = moreUsersDeferred.await()
    } catch (exception: Exception) {
        Log.d(TAG, "$exception handled !")
    }
}

在这里,我们将面临一个问题,如果出现任何网络错误,应用程序就会崩溃!,它不会去catch块。

为了解决这个问题,我们将不得不使用coroutineScope下面的方法:

launch {
    try {
        coroutineScope {
            val usersDeferred = async {  getUsers() }
            val moreUsersDeferred = async { getMoreUsers() }
            val users = usersDeferred.await()
            val moreUsers = moreUsersDeferred.await()
        }
    } catch (exception: Exception) {
        Log.d(TAG, "$exception handled !")
    }
}

现在,如果出现任何网络错误,它将进入catch块。

但是再次假设,我们想为失败的网络调用返回一个空列表,并继续来自另一个网络调用的响应。我们将不得不使用supervisorScope并将try-catch块添加到单个网络调用中,如下所示:

launch {
    try {
        supervisorScope {
            val usersDeferred = async { getUsers() }
            val moreUsersDeferred = async { getMoreUsers() }
            val users = try {
                usersDeferred.await()
            } catch (e: Exception) {
                emptyList<User>()
            }
            val moreUsers = try {
                moreUsersDeferred.await()
            } catch (e: Exception) {
                emptyList<User>()
            }
        }
    } catch (exception: Exception) {
        Log.d(TAG, "$exception handled !")
    }
}

同样,这样,如果出现任何错误,它将继续使用空列表。

这就是supervisorScope帮助。

结论:

虽然不使用async,但我们可以继续使用try-catchorCoroutineExceptionHandler并根据我们的用例实现任何目标。

在使用async时,除了try-catch,我们还有两个选择:coroutineScope和supervisorScope。

使用async,当您想要在其中一个或部分任务失败时继续执行其他任务时,除了顶级任务之外,还可以supervisorScope与个人一起使用。try-catchtry-catch

使用async,coroutineScope与顶级 一起使用try-catch,当您不想继续其他任务(如果其中任何一个任务失败)时。

a81a5d98b80f9ba9c44bb9dab22c65dc.png