当前位置: 首页 > news >正文

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

为什么需要超时控制

超时控制是很常见的需求,最普遍的场景是为了防止程序卡住或者长时间占用资源,程序会主动取消掉一些超过允许运行时间的或者无响应的线程,比如一些耗时很长的网络连接处理线程等。当然用户等得不耐烦了手动点击取消任务执行也勉强可以算在内。

通常超时发生或者用户点击取消之后,我们都期待线程能迅速终止执行并让整个程序保持一个完整且安全的状态。然而现实是复杂的,想实现上述功能对于线程来说是一件难事,尤其在Linux系统上。

第一个难点是如何让线程知道自己要退出。对于进程来说这不是难点,因为不管进程在做什么,我们都可以靠向其发送信号来立即中断进程的执行(前提是线程没有屏蔽这个信号),这样进程的停止请求可以被立即感知到,进程从而可以尽快完成善后工作退出执行。同样的招数对多线程程序来说就没那么好用了——信号默认是发给整个进程的,为了能让每个线程独立地接收信号,我们需要保存线程的标识符并在每个线程中设置接收和屏蔽信号的mask,这大大增加了程序的复杂性;其次信号处理函数是整个进程内所有线程共享的,我们需要额外的手段来保证并发安全,同时还得兼顾信号处理函数需要可重入、快速执行的最佳实践,这会提高程序的开发难度。

第二个难点在于如何保证线程一定会退出执行。前面说到信号可以打断进程的执行,但这只是通知,实际上进程完全可以在信号处理函数返回后无视这个通知继续运行,或者有一种更普遍的场景——程序正好卡在某个系统调用上,而程序又设置了系统调用被信号中断后自动重启,这样即使我们有效通知了进程,进程也会在收完通知之后再次进入系统调用从而无法响应停止请求。所以作为保底手段,Linux可以发送SIGKILL这个信号强制终止进程,这个信号无法捕获也无法屏蔽,是我们货真价实的“底牌”。

上述的情况在多线程中同样存在,而且我们没有“底牌”可用——因为不管给哪个线程发送SIGKILL,都会杀死整个进程而不是单独接收到信号的那个线程。另外即使有办法强制终止线程(比如早期的JVM),我们还会遇到资源释放的问题。进程退出执行之后,内核会尽可能释放进程持有的所有资源,打开的文件会被关闭,缓冲区的内容会被刷新,文件锁之类的同步机制也会正常解锁;但线程并没有这种自动清理机制,清理工作完全需要手动执行,一旦进程没有释放自己持有的资源就退出,系统就会遇到各种数据损坏和死锁等并发问题,排查和修复会极其困难。

为了克服上述难点并安全高效地实现终止超时线程的执行,我们需要一些额外的控制手段。这也一直都是开发者中的热门话题。

在介绍C++20如何简化超时控制之前,我们先来看看前人的智慧成果。

Golang实现超时控制

Golang是天生支持并发的语言,这一点可谓名副其实,尤其是在超时控制上。

我们直接看个例子,例子里有主线程和工作线程,工作线程超时时间为5秒,如果超过这个时间还有线程没完成工作,就取消所有线程的执行。Golang里没有系统级的线程,但我们可以用goroutine模拟。

在工作线程中我们用sleep代替耗时的工作,这样便于测试:

func Work(ctx context.Context, id int) error {
for range 10 {
select {
case <-ctx.Done():
fmt.Printf("worker %d: canceled\n", id)
return ctx.Err()
default:
}
if rand.IntN(2) == 0 {
time.Sleep(500 * time.Millisecond)
} else {
time.Sleep(time.Second)
}
}
fmt.Printf("worker %d: done\n", id)
return nil
}

超时控制是ctx参数实现的,每次循环处理前我们都会主动检查线程是否需要退出,这种协作式的“请求-检查-响应”是各种语言中取消线程执行的常见做法。

这个工作函数执行时间在5秒到10秒之间,取值的步长在0.5秒,加上go标准库默认随机数是均匀分布的,所以整体执行时间的概率是正态分布的,在7.5秒左右我们很容易看到超时和正常运行结束两种情况。所以我们把超时时间分别设为4秒、7.5秒、11秒,来进行模拟运行实验:

func main() {
// 从命令行获取超时时间,单位毫秒
timeout, err := strconv.Atoi(os.Args[1])
if err != nil {
panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Millisecond)
now := time.Now()
defer cancel()
g := &errgroup.Group{}
for i := range 3 {
g.Go(func() error {
return Work(ctx, i)
})
}
err = g.Wait()
fmt.Printf("run time: %s\n", time.Since(now))
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Tasks canceled")
return
}
panic(err)
}
fmt.Println("All work done!")
}

