线程是并发的一个单位。main()函数可以被视为执行的主线程。在操作系统的上下文中,主线程与其他进程拥有的其他线程并发运行。
std::thread类是STL中并发的基础。所有其他并发特性都建立在线程类的基础之上。
在本示例中,我们将探讨std::thread的基础知识,以及join()和detach()如何确定其执行上下文。
如何做……
在本示例中,我们将创建一些std::thread对象,并试验它们的执行选项。
我们首先编写一个方便函数,用于让线程休眠指定毫秒数:
void sleepms(const unsigned ms) {
using std::chrono::milliseconds;
std::this_thread::sleep_for(milliseconds(ms));
}
sleep_for()函数接受一个duration对象,并使当前线程阻塞指定的持续时间。这个sleepms()函数是一个方便的包装器,它接受一个无符号值作为休眠的毫秒数。
现在,我们需要一个线程函数。这个函数根据一个整数参数休眠可变毫秒数:
void fthread(const int n) {
cout << format("This is t{}\n", n);
for(size_t i{}; i < 5; ++i) {
sleepms(100 * n);
cout << format("t{}: {}\n", n, i + 1);
}
cout << format("Finishing t{}\n", n);
}
fthread()调用sleepms()五次,每次休眠100 * n毫秒。
我们可以在main()中使用std::thread来在新线程中运行这个函数:
int main() {
thread t1(fthread, 1);
cout << "end of main()\n";
}
代码可以编译,但运行时会出错:
terminate called without an active exception
Aborted
(你的错误消息可能会有所不同。这是Debian上使用GCC的错误消息。)
问题在于,当线程对象超出作用域时,操作系统不知道该如何处理这个线程对象。我们必须指定调用者是否等待线程,或者线程是否独立运行(即被分离)。
我们使用join()方法来表示调用者将等待线程完成:
int main() {
thread t1(fthread, 1);
t1.join();
cout << "end of main()\n";
}
输出:
This is t1
t1: 1
t1: 2
t1: 3
t1: 4
t1: 5
Finishing t1
end of main()
现在,main()等待线程完成。
如果我们调用detach()而不是join(),那么main()不会等待,程序在线程运行之前就会结束:
thread t1(fthread, 1);
t1.detach();
输出:
end of main()
当线程被分离时,我们需要给它时间来运行:
thread t1(fthread, 1);
t1.detach();
cout << "main() sleep 2 sec\n";
sleepms(2000);
输出:
main() sleep 2 sec
This is t1
t1: 1
t1: 2
t1: 3
t1: 4
t1: 5
Finishing t1
end of main()
让我们启动并分离第二个线程,看看会发生什么:
int main() {
thread t1(fthread, 1);
thread t2(fthread, 2);
t1.detach();
t2.detach();
cout << "main() sleep 2 sec\n";
sleepms(2000);
cout << "end of main()\n";
}
输出(输出顺序可能会有所不同):
main() sleep 2 sec
This is t1
This is t2
t1: 1
t2: 1
t1: 2
t1: 3
t2: 2
t1: 4
t1: 5
Finishing t1
t2: 3
t2: 4
t2: 5
Finishing t2
end of main()
因为我们的fthread()函数使用其参数作为sleepms()的乘数,所以第二个线程比第一个线程运行得稍微慢一些。我们可以看到输出中的计时器是交错的。
如果我们使用join()而不是detach(),我们会得到类似的结果:
int main() {
thread t1(fthread, 1);
thread t2(fthread, 2);
t1.join();
t2.join();
cout << "end of main()\n";
}
输出(输出顺序可能会有所不同):
This is t1
This is t2
t1: 1
t2: 1
t1: 2
t1: 3
t2: 2
t1: 4
t1: 5
Finishing t1
t2: 3
t2: 4
t2: 5
Finishing t2
end of main()
因为join()等待线程完成,所以我们不再需要在main()中使用2秒的sleepms()来等待线程完成。
它是如何工作的……
一个std::thread对象代表一个执行线程。对象与线程之间存在一对一的关系。一个线程对象代表一个线程,一个线程由一个线程对象表示。线程对象不能被复制或赋值,但可以被移动。
线程构造函数看起来像这样:
explicit thread(Function&& f, Args&&... args);
线程是通过函数指针和零个或多个参数构造的。该函数会立即使用提供的参数进行调用:
thread t1(fthread, 1);
这会创建对象t1,并立即用字面值1作为参数调用函数fthread(int)。
创建线程后,我们必须在线程上使用join()或detach():
t1.join();
join()方法会阻塞调用线程的执行,直到t1线程完成。
t1.detach();
detach()方法允许调用线程独立于t1线程继续执行。
还有更多……
C++20提供了std::jthread,它在作用域结束时自动将调用者加入:
int main() {
std::jthread t1(fthread, 1);
cout << "end of main()\n";
}
输出:
end of main()
This is t1
t1: 1
t1: 2
t1: 3
t1: 4
t1: 5
Finishing t1
这使得t1线程能够独立执行,并在其作用域结束时自动加入main()线程。