找回密码
 立即注册
首页 业界区 业界 从源码角度解析C++20新特性如何简化线程超时取消 ...

从源码角度解析C++20新特性如何简化线程超时取消

国语诗 3 小时前
C++20中增加了很多重量级新特性,它不仅带来了ranges、concept和协程,也为多线程编程带来了jthread和stop_source这些强力辅助。利用这些新特性,我们可以更高效地编写并发程序。
今天要说的就是利用jthread和stop_source来简化线程超时控制的实现,最终我们可以实现一个简单高效、可维护性不输给号称“天生支持并发”的Go语言的版本。
为什么需要超时控制

超时控制是很常见的需求,最普遍的场景是为了防止程序卡住或者长时间占用资源,程序会主动取消掉一些超过允许运行时间的或者无响应的线程,比如一些耗时很长的网络连接处理线程等。当然用户等得不耐烦了手动点击取消任务执行也勉强可以算在内。
通常超时发生或者用户点击取消之后,我们都期待线程能迅速终止执行并让整个程序保持一个完整且安全的状态。然而现实是复杂的,想实现上述功能对于线程来说是一件难事,尤其在Linux系统上。
第一个难点是如何让线程知道自己要退出。对于进程来说这不是难点,因为不管进程在做什么,我们都可以靠向其发送信号来立即中断进程的执行(前提是线程没有屏蔽这个信号),这样进程的停止请求可以被立即感知到,进程从而可以尽快完成善后工作退出执行。同样的招数对多线程程序来说就没那么好用了——信号默认是发给整个进程的,为了能让每个线程独立地接收信号,我们需要保存线程的标识符并在每个线程中设置接收和屏蔽信号的mask,这大大增加了程序的复杂性;其次信号处理函数是整个进程内所有线程共享的,我们需要额外的手段来保证并发安全,同时还得兼顾信号处理函数需要可重入、快速执行的最佳实践,这会提高程序的开发难度。
第二个难点在于如何保证线程一定会退出执行。前面说到信号可以打断进程的执行,但这只是通知,实际上进程完全可以在信号处理函数返回后无视这个通知继续运行,或者有一种更普遍的场景——程序正好卡在某个系统调用上,而程序又设置了系统调用被信号中断后自动重启,这样即使我们有效通知了进程,进程也会在收完通知之后再次进入系统调用从而无法响应停止请求。所以作为保底手段,Linux可以发送SIGKILL这个信号强制终止进程,这个信号无法捕获也无法屏蔽,是我们货真价实的“底牌”。
上述的情况在多线程中同样存在,而且我们没有“底牌”可用——因为不管给哪个线程发送SIGKILL,都会杀死整个进程而不是单独接收到信号的那个线程。另外即使有办法强制终止线程(比如早期的JVM),我们还会遇到资源释放的问题。进程退出执行之后,内核会尽可能释放进程持有的所有资源,打开的文件会被关闭,缓冲区的内容会被刷新,文件锁之类的同步机制也会正常解锁;但线程并没有这种自动清理机制,清理工作完全需要手动执行,一旦进程没有释放自己持有的资源就退出,系统就会遇到各种数据损坏和死锁等并发问题,排查和修复会极其困难。
为了克服上述难点并安全高效地实现终止超时线程的执行,我们需要一些额外的控制手段。这也一直都是开发者中的热门话题。
在介绍C++20如何简化超时控制之前,我们先来看看前人的智慧成果。
Golang实现超时控制

Golang是天生支持并发的语言,这一点可谓名副其实,尤其是在超时控制上。
我们直接看个例子,例子里有主线程和工作线程,工作线程超时时间为5秒,如果超过这个时间还有线程没完成工作,就取消所有线程的执行。Golang里没有系统级的线程,但我们可以用goroutine模拟。
在工作线程中我们用sleep代替耗时的工作,这样便于测试:
[code]func Work(ctx context.Context, id int) error {        for range 10 {                select {                case

相关推荐

您需要登录后才可以回帖 登录 | 立即注册