线程:C++ 中无限循环线程的终止

Threads: Termination of infinite loop thread in c++

本文关键字:线程 终止 无限循环 C++      更新时间:2023-10-16

我正在尝试编写一个线程,该线程将在主程序和监视器的后台运行。在某些时候,主程序应该向线程发出安全退出的信号。下面是以固定间隔将本地时间写入命令行的最小示例。

#include <cmath>
#include <iostream>
#include <thread>
#include <future>
#include <chrono>
int func(bool & on)
{
while(on) {
auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
std::cout << ctime(&t) << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main()
{
bool on = true;
std::future<int> fi = std::async(std::launch::async, func, on);
std::this_thread::sleep_for(std::chrono::seconds(5));
on = false;
return 0;
}

当 "on" 变量未通过引用传递时,此代码将编译并生成预期的结果,但线程永远不会终止。一旦变量通过引用传递,我就会收到编译器错误

In file included from /opt/extlib/gcc/5.2.0/gcc/5.2.0/include/c++/5.2.0/thread:39:0,
from background_thread.cpp:3:
/opt/extlib/gcc/5.2.0/gcc/5.2.0/include/c++/5.2.0/functional: In instantiation of ‘struct std::_Bind_simple<int (*(bool))(bool&)>’:
/opt/extlib/gcc/5.2.0/gcc/5.2.0/include/c++/5.2.0/future:1709:67:   required from ‘std::future<typename std::result_of<_Functor(_ArgTypes ...)>::type> std::async(std::launch, _Fn&&, _Args&& ...) [with _Fn = int (&)(bool&); _Args = {bool&}; typename std::result_of<_Functor(_ArgTypes ...)>::type = int]’
background_thread.cpp:20:64:   required from here
/opt/extlib/gcc/5.2.0/gcc/5.2.0/include/c++/5.2.0/functional:1505:61: error: no type named ‘type’ in ‘class std::result_of<int (*(bool))(bool&)>’
typedef typename result_of<_Callable(_Args...)>::type result_type;
^
/opt/extlib/gcc/5.2.0/gcc/5.2.0/include/c++/5.2.0/functional:1526:9: error: no type named ‘type’ in ‘class std::result_of<int (*(bool))(bool&)>’
_M_invoke(_Index_tuple<_Indices...>)

你会好心地建议一种修复此代码的方法吗?

奖励问题:出了什么问题,为什么它适用于 std::ref,但不适用于正常和

std::ref

是一个开始,但这还不够。 C++ 仅保证知道另一个线程对变量的更改,如果该变量由以下任一变量保护,

a) 原子,或

b) 内存围栏(互斥锁、condition_variable等)

在允许 main 完成之前同步线程也是明智的。请注意,我有一个对fi.get()的调用,它将阻塞主线程,直到异步线程满足未来需求。

更新的代码:

