关于Kotlin协程的一些应用

前语

首要关于Kotlin协程,我早前发布过一些系列文章 【传送门】。惭愧,感觉自己写的并不是很好。

又看了一下自己写的Kotlin协程的实战应用,【传送门】。感觉也并不是很全,还需求弥补。

其次,我看到一些友友们的代码,和一些同事的代码,实战中的一些相关的优化的进程。(在文章后边会和咱们一同参阅参阅)

本文的知识点:Kotlin协程的扩展的知识点关于协程与Java线程池的对比,协程的去掉回调,协程效果域等等。

对于这些Kotlin协程的小知识点碎片化的做一些整理,下面一同来看看吧。

一、Kotlin协程与Java线程池的对比

提到这一点也许许多人就得出了定论了,Kotlin协程便是线程池,实质便是线程池,没什么大不了的,便是对线程池的封装。

额…这么说确实没错,可是不行全面,我觉得应该这么说:Kotlin协程依据Java线程池,可是高于Java线程池。协程的内部对线程池的操作有一些特有的优化策略。

关键点是尾部调用和使命盗取。

尾部调用:

场景:登录->获取用户信息->保存用户信息到DB

假如线程池的线程资源比较富余,一个使命到来就分配到一个线程,上述的场景中明显是上下相关的依靠关系,需求先登录,再获取到用户信息,然后保存到数据库。

假如三个使命一同到来,分为ABC三个线程来别离履行,那么首要就需求履行A,BC线程就会被堵塞,从而导致资源的浪费。

而协程履行此类的场景就不会,它能够最大限度的保证一个连续的使命在同一个线程中履行,尽可能的节约线程资源。

中心代码在这里:kotlinx.coroutines.scheduling.CoroutineScheduler

Kotlin协程-与线程池的对比 & 铺平回调 & 协程分发器

内部的中心代码又调用到这里:kotlinx.coroutines.scheduling.WorkQueue

Kotlin协程-与线程池的对比 & 铺平回调 & 协程分发器

假如是增加到大局的使命行列中,也是相同的尾部调用

Kotlin协程-与线程池的对比 & 铺平回调 & 协程分发器

注:上面的大局行列与本地行列是 CoroutineScheduler 类与 Worker 中界说的,CoroutineScheduler中有两个大局行列,Worker中一个本地行列。

协程线程池会优先复用已有的线程使命,假如有就会把使命加到已有的work使命的本地行列里。否则会重新唤起或许创立线程。

这便是尾部调用机制,也被咱们简称为尾调。

使命盗取:

上面的行列履行的办法界说与增加之后就能够运用行列while循环履行了。

代码如下:kotlinx.coroutines.scheduling.CoroutineScheduler.Worker

runWorker()

Kotlin协程-与线程池的对比 & 铺平回调 & 协程分发器

这些代码都容易看,便是遍历找使命履行的逻辑。关键便是内部findTask的骚操作:

Kotlin协程-与线程池的对比 & 铺平回调 & 协程分发器

看办法名就知道了,尝试盗取使命,这,协程你是真的骚。

Kotlin协程-与线程池的对比 & 铺平回调 & 协程分发器

逻辑为:假如自己的本地行列没使命了,自己对应的大局行列也没有使命了,去其他的Work里面找使命履行。

这便是使命盗取机制。这也是Kotlin协程与Java线程池有所差异的最大两个点。

二、Kotlin协程怎么去除回调

为什么讲到这个,是因为真的许多人并不知道这一点,把Kotlin写出了Java的感觉,在协程里面还处处搞一些回调/高阶函数之类的,损坏了协程的效果域。

协程的高雅之处便是把异步调用变成类似同步的效果,去除了回调的逻辑,看起来也更容易了解,也不会损坏协程的效果域。

suspendCoroutine:

例如咱们写一个接口回调:

interface OnSingleMethodCallback {
    fun onValueCallback(value: String)
}

运用回调:

    fun runMethodTask(callback: OnSingleMethodCallback) {
        Thread {
            Thread.sleep(1000)
            callback.onValueCallback("abc")
        }.start()
    }
    fun runMethodTask(){
        runMethodTask(object : OnSingleMethodCallback {
            override fun onValueCallback(value: String) {
                YYLogUtils.w("value:$value")
            }
        })
    }