代码很简单,关键在这行:ctx, cancel := context.WithTimeout(context.Background(), 7500*time.Millisecond),只要我们设定的时间到了,<-ctx.Done()就会从阻塞变为非阻塞,循环开始处的检查会发现这个变化,然后会退出线程的执行。代码中使用了errgroup,但这不是必须的,实际上有很多办法可以通知主线程,这里我选择了一种最通用的,代价是代码会稍微复杂一些。

运行代码,会看到下面这样的输出,结果有很大的随机成分,下面只是无数种可能中的一种:

$ go build -o test
$ ./test 4000
worker 1: canceled
worker 0: canceled
worker 2: canceled
run time: 4.00431275s
Tasks canceled
$ ./test 7500
worker 0: done
worker 2: done
worker 1: canceled
run time: 7.507776458s
Tasks canceled
$ ./test 11000
worker 1: done
worker 2: done
worker 0: done
run time: 8.509193125s
All work done!

可以看到超时控制发挥了作用,尽管内置的time计时有一些误差,但程序的总体的运行时间是小于等于超时时间的。

Golang的超时控制可以通过context简单实现,但需要工作线程主动检查主动配合,前文我们也提到了强制终止工作线程很可能会造成并发问题,因此所有的线程超时控制中都是采用的这种协作式退出机制,即使天生并发的语言也不能免俗。作为代价,我们需要谨慎编码以免工作线程无法响应退出请求,同时还需要付出一点在循环里检查是否需要退出执行的性能损失。

C++中的典型超时控制实现

c++没有方便好用的context,想要实现协作式退出得自己造轮子。

Golang好用是因为标准库和运行时调度器隐藏了实现的细节:WithTimeout实际上会创建一个定时器,到时间后调度器会执行定时器的回调函数主动关闭ctx内部的channel,这样<-ctx.Done()就会从阻塞变成非阻塞,协程就能检查到这一变化从而退出执行。

核心只在于两点,以合适的方法标记线程已被取消和异步地在超时后设置取消标记。

http://www.jsqmd.com/news/1105717/

相关文章:

  • 调查研究-207 Claude Sonnet 5 发布:Agent 能力下放后,模型路由要从“强弱分层“改成“执行分层“
  • WorkshopDL:跨平台游戏模组获取的终极解决方案
  • 铜钟音乐:零广告干扰的现代Web音乐播放器技术实现全解析
  • 调查研究-208 OpenAI GPT-5.6 Sol / Terra / Luna 解读:AI 模型竞争正在从“更聪明“转向“能长期干活“
  • 5分钟快速上手:中国车牌生成器终极指南 - 免费开源车牌图像生成工具
  • IntelliJ IDEA AI插件性能压测实录:单次请求响应延迟<187ms、上下文窗口突破16K tokens、IDE无卡顿加载——仅3款通过 JetBrains 官方TCK认证(第2名意外落榜)
  • ComfyUI-WanVideoWrapper终极指南:零基础到实战的AI视频生成完整方案
  • 自研ChaCha20-Poly1305加密模块:移除时间戳匹配,性能提升30%+
  • 基于STM32F745VG与TPAFE0808的多通道信号采集系统设计
  • SQL查询结果导出总报错、乱码、截断?,深度解析IDEA 2023.3+版本导出引擎底层机制
  • Redis Bitmap 实现北极星日淘用户签到与活跃度统计(极致省内存)
  • STM32与A5000加密芯片实现安全物联网连接实战
  • B站视频转文字终极指南:5分钟快速获取视频文本内容
  • Typora LaTeX主题:3种应用场景深度解析与学术写作效率革命
  • 免费音乐解锁工具Unlock-Music:3步完成加密音乐格式转换
  • STM32与AD5593R实现高精度ADC-DAC混合信号处理
  • 大电流BLDC电机FOC控制方案与STM32实现
  • Android Root检测实战:RootBeer库原理、集成与对抗隐藏策略
  • 大模型相对位置编码层归零技术解析与工程落地
  • PCF8591与PIC18LF46K80的信号转换系统设计与优化
  • 2026:每月10小时免费额度,m4a转文字最简单方法省钱指南
  • 2026大二寸证件照制作工具指南:手机App、免费无水印小程序操作教程
  • AI Agent与RAG结合:构建知识增强型智能体
  • 如何高效解决Windows苹果设备驱动问题:一键安装完整指南
  • 猫抓浏览器资源嗅探插件:三步快速捕获网页视频音频的终极指南
  • 抖音无水印下载神器:3分钟搞定高清视频保存,告别水印烦恼!
  • 基于LARA-R6001与PIC18LF46K42的VoLTE通信平台开发指南
  • 大模型训练四阶段演化:从规则引擎到无监督预训练
  • PCF8591与MKV42F128VLH16的ADC/DAC信号转换实战
  • 科研工具推荐:专为硕博打造的一体化 AI 写作系统,全流程无需额外软件|PaperRed 一站式平衡写作效率与学术质量