首页 > 其他分享 >Kotlin协程:打破线程框架的思维

Kotlin协程:打破线程框架的思维

时间:2023-07-06 17:31:55浏览次数:39  
标签:Task 协程 Kotlin +-----------------+ 线程 +---------+

Kotlin协程:打破线程框架的思维

前言

协程是Kotlin对比Java的最大优势,需要理解协程的设计理念和知识体系,建立协程思维模型。本文将介绍协程的概念、特性和原理,以及如何在Android开发中使用协程来简化并发编程和优化软件架构。

什么是协程

协程是一种互相协作的程序,可以在任意地方挂起和恢复,每次返回一个值,而不是像普通程序那样只能在末尾返回一次。协程可以看成是一种用户态的轻量级线程,它不依赖于操作系统的调度,而是由程序员控制它们的执行流程。

举个例子,假设我们有一个普通的函数getFilteredBitmap(),它从一个API获取一张图片,并且给它加上一个雪花滤镜。我们想要在主线程中调用这个函数,并且显示处理后的图片。我们可以这样写:

fun showSnowyBitmap() {
  val snowyBitmap = getFilteredBitmap() // 阻塞主线程
  showBitmap(snowyBitmap) // 显示图片
}


这样的代码有一个很大的问题:getFilteredBitmap()是一个耗时的操作,它会阻塞主线程,导致应用无响应。为了解决这个问题,我们通常会使用异步回调或者RxJava等方式来在后台线程中执行这个操作,并且在主线程中接收结果。但是这样的代码会变得复杂和难以维护。

如果我们把getFilteredBitmap()改成一个协程,我们就可以用一种更简洁和直观的方式来写异步代码。我们可以这样写:

suspend fun showSnowyBitmap() = coroutineScope {
  val snowyBitmap = async { getFilteredBitmap() } // 在后台线程中启动一个协程
  showBitmap(snowyBitmap.await()) // 在主线程中等待结果并显示图片
}


这样的代码看起来就像是同步的代码,但实际上它是非阻塞的。async是一个协程构建器,它会在后台线程中启动一个新的协程,并且返回一个Deferred对象,表示异步计算的结果。await是一个挂起函数,它会挂起当前协程,直到Deferred对象完成,并且返回结果。挂起当前协程并不会阻塞主线程,而是让出执行权给其他协程或者任务。当Deferred对象完成时,当前协程会自动恢复执行,并且继续后面的代码。

Kotlin协程

Kotlin协程需要单独依赖协程库,协程框架是一个整体的框架。协程比线程更轻量级、更灵活、更高效,可以在不同线程间切换。

协程库

要使用Kotlin协程,我们需要添加以下依赖到我们的app的build.gradle文件:

dependencies {
  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}


这个库包含了以下几个部分:

  • kotlinx-coroutines-core: 包含了核心的API和数据结构,如suspendlaunchasyncawaitCoroutineScopeJobDeferred等。
  • kotlinx-coroutines-android: 包含了Android平台特有的API和数据结构,如Main调度器、LifecycleScope等。
  • kotlinx-coroutines-test: 包含了测试协程的工具类,如TestCoroutineDispatcherrunBlockingTest等。

协程框架

Kotlin协程框架是一个整体的框架,它包含了以下几个层次:

  • 语言层:Kotlin语言提供了suspend关键字,用于标记挂起函数。挂起函数是一种特殊的函数,它可以在不阻塞线程的情况下暂停和恢复执行。挂起函数只能在协程或者其他挂起函数中调用。
  • 库层:协程库提供了各种API和数据结构,用于创建和管理协程。主要有以下几个概念:
    • 协程构建器:用于启动新的协程,如launchasyncrunBlocking等。不同的协程构建器有不同的特点和用途,我们会在后面详细介绍。
    • 协程作用域:用于定义协程的生命周期范围,如coroutineScopesupervisorScopeGlobalScope等。协程作用域可以嵌套使用,形成一个结构化的并发模式。我们会在后面详细介绍。
    • 协程上下文:用于存储协程的相关信息,如调度器、Job、异常处理器等。协程上下文可以通过协程构建器或者作用域来指定或者修改。我们会在后面详细介绍。
    • 调度器:用于指定协程运行的线程或者线程池,如Dispatchers.MainDispatchers.IODispatchers.Default等。调度器是协程上下文的一部分,我们会在后面详细介绍。
    • Job:用于表示一个协程的任务状态,如是否激活、是否完成、是否取消等。Job也是协程上下文的一部分,我们会在后面详细介绍。
    • Deferred:用于表示一个异步计算的结果,它是一种特殊的Job,可以通过await()来获取结果。Deferred通常由async()来创建,我们会在后面详细介绍。
  • 平台层:不同的平台提供了不同的实现方式,如JVM、Android、JS、Native等。平台层主要负责实现挂起函数的机制和调度器的逻辑。