假如咱们运用 suspendCoroutine:

    suspend fun runMethodTaskWithSuspend(): String {
        return suspendCoroutine { continuation ->
            runMethodTask(object : OnSingleMethodCallback {
                override fun onValueCallback(value: String) {
                    continuation.resume(value)
                }
            })
        }
    }

可是咱们也需求留意,这是协程的写法,办法也标明晰 suspend ,所以只能在协程中运用。

假如是一个网络恳求有成功与失利的回调,那么咱们也能运用 suspendCancellableCoroutine 来达到效果:

interface ICallBack {
    fun onSuccess(data: String)
    fun onFailure(t: Throwable)
}
 private fun request(callback: ICallBack) {
   thread {
     try {
       callback.onSuccess("success")
     } catch (e: Exception) {
       callback.onFailure(e)
     }
   }
 }
 private fun requestDefault() {
  request(object : ICallBack {
    override fun onSuccess(data: String) {
        // ...
    }
    override fun onFailure(t: Throwable) {
      // ...
    }
  })
}

假如运用 suspendCancellableCoroutine 的话就变成这样:


 private suspend fun requestWithSuspend(): String {
        return suspendCancellableCoroutine { cancellableContinuation->
            request(object : ICallBack {
                override fun onSuccess(data: String) {
                    cancellableContinuation.resume(data)
                }
                override fun onFailure(t: Throwable) {
                    cancellableContinuation.resumeWithException(t)
                }
            })
        }
    }

相同需求留意是,这是协程的写法,办法也标明晰 suspend ,所以只能在协程中运用。

其实为什么Retrofit的恳求看似把异步的网络恳求用成了同步相同,Retrofit的内部也是相同的处理。

Retrofit最终的处理逻辑在此:KotlinExtensions.awaitResponse

Kotlin协程-与线程池的对比 & 铺平回调 & 协程分发器

所以咱们照着Retrofit学就行了。

三、Kotlin协程分发器

有没有同学悉数用 Dispatchers.IO 切换线程调度的。

Dispatchers.IO / Dispatchers.Default 的异同:

两者都是协程分发器,Dispatchers.IO 侧重于使命本身是堵塞型的,比方文件、数据库、网络等操作等。并不那么占用CPU

而Dispatchers.Default 则倾向那些可能会长时间占用CPU的使命。比方人脸特征提取,图片压缩处理,视频的组成等。

他们的线程池的实现也是不同的

协程线程池在规划的时候,针对两者在线程的调度策略上有所不同。

一切使命分成纯CPU使命和非纯CPU使命两种,对应着中心线程和非中心线程。

入队的逻辑是 Dispatchers.IO 的使命放入 globalBlockingQueue 行列,而 Dispatchers.Default 的使命放入的是 globalBlockingQueue 行列。

一切线程在履行前都先尝试成为中心线程,中心线程能够从两种使命中任意挑选履行,非中心线程只能履行非纯CPU使命。中心线程假如挑选履行非纯CPU使命会变成非中心线程。

所以真的有人从来没用过 Dispatchers.Default 吗?

四、运用协程有什么优点?怎么用?

看到过网上的一些Java线程池比协程线程池履行逻辑更快的文章,其实含义不大,协程最大的优势是会愈加的方便,能够很方便的把一些碎片化的办法参加协程,一同它能够去掉回调阴间还能愈加方便的实现并发与排队履行的效果。

比方这样的一个场景,在主线程核算薪水,咱们依据时薪与作业时长核算一共的薪水,内部有杂乱的判断,是否是签约职工,是否迟到了,迟到了扣钱,扣除五险一金,连续作业的奖赏,引荐的奖赏,顾客打赏,等等一系列的杂乱逻辑,咱们就能够随意参加协程中。

    private fun calculateSalary(): String {
        // 省掉100行代码
        return "1000"
    }
    private suspend fun calculateSalary2() = withContext(Dispatchers.Default) {
        // 省掉100行代码
        "2000"
    }
    private suspend fun calculateSalary3() = coroutineScope {
        // 省掉100行代码
        "3000"
    }

下面看看代码的优化:

