- 协程间的数据传递:单值传递和多值传递(数据流的传递)
- 单值传递:
- launch创建的协程:
- 多协程是顺序执行,且各个协程的函数体内没有挂起当前协程的逻辑(包括延时逻辑),各个协程的执行顺序是顺序执行,则下一个协程可以使用上一个协程的返回值。
- 根协程声明值,子协程修改值,或者延迟产生数据实现在协程之间传递值,不过需要注意的是分析根协程和各个协程的逻辑的执行顺序,对数据的修改顺序会影响到数据的状态进而也就影响了协程对数据的使用。
- async创建的协程:
- 协程并发,此时不建议协程间数据传递使用,个人理解的此时的使用场景是协程创建自己的数据而所有协程执行完成汇总协程产生的数据进行使用,使用其并发缩短了逻辑的执行时间。
- 数据流的传递:
- 通过通道(channel)进行协程间数据流的传递。
- 阻塞队列(BlockingQueue):项目中常见的模型(生产者消费者模型)常用的数据结构,提供put和take且都是阻塞的,即take的时候若没有数据则会阻塞线程直到有了数据,同样put也是没有空间则阻塞线程等到数据take队列有了空间。
-
协程中的通道等同于阻塞队列,用于协程间数据流的传递,提供send(数据的发送)和receive(数据的接受)和队列不同的是不会阻塞。
-
和队列不同,通道提供了close关闭逻辑,即通道关闭后则不会再有数据发送,close操作类似于向通道发送一个特殊的关闭标记. 收到这个关闭标记之后, 对通道的迭代操作将会立即停止, 因此可以保证在关闭操作以前发送的所有数据都会被正确接收。
- 构造通道的生产者(producer):在协程中产生一个数值序列, 这是很常见的模式. 这是并发代码中经常出现的 生产者(producer)/消费者(consumer) 模式的一部分. 你可以将生产者抽象为一个函数, 并将通道作为函数的参数, 然后向通道发送你生产出来的值, 但这就违反了通常的函数设计原则, 也就是函数的结果应该以返回值的形式对外提供.
有一个便利的协程构建器, 名为 produce, 它可以很简单地编写出生产者端的正确代码, 还有一个扩展函数 consumeEach, 可以在消费者端代码中替代for
循环:即:
- 带缓冲区的通道:
- 通道分为发送和接受两个场景,通常的通道如果没有接受仅有发送的场景则是将发送协程挂起,等待接受协程的调用时才会执行发送的逻辑进行数据的发送,同样 先执行接受协程也会将接受协程挂起等待发送协程的调用然后重新执行接受协程。
- 但是对于带缓冲区的通道则是在数据的发送提供一个缓冲区,即通道的参数指定发送数据的缓存个数,此时通道没有接受的场景,在仅有的发送场景的时候也会发送缓冲区的数据,达到缓冲区的指定数后将发送数据协程挂起。
- Channel() 工厂函数和 produce 构建器都可以接受一个可选的
capacity
参数, 用来指定 缓冲区大小. 缓冲区可以允许发送者在挂起之前发送多个数据, 类似于指定了容量的BlockingQueue
, 它会在缓冲区已满的时候发生阻塞.
- 带缓冲区的通道:
-
定时器通道:类似于timer定时任务,即根据指定的定时时间发送一个unit的定时值,用户可以根据定时值处理一些常见的定时逻辑,可以使用 ticker 工厂函数来创建这种通道. 使用通道的 ReceiveChannel.cancel 方法来指出不再需要它继续产生数据了。即:
-
- 通过通道(channel)进行协程间数据流的传递。
- 多协程访问通道:
- 多协程接受通道的数据:
- 通道里的数据可以被多个协程收到,通道的数据的发送不会发生变化,接受数据的协程不固定
- 取消通道的生成者协程,则通道会被关闭,进而接受通道数据的协程也都会被取消。
-
在接受通道数据的协程的函数体中for循环和consumeEach的不同点是:前面for循环若出现异常并不会取消接受数据的协程因为岂不是挂起函数,也就不会取消其他的接受通道数据的协程,但是后者是一个挂起函数若出现异常则会取消协程,进而影响到通道的关闭,其他接受者协程和生产者协程也就取消。
- 多协程向通道发送数据:
- 发射数据也可以多个协程向其写入数据,数据的顺序取决于协程的写入逻辑。
-
取消所有的协程则通道的数据也就随之取消。
-
如果从多个协程中调用通道的发送和接收操作, 从调用发生的顺序来看, 这些操作是 平等的. 通道对这些方法以先进先出(first-in first-out)的顺序进行服务, 也就是说, 第一个调用 receive 的协程会得到通道中的数据. 在下面的示例程序中, 有两个 "ping" 和 "pong" 协程, 从公用的一个 "table" 通道接收 "ball" 对象.
- 多协程接受通道的数据:
- 管道:上面讲了多协程生产数据和多协程接收数据,在这个场景中可以讲这些协程串联起来形成一个数据处理的管道。
- ReceiveChannel:通道的数据接受的封装对象,producer产生数据后返回的对象即时这个对象,封装了产生的数据(具体可以参考上面的代码)。
-
管道的协程串联即是通过上面的这个对象进行串联,即中间协程可以接受生产者产生的数据修改后再封装返回,最终到最后一个协程通过其接受者获取到数据。即:
- launch创建的协程:
- 单值传递:
- 多线程协程的数据的同步:
问题:-
通过Default线程指定,多协程的执行会在多线程中执行,此时就会出现多线程共享值的问题,即多个线程同时访问并修改同一个值就会出现意想不到的问题。
备注:多线程中多协程的原因导致并没有出现100*1000的结果值。
-
- 多协程的多线程执行的解决和多线程共享值基本一样,即:
-
针对简单的基础变量,使用volatile(即使用其原子性:针对变量的每一个线程的读和写保证其可见性)不能保证并发问题解决,对于上面的案例使用这个关键字并不能保证每次都是100*1000,即:
- 下面几种方法可以保证多线程对共享值的修改的线程安全。
-
和多线程处理一致数据结构使用线程安全的数据结构,即使用线程安全的 (也叫 同步的(synchronized), 线性的(linearizable), 或者 原子化的(atomic)) 数据结构, 这些数据结构会对需要在共享的状态数据上进行的操作提供必要的同步保障. 在我们的简单的计数器示例中, 可以使用 AtomicInteger 类, 它有一个原子化的 incrementAndGet 操作且对于这个具体的问题, 这是最快的解决方案. 这种方案适用于计数器, 集合, 队列, 以及其他标准数据结构, 以及这些数据结构的基本操作. 但是, 这种方案并不能简单地应用于复杂的状态变量, 或者那些没有现成的线程安全实现的复杂操作.
- 细粒度的线程限定:将涉及到的共享值转变的lambda表达式或者匿名函数放到独一线程中去,即对于共享值的修改放到唯一线程中去修改。
-
缺点是:代码的执行变慢了,因为在执行中要不停的进行线程的切换,即协程线程和修改数据的线程来回切换。
-
粗粒度的线程限定:即将整个操作放到一个独立线程中去这样比细粒度线程限定速度快又能保证共享值的线程安全。
- 加锁同步即和多线程中的锁同步一致,不过协程的语法和线程不一致,协程是通过Mutex实现的它的 lock和 unlock 函数可以用来界定临界区. 主要的区别在于
Mutex.lock()
是一个挂起函数. 它不会阻塞线程.
* 还有一个扩展函数 withLock, 它用非常便利的方式实现mutex.lock(); try { ... } finally { mutex.unlock() }
模式:
- 加锁同步即和多线程中的锁同步一致,不过协程的语法和线程不一致,协程是通过Mutex实现的它的 lock和 unlock 函数可以用来界定临界区. 主要的区别在于
-
-
-
- select选择:上面通道中介绍了多个挂起函数发送值的场景,使用select选择语法可以在多个挂起函数中选择第一个执行完毕的结果,其他挂起函数取消或者关闭。
-
通道中选择使用:
-
通道的关闭会导致通道的select选择的onReceive函数语句失败且抛出对应的异常,对于此可以使用函数onReceiveCatching对其进行抓取并处理其异常。即:
备注:通道上选择的优先级:
- 多个通道且每个通道的数据发送有先后顺序:此时取决于数据的发送顺序,例如上面的第一个案例,第一个通道500ms发送一个数据所以第一个数据是第一个通道,且第一个通道的数据居多,偶尔第二个通道也会有数据。
- 多个通道且每个通道没有具体的数据发送顺序区分:比如第二个案例:优先使用第一个通道,后续会根据通道的数据发送顺序获取数据。
- 通道的数据发送时选择发送的通道:上面介绍了在接受数据的时候多通道的选择,此处介绍的是发送数据发送到多个通道:选择表达式也可以使用 onSend 子句, 它可以与选择表达式的偏向性结合起来, 起到很好的作用。
- onSend函数可以指定要把数据发送到那一个通道,指定语法使用通道调用即可,若不指定则是发送到当前的生产者的主通道中去。
![测试及其结果](https://upload-images.jianshu.io/upload_images/1346105-347add8547d98405.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
- onSend函数可以指定要把数据发送到那一个通道,指定语法使用通道调用即可,若不指定则是发送到当前的生产者的主通道中去。
-