如何干净地退出线程C++程序?

Ris*_*ain 31 c++ multithreading signals exit

我正在我的程序中创建多个线程.按下时Ctrl-C,将调用信号处理程序.在一个信号处理程序里面,我exit(0)终于放了.问题是,有时程序安全终止,但有时,我得到运行时错误说明

abort() has been called
Run Code Online (Sandbox Code Playgroud)

那么避免错误的可能解决方案是什么?

Som*_*ude 30

通常的方法是设置一个由所有线程(包括主线程)检查的原子标志(如std::atomic<bool>).如果设置,则子线程退出,主线程开始到join子线程.然后你可以干净利落地退出.

如果您使用std::thread线程,这可能是您崩溃的原因.joinstd::thread对象被破坏之前必须有线程.

  • 我倾向于说,当用作退出线程标志时,非原子bool可以明确地**导致问题.主要问题是线程经常在循环中检查该标志`while(!exitThread()){doWork(); }`.如果这是一个非原子变量,它最终可能会从循环`if(exitThread)返回中被提升; while(true){doWork(); }`. (22认同)
  • 实际上是@Hamed?对于一个简单的`bool`标志然后没有,不多.理论上?是的,这是一场数据竞赛.但最好总是安全,所以你应该使用例如`std :: atomic <bool>`. (6认同)
  • @Hamed有用的附加阅读:[sig_atomic_t和std :: atomic <> interchangable](/sf/ask/1116432551/) (5认同)
  • @KhouriGiordano首先,`volatile`不提供线程安全性.请停止击败[死马](/sf/ask/319058561/#4558031).其次,标准没有声明"所有核心立即看到新值"的原子.轻松的内存顺序就足够了 - 理论上 - 除非你使用RMW(包括CAS),否则没有直接的开销,例如x86-64.事实上,我只是在godbolt上用MSVC和GCC 7.0测试它,轻松的商店和`bool`的负载被翻译成简单的`MOV`s. (5认同)
  • @Someprogrammerdude我猜你指定bool实际上是保存,因为它这么小的类型.但是原子不仅对它们的操作的原子性很重要,它关于内存排序也是因为它们在多线程代码中是有用的 (4认同)
  • @ArneVogel**巨大的**问题在于,凭借其无限的智慧,微软为其X86/X64编译器的`volatile`关键字添加了内存屏障语义.所以不幸的是,尽管它不符合标准,但在这种情况下,`volatile`在MSVC X86/X64下可以正常工作.也就是说,他们自己的页面现在解释了确切的内部工作原理,并建议不要使用它来进行多线程操作.https://docs.microsoft.com/en-us/cpp/cpp/volatile-cpp值得称道的是,他们现在还提供了`/ volatile:iso`编译器开关,使`volatile'在X86/X64上正常工作. (3认同)
  • @KhouriGiordano我不认为这是一个好主意,因为volatile只保证指令不被省略,指令顺序保持不变,但不保证内存屏障可以强制执行缓存一致性. (2认同)

Jer*_*ner 14

其他人已经提到让信号处理程序设置为a std::atomic<bool>并让所有其他线程定期检查该值以知道何时退出.

只要所有其他线程以合理的频率周期性地唤醒,那么这种方法效果很好.

如果你的一个或多个线程纯粹是事件驱动的,那就不完全令人满意了 - 但是在一个事件驱动的程序中,线程只有在它们有一些工作要做时才会被唤醒,这意味着它们可能很好一次睡几天或几周.如果他们被迫每隔(这么多)毫秒被唤醒只是为了轮询一个原子布尔标志,这使得一个非常高CPU效率的程序的CPU效率更低,因为现在每个线程都以很短的规则间隔唤醒, 24/7/365.如果您试图节省电池寿命,这可能会特别成问题,因为它可能会阻止CPU进入省电模式.

避免轮询的另一种方法是:

  1. 在启动时,让主线程创建一个fd-pipe或socket-pair(通过调用pipe()socketpair())
  2. 让您的主线程(或可能是其他一些负责的线程)在其读取就绪的select()fd_set中包含接收套接字(或采取类似的操作poll()或线程阻塞的等待IO函数)
  3. 当执行信号处理程序时,让它将一个字节(任何字节,无关紧要)写入发送套接字.
  4. 这将导致主线程的select()调用立即返回,FD_ISSET(receivingSocket)由于收到的字节,指示为true
  5. 此时,您的主线程知道该进程退出的时间,因此它可以开始指示其所有子线程开始关闭(通过任何方便的机制;原子布尔值或管道或其他东西)
  6. 在告诉所有子线程开始关闭之后,主线程应该调用join()每个子线程,以便可以保证所有子线程 main()返回之前实际上已经消失.(这是必要的,因为否则存在竞争条件的风险 - 例如,post-main()清理代码可能偶尔释放资源,而仍在执行的子线程仍在使用它,导致崩溃)

  • 使用[`std :: condition_variable <>`](http://en.cppreference.com/w/cpp/thread/condition_variable)比套接字更便携,并且不会占用尽可能多的系统资源. (3认同)

Yak*_*ont 10

你必须接受的第一件事是线程很难.

"使用线程的程序"与"使用内存的程序"一样通用,你的问题类似于"如何使用内存破坏程序中的内存?"

处理线程问题的方法是限制线程的使用方式和线程的行为.

如果您的线程系统是由数据流网络组成的一堆小操作,则隐含保证如果操作太大,则会将其分解为较小的操作和/或与系统进行检查点,然后关闭看起来非常不同比如果你有一个线程加载一个外部DLL,然后运行它从1秒到10小时到无限长度的某个地方.

像C++中的大多数东西一样,解决你的问题将是关于所有权,控制和(最后的手段)黑客攻击.

与C++中的数据一样,每个线程都应该被拥有.线程的所有者应该对该线程有很大的控制权,并且能够告诉它应用程序正在关闭.关闭机制应该是健壮的并且经过测试,并且理想地连接到其他机制(例如早期中止投机任务).

你调用exit(0)的事实是一个坏兆头.它意味着您的主要执行线程没有干净的关闭路径.从那里开始; 中断处理程序应该通知线程应该开始关闭,然后你的主线程应该正常关闭.所有堆栈帧都应该展开,数据应该清理,等等.

然后,同样类型的逻辑允许干净和快速关闭也应该应用于您的线程代码.

有人告诉你它就像条件变量/原子布尔一样简单,并且轮询正在向你出售货物清单.如果你很幸运,那只会在简单的情况下起作用,并且确定它是否可靠地工作会非常困难.