计算机处理某些任务需要时间,我们希望在等待时能做点别的事(并发)。操作系统提供了一种基础并发方式,但程序员可以做得更精细、更好。
1. 长时间任务很常见,干等着太无聊
- 例子1:导出视频(计算密集型/CPU密集型)
- 想象你用电脑剪辑家庭聚会视频。最后"导出"按钮一点,电脑就呼呼响,风扇狂转,整个电脑都变慢了甚至卡住。
- 为什么? 导出视频需要电脑的"大脑"(CPU)和"图形处理器"(GPU)使出吃奶的劲儿处理每一帧画面。这非常耗费计算能力。
- 问题: 如果电脑只有一个"大脑核心"(单核CPU),并且操作系统傻乎乎地让导出任务一直霸占着这个核心直到完成,那你在这期间啥也干不了!不能上网,不能听歌,不能打字。体验极差!
- 例子2:下载文件(I/O密集型)
- 想象你让电脑从网盘下载一个共享的家庭聚会大视频。
- 过程: 电脑说:"喂,网络,给我那个文件!" 然后它就...等着。网络一点点把数据传过来。数据到了,电脑还要花时间把这些数据从临时地方搬到它该在的位置(比如你的硬盘)。虽然可能就几秒,但对每秒能做几十亿次计算的CPU来说,这几秒简直像几个世纪那么漫长!
- 问题: CPU大部分时间在干等!它明明可以趁等网络数据的时候去干点别的活(比如让你看看邮件),但传统的下载方式会让CPU傻等。
2. 操作系统的"救场":基础并发(中断)
- 操作系统怎么解决"导出视频卡死电脑"问题?
- 操作系统很聪明,它不会让一个程序(比如视频导出)永远霸占CPU。它会频繁地、快速地打断正在运行的程序(比如视频导出),哪怕只打断一瞬间。
- 效果: 在这一瞬间被打断时,操作系统赶紧让CPU去处理一下别的程序(比如你正在打的字,或者播放的音乐)。然后再切回视频导出程序继续算一点。如此反复。
- 感觉: 对你用户来说,电脑看起来是"同时"在做几件事(导出视频+让你打字/听歌),虽然CPU核心在同一时刻其实只做一件事。这就叫并发 。操作系统通过中断实现了这种并发。
- 操作系统怎么解决"下载文件时CPU干等"问题?
- 同样用中断 !当下载程序发出"要数据"的请求后,在等待网络响应的漫长(对CPU而言)时间里,操作系统会打断这个等待中的下载程序。
- 效果: CPU立刻被解放出来,去运行其他准备好运行的程序(比如刷新你的浏览器页面)。等网络数据终于到了,操作系统会再让下载程序继续运行一小会儿去处理数据,然后可能又被打断。如此反复。
- 感觉: 下载在后台默默进行,你前台还能流畅地做其他事。
3. 程序员视角:操作系统并发还不够细,我们需要更好方式
- 操作系统的并发是"粗粒度"的:
- 它主要在不同程序(进程) 之间切换。比如在视频导出程序和浏览器程序之间切换。
- 程序员需要"细粒度"并发:
- 假设你写了一个下载管理器程序:
- 你不希望用户点"开始下载"按钮后,整个程序界面就卡死、无响应,直到下载完。这体验很糟。
- 用户可能想同时开始多个下载。
- 问题根源:传统API是"阻塞"的
- 大部分用来读写文件、访问网络的函数(API),默认是阻塞式的。
- "阻塞"是什么意思? 想象你打电话给客服查快递,客服说"请稍等,我帮您查"。然后电话里就安静了,你只能拿着电话干等,不能挂断去做饭,直到客服查完回来告诉你结果。这就是"阻塞"------这个电话(函数调用)卡住了你(程序),让你啥也干不了,直到它完成。
- 在下载管理器中: 如果下载文件的函数是阻塞的,那么主线程(负责显示界面、响应用户点击)一旦调用这个下载函数,整个界面就会卡住,直到下载完成。
- 假设你写了一个下载管理器程序:
- 传统解决方案:用线程(有代价)
- 一种办法是为每个下载任务单独开一个线程 。想象线程就像公司里的不同员工。
- 主线程(前台接待员):专门负责显示界面、响应用户操作(点按钮)。
- 下载线程1(员工A):专门负责下载第一个文件。
- 下载线程2(员工B):专门负责下载第二个文件。
- 好处: 主线程(接待员)不会被下载任务卡住,用户界面依然流畅。多个下载可以同时进行(员工A和B各忙各的)。
- 坏处: 创建和管理很多线程本身也需要消耗电脑资源(CPU时间、内存)。想象一下,如果用户一下子要下载1000个文件,你就得雇1000个临时工(线程),光是管理这1000个员工的开销就很大(线程开销),电脑可能反而变慢了。
- 一种办法是为每个下载任务单独开一个线程 。想象线程就像公司里的不同员工。
- 理想解决方案:非阻塞API + 异步编程
- 非阻塞API: 想象打电话给一个智能客服。你说"帮我查快递",智能客服说"好的,查到了我会回拨你,你先去忙吧!",然后主动挂断了电话。这样你就可以立刻挂电话去做饭了!这就是"非阻塞"------你(主线程)没有被卡住,可以立刻去做别的事。
- 异步编程(配合非阻塞API): 光有非阻塞API还不够,我们需要一种方便的编程方式来利用它。
- 我们希望能像写普通的、顺序执行的代码(阻塞风格)那样简单明了,但实际效果是非阻塞的。
- 目标代码:
let data = fetch_data_from(url).await; println!("{data}");
fetch_data_from(url)
:调用非阻塞API开始下载。.await
:这个关键字是魔法!它的意思是:"我知道下载需要时间,我现在要去干点别的(比如刷新界面、处理其他下载请求)。当下载任务真的完成时,请回到这里 ,把下载好的数据赋值给data
,然后继续执行下一行println!
。"
- 过程:
- 主线程执行到
fetch_data_from(url).await;
,启动非阻塞下载。 .await
告诉程序:"下载开始了,但我不会傻等。我现在暂停 执行这个函数的后续部分(println!
),交出控制权,让主线程可以去处理其他事情(比如响应用户点击'暂停'按钮)。"- 下载任务在后台(可能由操作系统或底层库管理)默默进行。
- 当下载完成时,一个"完成信号"会发出。
- 主线程在合适的时机(比如处理完一个用户点击后)检查到下载完成了,它就回到之前暂停的地方 ,把下载好的数据放进
data
,然后继续执行println!("{data}");
。
- 主线程执行到
- 感觉: 代码看起来是顺序写的("开始下载" -> "打印数据"),但实际执行时,在
await
点发生了"暂停并切换",等下载完成再"回来继续"。程序(主线程)在等待期间没有被阻塞,可以高效地做其他工作。
总结关键概念
- CPU密集型 (Compute-bound): 任务主要消耗CPU/GPU的计算能力(如视频转码、复杂计算)。快慢取决于CPU/GPU的速度。
- I/O密集型 (IO-bound): 任务主要消耗在等待输入/输出上(如网络下载、读写硬盘、等待数据库响应)。快慢取决于外部设备(网络、磁盘)的速度。
- 阻塞 (Blocking): 调用一个函数时,程序必须停下所有事情,一直等到这个函数完全执行完毕并返回结果,才能继续往下走。像打电话干等客服查快递。
- 非阻塞 (Non-blocking): 调用一个函数时,函数立刻返回 (可能返回一个"还没完成"的信号),程序不用干等,可以立刻去做其他事情。像打电话让智能客服查到后回拨。
- 并发 (Concurrency): 让一个系统(一台电脑、一个程序)看起来能同时处理多个任务。实际可能是在多个任务间快速切换(单核CPU),也可能是真并行(多核CPU)。
- 操作系统中断: 操作系统强制暂停当前运行的程序,让CPU去运行其他程序的基础并发机制。主要解决程序间的资源争抢。
- 线程: 程序内部的执行分支。可以用于实现并发,但创建和管理大量线程开销大。
- 异步编程 (Async/Await):
- 一种编写非阻塞代码的编程模式。
- 使用
async
标记的函数可以在内部使用await
。 await
点:程序执行到这里,如果等待的操作(如网络请求)还没完成,就暂停 这个函数的执行,交出控制权 让程序去做其他事。当等待的操作完成时,程序会回来从这里继续执行。- 目标: 用看起来像顺序执行(阻塞风格)的简洁代码,实现高效的、非阻塞的并发操作(尤其是处理大量I/O密集型任务),避免大量线程的开销。
简单来说: 为了避免电脑在干重活(计算)或者等人送东西(I/O)时我们只能傻等,操作系统会自己切换任务来让我们能同时干几件事(基础并发)。但程序员写程序时(比如下载工具),需要更精细的控制,让程序内部也能"同时"做多个任务且不卡界面。传统用"多开员工"(多线程)的方式管太多人很累。更好的办法是用"智能客服+任务暂停/继续"的方式(非阻塞API + 异步编程),这样代码写起来简单直接(像.await
),效率又高,特别适合处理大量需要等待的任务(如下载、网络请求)。