协程和线程

协程和线程都是一种并发编程的方式,但是它们有很多不同点:

  • 线程是操作系统级别的概念,它依赖于操作系统的调度和管理。线程之间的切换需要保存和恢复寄存器和栈信息,消耗较大。线程之间的通信需要使用共享内存或者消息队列等方式,容易出现竞态条件或者死锁等问题。
  • 协程是用户态级别的概念,它依赖于程序员控制它们的执行流程。协程之间的切换只需要保存和恢复少量信息,消耗较小。协程之间可以使用通道或者共享流等方式进行通信,更加安全和高效。

协程的思维模型

协程可以看成是运行在线程中的Task,每个Task都有一个"抓手"或者"挂钩",可以方便我们对它进行挂起和恢复。协程不会和特定的线程绑定,它可以在不同的线程之间灵活切换。

我们可以用一个图来表示协程的思维模型:

+-----------------+     +-----------------+     +-----------------+
|    Thread 1     |     |    Thread 2     |     |    Thread 3     |
+-----------------+     +-----------------+     +-----------------+
|                 |     |                 |     |                 |
|  +---------+    |     |  +---------+    |     |  +---------+    |
|  | Task A  |--->|---->|  | Task A  |--->|---->|  | Task A  |    |
|  +---------+    |     |  +---------+    |     |  +---------+    |
|                 |     |                 |     |                 |
|  +---------+    |     |  +---------+    |     |  +---------+    |
|  | Task B  |--->|---->|  | Task B  |--->|---->|  | Task B  |--->|
|  +---------+    |     |  +---------+    |     |  +---------+    |
|                 |     |                 |     |                 |
|  +---------+    |     |  +---------+    |     |  +---------+    |
|  | Task C  |--->|---->|  | Task C  |--->|---->|  | Task C  |--->|
|  +---------+    |     |  +---------+    |     |  +---------+    |
|                 |     |                 |     |                 |
+-----------------+     +-----------------+     +-----------------+


在这个图中,我们有三个线程和三个协程(Task A、B、C)。每个协程都可以在任意的线程中运行,只要有一个"抓手"或者"挂钩"来控制它们的挂起和恢复。这个"抓手"或者"挂钩"就是协程上下文,它包含了调度器、Job、异常处理器等信息。我们可以通过协程构建器或者作用域来指定或者修改协程上下文。

协程的非阻塞特性

由于协程具有挂起和恢复的能力,它就可以实现非阻塞的特性。这意味着当一个协程遇到耗时的操作时,它不会阻塞后面的任务执行,而是让出执行权给其他任务,等到合适的时机再恢复执行。

举个例子,假设我们有两个协程,分别是Task A和Task B。Task A需要从网络获取一些数据,Task B需要从数据库获取一些数据。我们可以这样写:

suspend fun getData() = coroutineScope {
  val dataFromNetwork = async { fetchFromNetwork() } // 在后台线程中启动一个协程,从网络获取数据
  val dataFromDatabase = async { fetchFromDatabase() } // 在后台线程中启动一个协程,从数据库获取数据
  val result = combine(dataFromNetwork.await(), dataFromDatabase.await()) // 在主线程中等待结果并组合
  result // 返回结果
}


在这个例子中,我们使用了async协程构建器来启动两个协程,分别是dataFromNetworkdataFromDatabase。它们都返回一个Deferred对象,表示异步计算的结果。我们使用了await()挂起函数来等待它们的结果,并且组合成一个最终的结果。

注意,当我们调用await()时,并不会阻塞当前线程,而是挂起当前协程。这样,其他协程或者任务就可以在当前线程中执行。当Deferred对象完成时,当前协程会自动恢复执行,并且继续后面的代码。

这样的代码有以下几个优点:

  • 它是非阻塞的,不会影响主线程的响应性。
  • 它是并发的,可以同时执行多个耗时的操作。
  • 它是顺序的,可以按照我们期望的顺序写代码,而不需要使用回调或者嵌套等方式。
  • 它是结构化的,可以使用协程作用域来管理协程的生命周期和异常处理。

