kotlin协程2:协程间数据同步(多线程间协程的数据同步)

  1. 协程间的数据传递:单值传递和多值传递(数据流的传递)
    • 单值传递:
      • launch创建的协程:
        • 多协程是顺序执行,且各个协程的函数体内没有挂起当前协程的逻辑(包括延时逻辑),各个协程的执行顺序是顺序执行,则下一个协程可以使用上一个协程的返回值。
        • 根协程声明值,子协程修改值,或者延迟产生数据实现在协程之间传递值,不过需要注意的是分析根协程和各个协程的逻辑的执行顺序,对数据的修改顺序会影响到数据的状态进而也就影响了协程对数据的使用。
      • async创建的协程:
        • 协程并发,此时不建议协程间数据传递使用,个人理解的此时的使用场景是协程创建自己的数据而所有协程执行完成汇总协程产生的数据进行使用,使用其并发缩短了逻辑的执行时间。
      • 数据流的传递:
        • 通过通道(channel)进行协程间数据流的传递。
          • 阻塞队列(BlockingQueue):项目中常见的模型(生产者消费者模型)常用的数据结构,提供put和take且都是阻塞的,即take的时候若没有数据则会阻塞线程直到有了数据,同样put也是没有空间则阻塞线程等到数据take队列有了空间。
          • 协程中的通道等同于阻塞队列,用于协程间数据流的传递,提供send(数据的发送)和receive(数据的接受)和队列不同的是不会阻塞。


            通道数据传递
            • 和队列不同,通道提供了close关闭逻辑,即通道关闭后则不会再有数据发送,close操作类似于向通道发送一个特殊的关闭标记. 收到这个关闭标记之后, 对通道的迭代操作将会立即停止, 因此可以保证在关闭操作以前发送的所有数据都会被正确接收。


              通道关闭
            • 构造通道的生产者(producer):在协程中产生一个数值序列, 这是很常见的模式. 这是并发代码中经常出现的 生产者(producer)/消费者(consumer) 模式的一部分. 你可以将生产者抽象为一个函数, 并将通道作为函数的参数, 然后向通道发送你生产出来的值, 但这就违反了通常的函数设计原则, 也就是函数的结果应该以返回值的形式对外提供.
              通道Produce
              备注:如图所示,produce是CoroutineScope提供的一个扩展函数,参数为上下文,通道缓冲区大小和产生序列数的lambda表达式,封装了通道的创建及其上下文环境,可以让用户专注于生产数序列即可。
              有一个便利的协程构建器, 名为 produce, 它可以很简单地编写出生产者端的正确代码, 还有一个扩展函数 consumeEach, 可以在消费者端代码中替代 for 循环:即:
              通道produce封装及其函数consumeEach
              • 带缓冲区的通道:
                • 通道分为发送和接受两个场景,通常的通道如果没有接受仅有发送的场景则是将发送协程挂起,等待接受协程的调用时才会执行发送的逻辑进行数据的发送,同样 先执行接受协程也会将接受协程挂起等待发送协程的调用然后重新执行接受协程。
              • 但是对于带缓冲区的通道则是在数据的发送提供一个缓冲区,即通道的参数指定发送数据的缓存个数,此时通道没有接受的场景,在仅有的发送场景的时候也会发送缓冲区的数据,达到缓冲区的指定数后将发送数据协程挂起。
              • Channel() 工厂函数和 produce 构建器都可以接受一个可选的 capacity 参数, 用来指定 缓冲区大小. 缓冲区可以允许发送者在挂起之前发送多个数据, 类似于指定了容量的 BlockingQueue, 它会在缓冲区已满的时候发生阻塞.
                缓冲区通道
            • 定时器通道:类似于timer定时任务,即根据指定的定时时间发送一个unit的定时值,用户可以根据定时值处理一些常见的定时逻辑,可以使用 ticker 工厂函数来创建这种通道. 使用通道的 ReceiveChannel.cancel 方法来指出不再需要它继续产生数据了。即:


              定时器通道
      • 多协程访问通道:
        • 多协程接受通道的数据:
          • 通道里的数据可以被多个协程收到,通道的数据的发送不会发生变化,接受数据的协程不固定
          • 取消通道的生成者协程,则通道会被关闭,进而接受通道数据的协程也都会被取消。
          • 在接受通道数据的协程的函数体中for循环和consumeEach的不同点是:前面for循环若出现异常并不会取消接受数据的协程因为岂不是挂起函数,也就不会取消其他的接受通道数据的协程,但是后者是一个挂起函数若出现异常则会取消协程,进而影响到通道的关闭,其他接受者协程和生产者协程也就取消。


            生产者协程

            处理数据协程

            启动多个数据的协程
        • 多协程向通道发送数据:
          • 发射数据也可以多个协程向其写入数据,数据的顺序取决于协程的写入逻辑。
          • 取消所有的协程则通道的数据也就随之取消。


            通道数据生产者

            多协程写入数据
        • 如果从多个协程中调用通道的发送和接收操作, 从调用发生的顺序来看, 这些操作是 平等的. 通道对这些方法以先进先出(first-in first-out)的顺序进行服务, 也就是说, 第一个调用 receive 的协程会得到通道中的数据. 在下面的示例程序中, 有两个 "ping" 和 "pong" 协程, 从公用的一个 "table" 通道接收 "ball" 对象.


          多协程访问通道的顺序
      • 管道:上面讲了多协程生产数据和多协程接收数据,在这个场景中可以讲这些协程串联起来形成一个数据处理的管道。
      • ReceiveChannel:通道的数据接受的封装对象,producer产生数据后返回的对象即时这个对象,封装了产生的数据(具体可以参考上面的代码)。
      • 管道的协程串联即是通过上面的这个对象进行串联,即中间协程可以接受生产者产生的数据修改后再封装返回,最终到最后一个协程通过其接受者获取到数据。即:


        管道生产者

        管道数据中间处理

        管道的串联
  2. 多线程协程的数据的同步:
    问题:
    • 通过Default线程指定,多协程的执行会在多线程中执行,此时就会出现多线程共享值的问题,即多个线程同时访问并修改同一个值就会出现意想不到的问题。


      共享值

      多线程

      备注:多线程中多协程的原因导致并没有出现100*1000的结果值。

  • 多协程的多线程执行的解决和多线程共享值基本一样,即:
    • 针对简单的基础变量,使用volatile(即使用其原子性:针对变量的每一个线程的读和写保证其可见性)不能保证并发问题解决,对于上面的案例使用这个关键字并不能保证每次都是100*1000,即:


      并发案例
    • 下面几种方法可以保证多线程对共享值的修改的线程安全。
      • 和多线程处理一致数据结构使用线程安全的数据结构,即使用线程安全的 (也叫 同步的(synchronized), 线性的(linearizable), 或者 原子化的(atomic)) 数据结构, 这些数据结构会对需要在共享的状态数据上进行的操作提供必要的同步保障. 在我们的简单的计数器示例中, 可以使用 AtomicInteger 类, 它有一个原子化的 incrementAndGet 操作且对于这个具体的问题, 这是最快的解决方案. 这种方案适用于计数器, 集合, 队列, 以及其他标准数据结构, 以及这些数据结构的基本操作. 但是, 这种方案并不能简单地应用于复杂的状态变量, 或者那些没有现成的线程安全实现的复杂操作.


        线程安全的数据结构
      • 细粒度的线程限定:将涉及到的共享值转变的lambda表达式或者匿名函数放到独一线程中去,即对于共享值的修改放到唯一线程中去修改。
        • 缺点是:代码的执行变慢了,因为在执行中要不停的进行线程的切换,即协程线程和修改数据的线程来回切换。


          单线程
        • 粗粒度的线程限定:即将整个操作放到一个独立线程中去这样比细粒度线程限定速度快又能保证共享值的线程安全。


          单线程线程安全
          • 加锁同步即和多线程中的锁同步一致,不过协程的语法和线程不一致,协程是通过Mutex实现的它的 lock和 unlock 函数可以用来界定临界区. 主要的区别在于 Mutex.lock() 是一个挂起函数. 它不会阻塞线程.
            * 还有一个扩展函数 withLock, 它用非常便利的方式实现 mutex.lock(); try { ... } finally { mutex.unlock() } 模式:
            加锁实现同步
  1. select选择:上面通道中介绍了多个挂起函数发送值的场景,使用select选择语法可以在多个挂起函数中选择第一个执行完毕的结果,其他挂起函数取消或者关闭。
    • 通道中选择使用:


      挂起函数1

      挂起函数2

      通道选择挂起函数

      测试代码及其结果
    • 通道的关闭会导致通道的select选择的onReceive函数语句失败且抛出对应的异常,对于此可以使用函数onReceiveCatching对其进行抓取并处理其异常。即:


      选择处理异常挂起函数

      生产者挂起函数及其测试挂起函数和结果

      备注:通道上选择的优先级:

      • 多个通道且每个通道的数据发送有先后顺序:此时取决于数据的发送顺序,例如上面的第一个案例,第一个通道500ms发送一个数据所以第一个数据是第一个通道,且第一个通道的数据居多,偶尔第二个通道也会有数据。
      • 多个通道且每个通道没有具体的数据发送顺序区分:比如第二个案例:优先使用第一个通道,后续会根据通道的数据发送顺序获取数据。
    • 通道的数据发送时选择发送的通道:上面介绍了在接受数据的时候多通道的选择,此处介绍的是发送数据发送到多个通道:选择表达式也可以使用 onSend 子句, 它可以与选择表达式的偏向性结合起来, 起到很好的作用。

参考文章:
协程间的通信
协程与线程间的关系
协程的并发问题

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

推荐阅读更多精彩内容