本文主要是介绍补篇协程:协程(Coroutine)里通过挂起suspend函数实现异步IO操作,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
异步IO的概念
异步IO是一种非阻塞的数据读写方法,异步IO与同步IO相对。 当一个异步过程调用发出后,调用者不能立刻得到结果。 实际的IO处理部件在完成操作后,会通过状态、通知或回调机制来通知调用者。
在一个CPU密集型的应用中,有一些需要处理的数据可能放在磁盘上。预先知道这些数 据的位置,所以预先发起异步IO读请求。等到真正需要用到这些数据的时候,再等待异步IO完成后获取数据。这种方式使用了异步IO允许程序在等待IO操作完成的同时继续执行其他任务,从而提高了系统的整体效率。
异步IO将比特分成小组进行传送,小组可以是8位的1个字符或更长。发送方可以在任何时刻发送这些比特组,而接收方从不知道它们会在什么时候到达。
异步传输存在一个潜在的问题,即接收方并不知道数据会在什么时候到达。在它检测到数据并做出响应之前,第一个比特已经过去了。这就像有人出乎意料地从后面走上来跟你说话,而你没来得及反应过来,漏掉了最前面的几个词。因此,每次异步传输的信息都以一个起始位开头,它通知接收方数据已经到达了,这就给了接收方响应、接收和缓存数据比特的时间;在传输结束时,一个停止位表示该次传输信息的终止。按照惯例,空闲(没有传送数据)的线路实际携带着一个代表二进制1的信号,异步传输的开始位使信号变成0,其他的比特位使信号随传输的数据信息而变化。最后,停止位使信号重新变回1,该信号一直保持到下一个开始位到达。例如在键盘上数字“1”,按照8比特位的扩展ASCII编码,将发送“00110001”,同时需要在8比特位的前面加一个起始位,后面一个停止位。
Android 实现异步IO操作:
在Android开发中,异步IO通常用于执行耗时的网络请求、文件读写等操作,避免UI线程被长时间运行的IO耗时任务阻塞(避免阻塞UI线程,保持应用的响应性)。但是,在使用异步IO时,也需要注意线程安全和资源管理的问题,确保不会出现内存泄漏或竞态条件等问题。
Android提供了多种机制来实现异步IO操作:使用AsyncTask,Thread,HandlerThread,IntentService,JobIntentService,RxJava等机制实现异步IO操作.它们本质上都是对线程或线程池的封装,该类可以将耗时的操作放在后台线程池来处理,而不需要人为地另开线程来处理。
使用AsyncTask实现异步IO操作:
这是一个轻量级的异步类,适合在后台执行简单的异步任务,如网络请求或文件读写。它包含doInBackground
方法在后台线程执行耗时操作,onProgressUpdate
在UI线程更新进度,以及onPostExecute
在任务完成后在UI线程执行后续操作。使用AsyncTask来实现异步IO的优点就是简单便捷,各个过程都有明确的回调,过程可控。
使用HandlerThread实现异步IO操作:
HandlerThread是一种在单独的线程中处理消息和Runnable对象的机制。通过使用Handler与HandlerThread结合,可以在后台线程中处理耗时的操作,并通过Handler将结果发送回UI线程进行更新。
使用IntentService实现异步IO操作:
IntentService是一种在后台执行异步任务的服务。它会在一个单独的工作线程中处理传入的Intent,并自动处理线程间的通信。IntentService适用于执行不需要立即返回结果的长时间运行的操作。
使用JobIntentService实现异步IO操作:
在Android O及更高版本中,建议使用JobIntentService作为IntentService的替代方案。它提供了更多的灵活性和更好的电池效率。
使用RxJava实现异步IO操作:
RxJava是一个在Java VM上使用可观察序列来组成异步和基于事件的程序的库。它提供了一种声明式的方式来处理异步数据流。
Handler
在Android中,Handler
本身并不直接实现异步IO操作。
可以在后台子线程中执行IO操作(耗时任务), 然后通过Handler将数据结果,发送传递 给UI主线程进行更新,从而间接地支持异步IO的实现。
例子:使用Handler
结合Thread
实现异步IO:
1.我们首先初始化了一个
Handler
对象,它绑定到主线程的Looper
上,这样我们就可以在主线程中执行Runnable
对象。2.然后,我们创建并启动了一个新的
Thread
来执行耗时的IO操作。3.在后台线程中,我们调用
performIOOperation()
方法模拟了一个耗时的IO操作。4.一旦IO操作完成,我们使用
Handler
的post
方法将一个Runnable
对象发送回主线程。这个Runnable
对象包含了更新UI或处理IO操作结果的逻辑。5.最后,在
updateUIWithResult()
方法中,我们执行了必须在主线程中完成的UI更新操作。
public class AsyncIOActivity extends AppCompatActivity {private static final String TAG = "AsyncIOActivity";private Handler mainHandler;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_async_io);// 初始化主线程的HandlermainHandler = new Handler(Looper.getMainLooper());// 启动后台线程执行IO操作new Thread(new Runnable() {@Overridepublic void run() {// 模拟耗时IO操作,比如网络请求或文件读写final String result = performIOOperation();// 使用Handler将结果发送回主线程mainHandler.post(new Runnable() {@Overridepublic void run() {// 在主线程中更新UI或处理结果updateUIWithResult(result);}});}}).start();}private String performIOOperation() {// 在这里执行实际的IO操作,例如网络请求或文件读写// 这个操作是耗时的,因此应该在后台线程中执行// 模拟耗时操作try {Thread.sleep(2000); // 假设耗时2秒} catch (InterruptedException e) {e.printStackTrace();}return "IO操作完成";}private void updateUIWithResult(String result) {// 在这里更新UI或处理从IO操作中获取的结果// 这个操作必须在主线程中执行,因为Android不允许在非主线程中更新UILog.d(TAG, "UI updated with result: " + result);// 例如:textView.setText(result);}
}
在主线程也可以通过 Handler发消息给子线程,不过在子线程接收数据,需要轮训Handler,需要初始化Looper.prepare()和Looper.loop()。
public class LooperThreadActivity extends Activity {private Handler mHandler = null;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);new MyThread().start();//发消息到目标子线程mHandler.obtainMessage(0).sendToTarget();}class MyThread extends Thread{@Overridepublic void run() {super.run();//1.建立消息循环,初始化LooperLooper.prepare();mHandler = new Handler(){@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);int what = msg.what;if(what == 0){//}}};//启动消息循环Looper.loop();}} }
使用协程(Coroutine)实现异步IO操作
Kotlin 协程(Coroutine)是一种轻量级的线程,它允许我们以同步的方式编写异步代码,从而极大地简化了异步编程的复杂性。协程不是真正的线程,它们是在单个线程上通过协作调度来执行的,这避免了线程切换的开销,提高了效率。下面将详细讲解 Kotlin 使用协程实现异步 IO 的原理和流程。
使用协程实现异步 IO 的原理
1.挂起函数(Suspend Function):
Kotlin 协程的核心是suspend挂起函数。这些suspend挂起函数可以在执行过程中暂停(挂起),等待某个异步任务(IO操作,网络请求)完成后再恢复执行。这使得协程能够在不阻塞主线程的情况下执行耗时的操作。
挂起函数(Suspend Functions)在 Kotlin 协程中扮演着至关重要的角色,它们是实现异步 IO 的 关键。
挂起函数 原理:
挂起函数必须在一个协程上下文中执行,通常是通过
launch
、async
等协程构建器创建的。此外,挂起函数只能被其他挂起函数或协程调用,以确保正确的执行顺序和线程安全性。挂起函数通过非阻塞性和协作式调度的原理,实现了在 Kotlin 协程中执行异步 IO 操作的高效流程。它们允许代码以同步的方式编写,而实际上却是在底层执行异步操作,从而简化了异步编程的复杂性。❶非阻塞性:挂起函数允许协程在遇到耗时的操作时(IO 操作), 而不会阻塞当前(主)线程。这使得(主)线程可以继续执行其他任务,从而提高了整体的并发性能。
❷协作式调度:协程不是由操作系统来调度的,而是由用户代码(或者库代码)来显式地调度和挂起。挂起函数是这种协作式调度的关键部分。当这个挂起函数被调用执行时,它会暂停当前协程的执行,并释放当前(主)线程的控制权。当异步操作完成或条件满足时,协程会恢复执行,恢复对当前(主)线程的控制权。
❸轻量级:由于挂起函数并不涉及线程的切换,因此它们相比于传统的线程或回调机制更加轻量级。这减少了系统资源的消耗,提高了程序的性能。
2.协程构建器(Coroutine Builder):
Kotlin 标准库提供了多个协程构建器,如 launch
、async
和 runBlocking
等。这些构建器用于创建协程并启动它们的执行。
3.调度器(Dispatcher):
协程的调度器决定了协程在哪个线程或线程池上执行任务。如果你要更新UI,就让调度器把协程调度到UI主线程Dispatchers.Main
(或许默认就在UI主线程);如果你要做异步IO耗时任务,就让调度器把协程调度到IO子线程Dispatchers.IO
。
Kotlin 提供了默认的调度器,如 Dispatchers.Default
(用于计算密集型任务)、Dispatchers.IO
(用于 IO 密集型任务)和 Dispatchers.Main
(用于在 Android 的主线程上执行 UI 更新)。
4.延续(Continuation):
协程的挂起和恢复是通过 Continuation 机制实现的。
当一个挂起函数被调用时,它会将当前的执行状态保存到一个 Continuation 对象中,并释放当前线程的控制权(不在当前线程里了)。
当异步操作完成时,它会恢复该 Continuation,从而使挂起函数继续执行,重新掌控当前线程的控制权(又回到当前线程里了)。
使用协程实现异步 IO 的流程
以下是一个使用 Kotlin 协程实现异步 IO 的典型流程:
1.添加依赖:
首先,你需要在项目的 build.gradle
文件中添加 Kotlin 协程的依赖。
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:<version>"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:<version>" // 对于 Android 项目
2.创建一个协程:
使用协程构建器(如 GlobalScope.launch
或 viewModelScope.launch
对于 Android ViewModel)来创建协程。
//协程调度器调度到IO子线程
GlobalScope.launch(Dispatchers.IO) {// 在这里执行异步 IO 操作
}
3.调用挂起函数:
在协程中,你可以调用挂起函数来执行异步 IO 操作。这些挂起函数通常是异步 API 的封装,它们使用协程的挂起和恢复机制来处理异步性。
使用挂起函数withContext, 指定一个不同的调度器(
Dispatchers.IO
)调度到IO子线程里,在代码块里执行异步IO操作。释放对当前主线程的控制权(不在当前主线程里了)。
/**
使用挂起函数withContext, 指定一个不同的调度器(Dispatchers.IO )调度到IO子线程里,在代码块里执行异步IO操作
*/val result = withContext(Dispatchers.IO) {// 执行网络请求或文件读写等异步操作// 返回操作结果
}
4.挂起协程:
当挂起函数被调用时,它会检查当前是否处于可以立即执行的状态。如果不能立即执行(比如需要等待网络响应),挂起函数就会暂停协程的执行,并释放当前(主)线程的控制权。此时,协程的状态会被保存,以便稍后恢复。
5.执行异步 IO操作
挂起函数withContext内部会开始执行异步IO操作。这通常涉及到底层库或框架的异步 API,比如使用 kotlinx.coroutines
提供的网络请求库。这些 API 在后台线程上执行实际的 IO 操作,而不会阻塞主线程。
6.恢复协程:
当异步操作完成(比如网络请求返回了结果),挂起函数withContext会接收到通知。此时,它会检查之前挂起的协程是否还存活,并尝试恢复该协程的执行。恢复过程包括重新获取对(主)线程的控制权,并从上一次挂起的位置继续执行代码。
7.更新 UI:
一旦协程恢复执行,挂起函数withContext会返回异步IO操作的结果(或异常)。你可以在协程中处理这个结果,比如更新 UI 或进行进一步的计算。
如果需要在 UI 线程上更新数据结果,你可以使用
withContext(Dispatchers.Main),
指定一个不同的调度器(Dispatchers.Main
)调度到Main
线程里(切换到UI主线程),在代码块里执行更新数据结果 。
//返回数据结果result
val result=withContext(Dispatchers.Main) {// 更新 UI,如设置文本、显示图像等
}
在协程中,你可以使用 try-catch
块来处理可能出现的异常。
try {//返回数据结果result val result = withContext(Dispatchers.IO) {// 更新 UI,如设置文本、显示图像等// 执行可能抛出异常的异步操作}
} catch (e: Exception) {// 处理异常
}
8.取消协程:
如果需要取消协程的执行,你可以使用 Job
对象来管理协程的生命周期。launch
和 async
构建器都会返回一个 Job
对象,你可以调用它的 cancel
方法来取消协程。
val job = GlobalScope.launch {// 协程代码
}// 在某个时刻取消协程
job.cancel()
协程任务和异步任务区别在哪里?
协程任务和异步任务都是处理并发和异步操作的重要手段,但它们在执行方式、并发执行、任务调度与使用场景等方面存在明显的区别。选择使用哪种方式取决于具体的应用需求和场景。在实际开发中,应根据项目的具体需求和团队的技能水平来选择合适的方案。
协程任务和异步任务在多个方面存在显著的区别。以下是它们之间的主要差异:
1.执行方式不同:
异步任务(Asynchronous Task):异步任务不进入主线程,而是进入任务队列。只有当主线程的任务执行完毕后,任务队列才会通知主线程请求执行任务,这时该任务才会进入主线程执行1。异步任务通常需要自己不断轮询,条件不满足就返回特定值(如EAGAIN),然后重新尝试2。
协程任务(Coroutine Task):协程任务是基于事件驱动的,它使用库封装好的API,这些API会往事件驱动模块(如epoll)订阅事件,并记录一些上下文(如回调)。当条件满足时,它会执行相应的回调。协程任务结合了同步和异步的优点,允许用户以同步的方式编写异步代码,从而简化了异步编程的复杂性124。
2.并发执行不同:
异步任务:异步任务通常用于执行不会阻塞程序执行的任务,如网络请求、定时器、事件处理或异步函数1。
协程任务:协程在单线程下通过用户自己控制任务调度来实现并发。当遇到IO阻塞时,协程会切换到另一个任务执行,以此来提升效率3。协程的切换开销更小,属于程序级别的切换,因此更加轻量级3。
3.任务调度与状态保存不同:
协程任务:协程允许用户程序自己控制任务调度,可以检测IO操作并在遇到IO操作时进行切换。同时,协程可以控制多个任务之间的切换,并在切换之前保存任务的状态,以便重新运行时可以从暂停的位置继续执行3。
4.使用场景不同:
异步任务:更适用于那些天然就是异步性质的操作,比如网络请求,这些操作本身就支持异步处理,因此使用异步任务可以更有效地利用系统资源。
协程任务:更适用于那些需要精细控制并发和IO操作的场景,特别是在需要编写复杂异步逻辑的代码时,协程可以大大简化代码结构,提高代码的可读性和可维护性。
挂起函数和异步任务区别在哪里?
挂起函数和异步任务都是处理异步操作的重要手段,但它们在实现机制、编程模型、错误处理和性能开销方面存在明显的区别。Kotlin 协程的挂起函数提供了一种更加直观和高效的异步编程方式,适用于需要简化异步逻辑和提高代码可读性的场景。而异步任务则更适合于需要利用多线程并行处理任务的场景。在选择使用哪种方式时,需要根据具体的应用需求和场景来做出决策。
挂起函数(Suspend Function)和异步任务(Asynchronous Task)在异步编程中有不同的实现方式和应用场景,它们之间的主要区别如下:
1.执行机制不同
挂起函数:在 Kotlin 协程中,挂起函数允许程序在执行过程中暂停并恢复,而不需要阻塞线程。当挂起函数被调用时,它会将当前协程的执行状态保存起来,并释放线程的执行权,以便其他协程或任务能够运行。当挂起的原因(如IO操作完成)消失时,协程会恢复执行,从挂起点继续执行后续的代码。挂起函数通常用于封装那些可能阻塞的异步操作,如网络请求或文件读写。
异步任务:异步任务通常指的是在单独的线程或线程池中执行的任务,它们与主线程并行运行,不会阻塞主线程的执行。异步任务通过回调函数、Promise、Future 或其他机制来处理异步操作的结果。异步任务的执行和结果处理通常分散在多个方法或回调中,使得代码结构相对复杂。
2.编程模型不同:
挂起函数:Kotlin 协程提供了一种更直观的编程模型,使得异步代码看起来像是同步执行的。挂起函数的使用使得异步逻辑可以更加自然地融入代码结构中,减少了回调嵌套和代码分散的问题,提高了代码的可读性和可维护性。
异步任务:异步任务的编程模型通常涉及更多的回调和状态管理。开发者需要显式地处理异步操作的启动、结果监听和异常处理。这种模型在复杂的异步逻辑中容易导致代码结构混乱和错误处理困难。
3.错误处理不同:
挂起函数:在 Kotlin 协程中,错误处理通常使用 try-catch 语句来捕获和处理异常。挂起函数中的异常会冒泡到调用栈的顶层,使得错误处理更加集中和方便。
异步任务:异步任务的错误处理通常需要在回调函数中显式处理。这要求开发者在每个回调中都要考虑异常处理的情况,增加了代码的复杂性和出错的可能性。
4.上下文切换和开销不同:
挂起函数:挂起函数在协程间的上下文切换通常比线程切换开销要小得多,因为它们是在用户空间内完成的,不涉及内核态的切换。这使得协程在处理大量轻量级任务时更加高效。
异步任务:异步任务通常使用线程或线程池来执行,线程切换涉及到内核态的切换,因此开销相对较大。在处理大量异步任务时,线程的管理和调度可能会成为性能瓶颈。
挂起函数与协程的关系(区别)
挂起函数是协程中实现异步IO操作和执行流程控制的关键组成部分。它们允许我们以同步的方式编写异步代码,提高程序的并发性能,并简化复杂的异步逻辑。在Kotlin等语言中,通过使用挂起函数和协程,我们可以更加高效地处理异步IO和其他异步任务。
挂起函数只能在协程环境中被调用,它们不能在普通的同步代码块中直接使用。这是因为挂起函数的实现依赖于协程的运行时系统,该系统负责管理协程的挂起和恢复操作。
挂起函数与协程之间存在着紧密的关系,主要体现在以下几个方面:
1.协程的定义与特性:
协程是一种用户态的轻量级线程,它可以在单线程中实现非阻塞的并发执行。协程具有可中断和可恢复的特性,这意味着它可以在执行过程中被挂起,并在稍后的某个时刻恢复执行。这种中断和恢复的能力使得协程可以非常高效地处理异步操作,如IO操作,而不会阻塞线程。
2.挂起函数的作用:
挂起函数是协程中用于实现挂起和恢复操作的关键函数。当调用一个挂起函数时,当前协程会暂停执行,并释放线程的控制权,以便其他任务可以执行。
挂起函数内部通常包含一些可能阻塞的耗时的异步IO操作,如网络请求或文件读写。当这些异步操作完成时,挂起函数会恢复执行,并继续执行后续的代码。
3.实现异步IO:
挂起函数允许我们以同步的方式编写异步代码,这对于实现异步IO特别有用。通过挂起函数,我们可以避免阻塞线程,从而提高程序的并发性能。挂起函数内部通常封装了对底层异步API的调用,如网络请求或文件读写,使得异步操作看起来像是同步执行的2。
4.协作式调度:
协程的调度是协作式的,这意味着协程的挂起和恢复是由用户代码(或库代码)显式控制的,而不是由操作系统调度的。挂起函数在这种协作式调度中起着关键作用,它们决定了何时挂起协程以及何时恢复协程的执行。
5.简化异步逻辑:
通过使用挂起函数,我们可以将复杂的异步逻辑封装在简单的函数中,使得代码更加清晰和易于维护。这有助于减少回调地狱和提高代码的可读性。
挂起函数和协程的例子
下面例子,演示了如何使用 Kotlin 的协程和挂起函数来实现异步 IO操作:
1.在 Gradle 构建文件中添加以下依赖:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1" // 使用最新版本
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1" // 如果你在 Android 项目中使用
2.创建一个挂起函数来模拟异步 IO 操作(网络请求):
class SuspendActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?){lifecycleScope.launch(context=Dispatchers.Main,block={Log.e(TAG, "当前所处的线程1: ${Thread.currentThread().name} ") //当前所处的线程1: mainLog.e(TAG, "获取 协程在上下文指定的调度器1:${this.coroutineContext}") //获取 协程在上下文指定的调度器1:[StandaloneCoroutine{Active}@8872fef, Dispatchers.Main]//在launch协程里调用挂起函数findBigPrime(),执行异步 IO 操作 它不会阻塞当前线程val bigIntegerResult:BigInteger=findBigPrime()Log.e(TAG, "输出bigIntegerResult=$bigIntegerResult")Log.e(TAG, "当前所处的线程3: ${Thread.currentThread().name} ") //当前所处的线程3: mainLog.e(TAG, "获取 协程在上下文指定的调度器3:${this.coroutineContext}") //获取 协程在上下文指定的调度器3:[StandaloneCoroutine{Active}@8872fef, Dispatchers.Main]//在主线程更新显示请求的数据bigIntegerResultfindViewById<TextView>(R.id.textview).text= bigIntegerResult.toString()})}/** 改进正确的做法:* 借助 withContext 我们把BigInteger.probablePrime()的耗时CPU 计算操作 从当前主线程(切换到一个子线程里去)挪到了一个默认的后台线程池。不会阻塞当前的主线程,主线程能够进行其他的初始化操作* 当耗时操作完成计算出结果数据后,再在主线程更新显示请求的数据** */suspend fun findBigPrime():BigInteger{val bigIntegerResult:BigInteger=withContext(context=Dispatchers.Default,block={Log.e(TAG, "当前所处的线程2: ${Thread.currentThread().name} ") //当前所处的线程2: DefaultDispatcher-worker-1Log.e(TAG, "获取 协程在上下文指定的调度器2:${this.coroutineContext}") //获取 协程在上下文指定的调度器2:[DispatchedCoroutine{Active}@4046afc, Dispatchers.Default]BigInteger.probablePrime(4096, Random()) //lambda最后一行结果值作为返回值})return bigIntegerResult}}
这篇关于补篇协程:协程(Coroutine)里通过挂起suspend函数实现异步IO操作的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!