总结

本文介绍了Kotlin协程的概念、特性和原理,以及如何在Android开发中使用协程来简化并发编程和优化软件架构。我们学习了以下几个知识点:

  • 协程是一种互相协作的程序,可以在任意地方挂起和恢复,每次返回一个值。

  • Kotlin协程需要单独依赖协程库,协程框架是一个整体的框架。

  • 协程比线程更轻量级、更灵活、更高效,可以在不同线程间切换。

  • 协程可以看成是运行在线程中的Task,每个Task都有一个"抓手"或者"挂钩",可以方便我们对它进行挂起和恢复。

  • 协程具有非阻塞的特性,可以让出执行权给其他任务,等到合适的时机再恢复执行。

标签:Task,协程,Kotlin,+-----------------+,线程,+---------+
From: https://blog.51cto.com/u_16175634/6643994

相关文章

  • python 并发编程之线程
    一、队列的使用1、在python中,内置的有一个类,Queue就是队列2、队列的使用frommultiprocessingimportQueueif__name__=='__main__':q=Queue(3)#队列的大小默认很大#1.如何入队、"""obj,block=True,timeout=None"""q.put('hellow......
  • 多线程
    了解多线程并发和并行进程和线程总结多线程的实现方式--继承Thread实现步骤packagecom.thread;publicclassMyThreadextendsThread{@Overridepublicvoidrun(){//run()里面的代码就是线程开启之后执行的代码for(inti=0;......
  • Guzzle 协程原理
    Guzzle是一个流行的PHPHTTP客户端库,它提供了方便的方式来发送HTTP请求并处理响应。Guzzle也支持协程,允许开发者使用协程来编写异步的、非阻塞的代码。在Guzzle中,协程的实现基于Coroutine(协程)这个PHP扩展库。Coroutine提供了一种轻量级的线程替代方案,可以在一个线程......
  • python基础 如何查看进程的id号、队列的使用(queue)、解决进程之间隔离关系、生产者消
    如何查看进程id号进程都有几个属性:进程名、进程id号(pid-->processid)每一个进程都有一个唯一的id号,通过这个id号就能找到这个进程importosimporttimedeftask():print("task中的子进程号:",os.getpid())print("主进程中的进程号:",os.getppid())#parent......
  • 解决Java 线程池 共享变量的具体操作步骤
    实现Java线程池共享变量的步骤为了实现Java线程池共享变量,我们需要以下步骤:步骤描述步骤一创建一个线程池步骤二创建一个共享变量步骤三在需要共享变量的地方,使用线程池提交任务步骤四在任务中使用共享变量进行操作下面我将详细介绍每一步的操作和所需......
  • 解决Java 线程安全的DateFormat的具体操作步骤
    Java线程安全的DateFormat在多线程的环境下使用Java的SimpleDateFormat类进行日期格式化操作时,可能会遇到线程安全的问题。这篇文章将会介绍为什么SimpleDateFormat不是线程安全的,以及如何解决这个问题。为什么SimpleDateFormat不是线程安全的?SimpleDateFormat是Java中用于格......
  • 一个C++11的线程函数
    一个C++11的线程函数#include<iostream>#include<thread>#include<chrono>voidprintNumbers(){for(inti=1;i<=100;++i){std::cout<<i<<std::endl;std::this_thread::sleep_for(std::chrono::millis......
  • 协程
    #include"co.h"#include<stdlib.h>#include<string.h>#include<stdio.h>enumstate{CREATED=0,RUNNING,HALT,WAIT,FINISHED};#defineSTACK_SIZE4*1024*1024*sizeof(char)structco{charst......
  • aiohttp模块引出_aiohttp+多任务异步协程实现异步爬虫
    1.为什么要用aiohttp模块引出: 2.异步模块aiohttp对比requests基于同步的区别: 3.需要在response.text()前面添加await进行手动挂起: 4.response.text()前面一定要添加await再次运行程序告警取消: 5.异步爬虫get或post中写入的参数: ......
  • 2023年7月5日,生产者消费者模型,线程的休眠、礼让、合并、中断、生命周期、守护线程
    线程复习1.线程的休眠需求:编写一个抽取学员回答问题的程序,要求倒数三秒后输出被抽中的学员姓名分析:1.创建String数组存放学员姓名2.利用随机数获取学员下标3.通过下标获取学员姓名4.倒计时3秒通过for循环使用Thread.sleep(1000)来实现,Thread.sleep(1000);此方法为静态......