#include <cmath>
#include <iostream>
#include <thread>
#include <future>
#include <chrono>
#include <functional>
#include <atomic>
// provide a means of emitting to stdout without a race condition
std::mutex emit_mutex;
template<class...Ts> void emit(Ts&&...ts)
{
auto lock = std::unique_lock<std::mutex>(emit_mutex);
using expand = int[];
void(expand{
0,
((std::cout << ts), 0)...
});
}
// cross-thread communications are UB unless either:
// a. they are through an atomic
// b. there is a memory fence operation in both threads
//    (e.g. condition_variable)
int func(std::atomic<bool>& on)
{
while(on) {
auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
emit(ctime(&t), "n");
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return 6;
}
int main()
{
std::atomic<bool> on { true };
std::future<int> fi = std::async(std::launch::async, func, std::ref(on));
std::this_thread::sleep_for(std::chrono::seconds(5));
on = false;
emit("function returned ", fi.get(), "n");
return 0;
}

示例输出:

Wed Jun 22 09:50:58 2016
Wed Jun 22 09:50:59 2016
Wed Jun 22 09:51:00 2016
Wed Jun 22 09:51:01 2016
Wed Jun 22 09:51:02 2016
function returned 6

根据要求,emit<>(...)解释

template<class...Ts> void emit(Ts&&...ts)

emit是一个返回 void 的函数,并通过 x 值引用(即常量引用、引用或 r 值引用)获取任意数量的参数。 即它将接受任何东西。这意味着我们可以调用:

  • emit(foo())- 使用函数的返回值(r 值)进行调用

  • emit(x, y, foo(), bar(), "text")- 使用两个引用、2 个 r 值引用和一个字符串文本调用

using expand = int[];将类型定义为长度不确定的整数数组。我们将仅使用它来强制计算表达式,当我们实例化类型为expand的对象时。实际的数组本身将被优化器丢弃 - 我们只想要构造它的副作用

void(expand{ ... });- 强制编译器实例化数组,但 void 强制转换告诉它我们永远不会使用实际数组本身。

((std::cout << ts), 0)...- 对于每个参数(用 ts 表示),在数组构造中扩展一个项。请记住,数组是整数。cout << ts将返回一个ostream&,因此我们使用逗号运算符对ostream<<的调用进行顺序排序,然后再简单地计算表达式 0。零实际上可以是任何整数。没关系。正是这个整数在概念上存储在数组中(无论如何都会被丢弃)。

0,- 数组的第一个元素为零。这迎合了有人在没有参数的情况下打电话给emit()的情况。参数包Ts将为空。如果我们没有这个前导零,数组的结果评估将是int [] { },这是一个零长度数组,这在 c++ 中是非法的。

初学者的进一步说明:

数组的初始化器列表中的所有内容都是表达式

表达式是一系列"算术"运算,它会产生一些对象。该对象可以是实际对象(类实例)、指针、引用或基本类型(如整数)。

因此,在这种情况下,std::cout << x是通过调用std::ostream::operator<<(std::cout, x)(或其自由函数等效项,取决于 x 是什么)计算的表达式。此表达式的返回值始终为std::ostream&

将表达式放在括号中不会改变其含义。它只是强制订购。例如a << b + c的意思是"a向左移动(b加c)",而(a << b) + c的意思是"a向左移动b,然后加上c"。

逗号","也是一个运算符。a(), b()表示"调用函数 A,然后丢弃结果,然后调用函数 B。返回的值应为 b' 返回的值。

因此,通过一些心理体操,您应该能够看到((std::cout << x), 0)的意思是"调用std::ostream::operator<<(std::cout, x),丢弃生成的 ostream 引用,然后计算值 0)。此表达式的结果为 0,但将 x 流式传输到cout的副作用将在我们产生 0' 之前发生。

因此,当Ts是(比如)一个 int 和一个字符串指针时,Ts...将是这样的<int, const char*>ts...的排字表,将被有效地<int(x), const char*("Hello world")>

因此,表达式将扩展为:

void(int[] {
0,
((std::cout << x), 0),
((std::cout << "Hello world"), 0),
});

在婴儿步骤中意味着:

  1. 分配长度为 3 的数组
  2. 数组[0] = 0
  3. 调用 std::cout <<x,丢弃结果,array[1] = 0
  4. 调用 std::cout <<"Hello world",扔掉结果,array[2] = 0

当然,优化器看到这个数组从未被使用过(因为我们没有给它一个名字),所以它删除了所有不必要的位(因为这就是优化器所做的),它变得等效于:

  1. 调用 std::cout <<x,扔掉结果
  2. 调用 std::cout <<"Hello world",扔掉结果

我可以看到您的方法存在两个问题:

  • 正如 The Dark 所提到的,您引用的布尔值可能已超出范围,因为您的main在设置on = false后存在
  • 您应该尝试让编译器知道on的值可能会在循环执行期间更改。如果不这样做,某些编译器优化可能会简单地将 while 替换为无限循环。

一个简单的解决方案是将标志放在全局内存中:

static std::atomic<bool> on = false;
void func()
{
while (on)
/* ... */
} 
int main() 
{
on = true;
/* launch thread */
/* wait */
on = false;
return 0;
}

奖励问题:出了什么问题,为什么它适用于 std::ref,但不适用于普通和

因为std::async以及类似的接口(如std::threadstd::bind按值获取参数。传递引用 by-value 时,引用的对象将作为参数进行复制。使用std::reference_wrapper时,将复制包装器,并且内部的引用保持不变。

如果您想知道为什么它被设计成这样工作,请查看 SO 上的这个答案

正如一个好人西蒙所建议的那样,一种方法是使用 std::ref

#include <cmath>
#include <iostream>
#include <thread>
#include <future>
#include <chrono>
#include <functional>
int func(std::reference_wrapper<bool> on)
{
while(on) {
auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
std::cout << ctime(&t) << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main()
{
bool on = true;
std::future<int> fi = std::async(std::launch::async, func, std::ref(on));
std::this_thread::sleep_for(std::chrono::seconds(5));
on = false;
return 0;
}