class CalculateFaceUtil private constructor() : CoroutineScope by MainScope() {
    //...  单例
    /**
     * 核算并找到最匹对的人脸信息
     *
     * 运用协程异步的并发的双端遍历查询最大值
     */
    fun getTopFace(
        list: List<FaceRegisterInfo>,
        faceEngine: FaceEngine,
        faceFeature: FaceFeature,
        action: (similar: Float, index: Int) -> Unit
    ) {
      // 、、、其他逻辑
       val middlePosition = list.size / 2
        launch(Dispatchers.IO) {
            val topface1 = async {
                val tempFaceFeature = FaceFeature()
                val faceSimilar = FaceSimilar()
                var maxSimilar = 0f
                var maxSimilarIndex = -1
                for (i in 0 until middlePosition) {
                    tempFaceFeature.featureData = list[i].featureData
                    //调用SDK比对两个 FaceFeature 人脸特征,返回类似度
                    faceEngine.compareFaceFeature(faceFeature, tempFaceFeature, faceSimilar)
                    //拿到类似度的目标,获取得分(每一次都会悉数遍历,假如有相同的图片仍是会取到最终的)
                    if (faceSimilar.score > maxSimilar) {
                        maxSimilar = faceSimilar.score
                        maxSimilarIndex = i
                    }
                }
                TopFace(maxSimilar, maxSimilarIndex)
            }
            val topface2 = async {
                val tempFaceFeature = FaceFeature()
                val faceSimilar = FaceSimilar()
                var maxSimilar = 0f
                var maxSimilarIndex = -1
                for (i in middlePosition until list.size) {
                    tempFaceFeature.featureData = list[i].featureData
                    //调用SDK比对两个 FaceFeature 人脸特征,返回类似度
                    faceEngine.compareFaceFeature(faceFeature, tempFaceFeature, faceSimilar)
                    //拿到类似度的目标,获取得分(每一次都会悉数遍历,假如有相同的图片仍是会取到最终的)
                    if (faceSimilar.score > maxSimilar) {
                        maxSimilar = faceSimilar.score
                        maxSimilarIndex = i
                    }
                }
                TopFace(maxSimilar, maxSimilarIndex)
            }
            //并发查找并找到最大值
            val face1 = topface1.await()
            val face2 = topface2.await()
            if (face1 != null && face2 != null) {
                //回调到主线程
                withContext(Dispatchers.Main) {
                    //优先返回后边的数据
                    if (face2.similar > face1.similar) {
                        action(face2.similar, face2.index)
                    } else {
                        action(face1.similar, face1.index)
                    }
                }
            }
        }
    }
}

场景:ViewModel中调用这个东西类,查找较大调集中最匹配的人脸,运用头尾双端遍历找到最大值。

咱们以这一个运用场景为例,逻辑没问题,可是协程的运用有优化的空间。

1.核算最好运用 Dispatchers.Default , 这是小问题。 2.viewModel中viewModelScope协程效果域中调用大局的协程效果域,这…感觉不太好,引荐运用下面的办法,继承父布局的协程效果域。

    suspend fun getTopFace() = coroutineScope {
        async(Dispatchers.Default) {
        }
        async(Dispatchers.Default) {
        }
        // 。。。
    }

3.运用高阶函数回调,假如是协程中最好是能够铺平回调

    suspend fun getTopFace(
        list: List<FaceRegisterInfo>,
        faceEngine: FaceEngine,
        faceFeature: FaceFeature,
    ): TopFace = suspendCoroutine { continuation ->
        async(Dispatchers.Default) {
        }
        async(Dispatchers.Default) {
        }
        // 。。。
        continuation.resume(TopFace(maxSimilar, maxSimilarIndex))
    }

之前的viewModel中运用:

  private fun searchFace(
        frFace: FaceFeature, requestId: Int,
        orignData: ByteArray?, faceInfo: FaceInfo?, width: Int, height: Int
    ) {
        viewModelScope.launch {
            //经过FaceServer找到最匹配的人脸
            CalculateFaceUtil.getInstance().getTopFace(frFace) { compareResult ->
               // 。。。逻辑
            }
        }
    }

