暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Kotlin中为什么不推荐使用GlobalScope.launch?

阿柒的魔法屋 2022-08-08
2000

一、前言:

kotlin 中 GlobalScope 类提供了几个创建协程的构造函数:

1、 runBlocking:

  1. 创建的是主协程,区别是 runBlocking 里面的 delay 会阻塞线程,而 launch 创建的不会;

  2. 可以指定runBlocking的工作线程;

  3. 使用runBlocking一定会阻塞主线程;

Log.d("LUO","1111========${DateTimeHelper.format(Date(),"yyyy-MM-dd HH:mm:ss")}")
//调用协程方法
run1()
Log.d("LUO","2222========${DateTimeHelper.format(Date(),"yyyy-MM-dd HH:mm:ss")}")

结果:
D/LUO: 1111========2021-08-12 13:47:40
LUO: 主协程3===========DefaultDispatcher-worker-1
LUO: 2222========2021-08-12 13:47:42

//所有的协程类型
fun run1() {
    //默认主协程
    runBlocking {
        Log.d("LUO""主协程1===========${Thread.currentThread().name}")
    }
    //main主协程
    runBlocking(Dispatchers.Main) {
        Log.d("LUO""主协程2===========${Thread.currentThread().name}")
    }
    //IO主协程
    runBlocking(Dispatchers.IO) {
        Log.d("LUO""主协程3===========${Thread.currentThread().name}")
    }

    //runBlocking最后一个就是返回值
    val job = runBlocking {
        "我是小白啊"
    }
    Log.d("LUO","job========${job}")
}

2、launch:

  1. GlobalScope.launch创建主协程;

  2. runBlocking创建主协程(在runBlocking内创建launch{}子协程);

private fun run2() {
    //GlobalScope主协程
    GlobalScope.launch {
        Log.d("LUO""主协程1===========${Thread.currentThread().name}")
    }

    //GlobalScope主协程,main线程
    GlobalScope.launch(Dispatchers.Main) {
        Log.d("LUO""主协程2===========${Thread.currentThread().name}")
    }

    //GlobalScope主协程,IO线程
    GlobalScope.launch(Dispatchers.IO) {
        Log.d("LUO""主协程3===========${Thread.currentThread().name}")
    }

    //runBlocking主协程
    runBlocking {
        launch {

        }
    }
    //启动主协程
    GlobalScope.async {
        Log.d("LUO""主协程4===========${Thread.currentThread().name}")
    }
    //自定义协程
    val scope = CoroutineScope(EmptyCoroutineContext)
    scope.launch { 

    }
    scope.async { 

    }
}

3、 CoroutineScope :

  1. CoroutineScope 可以开启一个协程,并且不会阻塞主线程;

  2. 通过CoroutineScope.launch开启一个协程,协程体里的任务时就会先挂起(suspend),让CoroutineScope.launch后面的代码继续执行,直到协程体内的方法执行完成再自动切回来所在的上下文回调结果。

  3. CoroutineScope.launch 中我们可以看到接收了一个参数Dispatchers.Main,这是一个表示协程上下文的参数,用于指定该协程体里的代码运行在哪个线程。当指定为Dispatchers.Main时,协程体里的代码也是运行在主线程。当指定为Dispatchers.IO,则当前协程运行在一个子线程里。

//调用:
Log.d("LUO","1111========${DateTimeHelper.format(Date(),"yyyy-MM-dd HH:mm:ss")}")
run3()
Log.d("LUO","2222========${DateTimeHelper.format(Date(),"yyyy-MM-dd HH:mm:ss")}")
结果:
LUO: 1111========2021-08-12 14:13:00
LUO: 2222========2021-08-12 14:13:00

//被调用:
private fun run3() {
    CoroutineScope(Dispatchers.Main).launch{
        Log.d("LUO""协程1===========${Thread.currentThread().name}")
        delay(2000)
    }

    CoroutineScope(Dispatchers.IO).launch{
        Log.d("LUO""协程2===========${Thread.currentThread().name}")
    }

    CoroutineScope(Dispatchers.Default).launch{
        Log.d("LUO""协程3===========${Thread.currentThread().name}")
    }
}

源码:

/**
 * Creates a [CoroutineScope] that wraps the given coroutine [context].
 *
 * If the given [context] does not contain a [Job] element, then a default `Job()` is created.
 * This way, cancellation or failure of any child coroutine in this scope cancels all the other children,
 * just like inside [coroutineScope] block.
 */

@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

翻译:

/*
 * 创建一个[CoroutineScope],用于包装给定的coroutine[context]。
 * 如果给定的[context]不包含[Job]元素,则会创建默认的'Job()'。
 * 这样,此范围内任何子协同程序的取消或失败都会取消所有其他子程序,
 * 就像在[coroutineScope]块中一样。
 */


