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

一、前言:

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>,需要调用其await()方法获取结果。

5、 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可以用在一个请求结果依赖另一个请求结果的这种情况。

6、 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() 则直接获取其结果并赋值给变量,此时不会挂起协程。

7、Dispatchers切换到线程

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

8、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 是否已取消

9、协程超时

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

withTimeout(1300L){...}

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

val result = withTimeoutOrNull(1300L)

withTimeoutOrNull 当超时时会返回 null, 来进行超时操作,从而替代抛出一个异常;

二、问题:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容