在回调里面写逻辑,后边的逻辑就损坏了协程效果域,那么又要运用东西类敞开一个新的协程,这样就很欠好。

现在的viewModel中运用:

  private fun searchFace(
        frFace: FaceFeature, requestId: Int,
        orignData: ByteArray?, faceInfo: FaceInfo?, width: Int, height: Int
    ) {
        viewModelScope.launch {
           //经过FaceServer找到最匹配的人脸
           val topFace = CalculateFaceUtil.getInstance().getTopFace(frFace) 
            // 。。。逻辑
            withContext(Dispatchers.IO){
                //上传到网络逻辑
            }
        }
    }

修正之后就能够直接在一个效果域中切换线程的调度。

还有一个比较典型的比如便是网络恳求用的许多的协程处理类,许多人喜欢把网络恳求的结果再封装一层,指定成功或失利。例如:

sealed class OkResult<out T : Any> {
    data class Success<out T : Any>(val data: T) : OkResult<T>()
    data class Error(val exception: Exception) : OkResult<Nothing>()
    //检测成功与失利
    fun checkResult(success: (T) -> Unit, error: (String?) -> Unit) {
        if (this is Success) {
            success(data)
        } else if (this is Error) {
            error(exception.message)
        }
    }
    //只是检测成功
    fun checkSuccess(success: (T) -> Unit) {
        if (this is Success) {
            success(data)
        }
    }
}

因为也是运用高阶函数回调的,那么就会遇到相同的问题。


viewModelScope.launch {
    val result = mSyncRepository.syncAttendance(attendance, token)
    result.checkSuccess {
        //数据库操作
        AttendanceDBHelper.updateAttendance(attendance)
    }
}

场景是网络恳求提交考勤数据,然后保存到数据库里,那么这样的办法数据库的操作就只能在主线程了,不能切换线程了,所以引荐运用去除回调的办法:

    //查看并返回是成功仍是失利(在协程中运用直接返回铺平回调)
    suspend fun isSuccess(): Boolean {
        return suspendCoroutine { continuation ->
            continuation.resume(this is Success)
        }
    }

修正之后

   val result = mSyncRepository.syncAttendance(attendance, token)
    if(result.isSuccess()){
        //数据库操作
        withContext(Dispatchers.IO){
            AttendanceDBHelper.updateAttendance(attendance)
        } 
    }

这样修正之后相对就会好一些。

总结

Java线程池与Kotlin协程线程池的总结:

Java线程池:中心线程+行列+其他线程

首要运用中心线程履行使命,一旦中心线程满了,就把使命加到行列中,内部依据不同的调度实现来判断是否敞开其他线程来履行行列的使命。

协程线程池:大局行列+本地行列

先尝试增加到本地行列(尾部调用机制),再增加到大局行列,协程线程池从行列中找使命(使命盗取机制)履行,PS:内部又一系列的CUP使命与非CUP使命的转换逻辑

Java线程池与Kotlin协程线程池的差异:

Java线程池比较开发,能够挑选系统不同的线程策略,也能够自界说线程池,不同的组合能够实现不同的效果,没有区别使命是否堵塞的特点。

协程的线程池是专供协程运用,没有那么开放,内部的使命区别是否堵塞的特点,会放到不同的行列中,CoroutineScheduler类中的两个大局行列 globalCpuQueue(存非堵塞的使命) , globalBlockingQueue(存堵塞的使命)。感觉调度会愈加的合理。

suspendCoroutine 与 Dispatchers.IO 的总结:

在协程中善用去除回调的办法,尽量把异步的逻辑同步化,不损坏协程的效果域,一同善用线程调度器,区别CUP使命与非CUP使命。最大化的优化线程池功率。

结束

哎,文章就到这里了,感觉知识点没串起来,篇幅太长了,东西杂了,写的欠好,感觉好杂乱,咱们将就着看看。

其实协程的概念仍是有点大的,零零散散的知识点许多,我本人如有解说不到位或了解错误的当地,也希望同学们能够指出,咱们一同沟通嘛。

假如感觉本文对你有一点点点的启发,还望你能点赞支撑一下,你的支撑是我最大的动力。

Ok,这一期就此完结。

Kotlin协程-与线程池的对比 & 铺平回调 & 协程分发器