4、可返回结果的协程:withContext 与 async

withContext
 与 async
 都可以返回耗时任务的执行结果。一般来说,多个 withContext 任务是串行的, 且withContext 可直接返回耗时任务的结果。 多个 async 任务是并行的,async 返回的是一个Deferred<T>
,需要调用其DeferredInstance.await()
方法获取结果。

4.1、 withContext:

  1. 不创建新的协程,指定协程上运行代码块(这个函数主要可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行);

  2. withContext必须在协程或者suspend函数中调用

  3. 通过Dispatchers来指定代码块所运行的线程

  4. withContext会阻塞上下文线程

  5. withContext有返回值,会返回代码块的最后一行的值

private fun run6() {
    CoroutineScope(Dispatchers.Main).launch {
        val time1 = System.currentTimeMillis()

        val task1 = withContext(Dispatchers.IO) {
            delay(2000)
            Log.e("LUO""1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
            "one"  //返回结果赋值给task1
        }

        val task2 = withContext(Dispatchers.IO) {
            delay(1000)
            Log.e("LUO""2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
            "two"  //返回结果赋值给task2
        }
        Log.e("LUO""task1 = $task1  , task2 = $task2 , 耗时 ${System.currentTimeMillis()-time1} ms  [当前线程为:${Thread.currentThread().name}]")
    }
}

结果:

LUO: 1.执行task1.... [当前线程为:DefaultDispatcher-worker-1]
LUO: 2.执行task2.... [当前线程为:DefaultDispatcher-worker-3]
LUO: task1 = one  , task2 = two , 耗时 3032 ms  [当前线程为:main]

从上面结果可以看出,多个withConext
串行执行
,如上代码执行顺序为先执行task1再执行task2,共耗时两个任务的所需时间的总和。这是因为withConext是个 suspend
 函数,当运行到 withConext
 时所在的协程就会挂起,直到withConext
执行完成后再执行下面的方法。所以 withConext可以用在一个请求结果依赖另一个请求结果的这种情况
 。

4.2、 async :

  1. 创建带返回值的协程,返回的是 Deferred 类;

  2. 一定要用async … await() 来取返回数据;

如果同时处理多个耗时任务,且这几个任务都无相互依赖时,可以使用 async ... await()
 来处理,将上面的例子改为 async 来实现如下:

private fun run7() {
    CoroutineScope(Dispatchers.Main).launch {
        val time1 = System.currentTimeMillis()

        val task1 = async(Dispatchers.IO) {
            delay(2000)
            Log.e("LUO""1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
            "one"  //返回结果赋值给task1
        }

        val task2 = async(Dispatchers.IO) {
            delay(1000)
            Log.e("LUO""2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
            "two"  //返回结果赋值给task2
        }

        Log.e("LUO""task1 = ${task1.await()}  , task2 = ${task2.await()} , 耗时 ${System.currentTimeMillis() - time1} ms  [当前线程为:${Thread.currentThread().name}]")
    }
}

结果:

LUO: 2.执行task2.... [当前线程为:DefaultDispatcher-worker-3]
LUO: 1.执行task1.... [当前线程为:DefaultDispatcher-worker-3]
LUO: task1 = one  , task2 = two , 耗时 2025 ms  [当前线程为:main]

改为用async
后,运行结果耗时明显比使用withContext
更短,且看到与withContext
不同的是,task2比task1优先执行完成。所以说 async
 的任务都是并行执行
的。但事实上有一种情况例外,当我们把await()方法的调用提前到 async 的后面时,他就是不是并行的

private fun run8() {
    CoroutineScope(Dispatchers.Main).launch {
        val time1 = System.currentTimeMillis()

        val task1 = async(Dispatchers.IO) {
            delay(2000)
            Log.e("LUO""1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
            "one"  //返回结果赋值给task1
        }.await()

        val task2 = async(Dispatchers.IO) {
            delay(1000)
            Log.e("LUO""2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
            "two"  //返回结果赋值给task2
        }.await()

        Log.e("LUO""task1 = $task1  , task2 = $task2 , 耗时 ${System.currentTimeMillis() - time1} ms  [当前线程为:${Thread.currentThread().name}]")
    }
}

此时的结果居然和使用withContext
几乎差不多,不是说好的并行,怎么又好像是串行执行了?

刚只是把await()的位置改了,就出现这样的结果,所以原因应该就是在await()方法身上,点进 await() 源码看一下,终于明白了是怎么一回事,原来await() 仅仅被定义为 suspend 函数,因此直接在async 后面使用 await() 就和 withContext 一样,程序运行到这里就会被挂起直到该函数执行完成才会继续执行下一个 async 。但事实上await()也不一定导致协程会被挂起,await() 只有在 async 未执行完成返回结果时,才会挂起协程。若 async 已经有结果了,await() 则直接获取其结果并赋值给变量,此时不会挂起协程

5、Dispatchers切换到线程

类型功能
不指定它从启动了它的 CoroutineScope 中承袭了上下文
Dispatchers.Main用于Android. 在UI线程中执行
Dispatchers.IO子线程, 适合执行磁盘或网络 I/O操作
Dispatchers.Default子线程,适合 执行 cpu 密集型的工作
Dispatchers.Unconfined从当前线程直接执行, 直到第一个挂起点

6、Job

launch
 会返回一个 Job
 对象

public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.()
 -> Unit
): Job {

它的方法有:

函数用法
join()挂起当前协程, 等待 job 协程执行结束
cancel()取消协程
cancelAndJoin()取消协程并等待结束. 协程被取消, 但不一定立即结束, 或许还有收尾工作

解释:

cancel() 函数用于取消协程,join() 函数用于阻塞等待协程执行结束。之所以连续调用这两个方法,是因为 cancel() 函数调用后会马上返回而不是等待协程结束后再返回,所以此时协程不一定是马上就停止了,为了确保协程执行结束后再执行后续代码,此时就需要调用 join() 方法来阻塞等待。可以通过调用 Job 的扩展函数 cancelAndJoin() 来完成相同操作

CoroutineScope内包含的字段

参数意义
isActive是否正在运行
isCompleted是否运行完成
isCancelled是否已取消

7、协程超时

在实践中绝大多数取消一个协程的理由是它有可能超时。

withTimeout(1300L){...}

withTimeout 是一个挂起函数, 需要在协程中执行. 超时会抛出 TimeoutCancellationException 异常, 它是 CancellationException 的子类。CancellationException 被认为是协程执行结束的正常原因。因此没有打印堆栈跟踪信息.

val result = withTimeoutOrNull(1300L)
withTimeoutOrNull 当超时时会返回 null, 来进行超时操作,从而替代抛出一个异常;

8、创建协程的几种方式:

方式作用
launch:Job创建一个不会阻塞
当前线程、没有返回结果的Coroutine,但会返回一个Job
对象,Job可以控制这个Coroutine的执行和取消
runBlocking:T创建一个会阻塞
当前线程的Coroutine,常用于单元测试的场景,开发一般用不到
async/awit:Deferredasync 返回了一个Deferred接
口,Deferred接口继承与Job

二、问题:

GlobalScope.launch
 的协程作用域不受限制, 即 除非主进程退出, 否则只要该协程不结束就会占用资源 ;

这导致了如果协程的执行体中出现异常协程仍会占用资源而非释放. 最差的情况下有可能反复调用导致设备资源被占满宕机.

  • GlobalScope 生命周期受整个进程限制, 进程退出才会自动结束. 它不会使进程保活, 像一个守护线程

  • 一个线程可以有多个等待执行的协程, 它们不像多线程争抢cpu那样, 它们是排队执行.

综上, 使用GlobalScope.launch有可能导致无法预料的内存泄漏
.

因此, 在任何情况下, 我们都应限制线程的作用域"CoroutineScope";

  • 在使用suspend修饰的方法中, 可以使用"coroutineScope"

  • 在没有suspend修饰的方法中, 可以使用"runBlocking"

 private fun run5() {
    //1、不阻塞主线程(推荐)
    CoroutineScope(Dispatchers.IO).launch {
        //执行代码.....
    }

    //2、塞主线程(推荐)
    GlobalScope.launch(Dispatchers.IO) { 
        //执行代码.....
    }

    //3、优秀的线程切换
    CoroutineScope(Dispatchers.Main).launch {
        val task1 = withContext(Dispatchers.IO) {
            Log.d("LUO","1111========${DateTimeHelper.format(Date(),"yyyy-MM-dd HH:mm:ss")}")
            delay(2000)
            "服务器返回值:json"  //服务器返回结果赋值给task1
        }
        //刷新UI,task1
        Log.d("LUO","2222========${DateTimeHelper.format(Date(),"yyyy-MM-dd HH:mm:ss")}")
        Log.d("LUO""值===========${task1}")
    }
    Log.d("LUO","3333========${DateTimeHelper.format(Date(),"yyyy-MM-dd HH:mm:ss")}")
}

参考:https://blog.csdn.net/zhong_zihao/article/details/105145206

作者:因为我的心
链接:https://www.jianshu.com/p/1b289ff09709
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


文章转载自阿柒的魔法屋,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论