目录
3.1、可以使用try/catch 防止因抛出异常而导致的应用程序终结。
3.2、 用标准的RAII手法,在其析构函数中调用join()
一、创建线程
在C++标准库中,线程通过构建std::thread对象而启动,该对象指明线程要运行的任务。线程管控需要包含头文件<thread>。与 C++标准库中的许多类型相同,任何可调用类型(callable type)都适用于std::thread。
1.1 以普通函数为参数创建线程
void do_some_work();
std::thread my_thread(do_some_work);
1.2 以函数对象为参数创建线程
class background_task
{
public:
void operator()() const
{
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f);
1.3 如何避免创建线程被解释为函数声明
例如
std::threadmy_thread(background_task());//本意是发起新线程,却被编译器解释成函数声明
为解决上述问题有以下几种方案:
1.3.1 加入额外的圆括号,以防语句被解释成函数声明
std::thread my_thread((background_task()));
1.3.2 使用列表初始化
std::thread my_thread{background_task()};
1.3.3 使用lambda表达式
std::thread my_thread([]{
do_something();
do_something_else();
});
二、分离线程在后台运行线程
调用std::thread对象的成员函数detach(),会令线程在后台运行,遂无法与之直接通信。UNIX操作系统中,有些进程叫作守护进程(daemon process),它们在后台运行且没有对外的用户界面;沿袭这一概念,分离出去的线程常常被称为守护线程(daemon thread)。
若要分离线程,则需在std::thread对象上调用其成员函数detach()。调用完成后,std::thread对象不再关联实际的执行线程,故它变得从此不可汇合。
std::thread t(do_background_work);
t.detach();
assert(!t.joinable());
三、等待线程完成
若需等待线程完成,那么可以在与之关联的std::thread实例上,通过调用成员函数join()实现。对于某个给定的线程,join()仅能调用一次;只要std::thread对象曾经调用过join(),线程就不再可汇合(joinable),成员函数joinable()将返回false。
- 在出现异常的情况下等待
在std::thread对象被销毁前,我们需确保已经调用join()或detach()。如果线程启动以后有异常抛出,而join()尚未执行,则该join()调用会被略过。
3.1、可以使用try/catch 防止因抛出异常而导致的应用程序终结。
struct func;
void f()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread t(my_func);
try
{
do_something_in_current_thread();
}
catch(...)
{
t.join();
throw;
}
t.join();
}
try/catch块稍显冗余,还容易引发作用域的轻微错乱,故它并非理想方案。
3.2、 用标准的RAII手法,在其析构函数中调用join()
class thread_guard
{
std::thread& t;
public:
explicit thread_guard(std::thread& t_):
t(t_)
{}
~thread_guard()
{
if(t.joinable())
{
t.join();
}
}
thread_guard(thread_guard const&)=delete;
thread_guard& operator=(thread_guard const&)=delete;
};
struct func;
void f()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread t(my_func);
thread_guard g(t);
do_something_in_current_thread();
}
当主线程执行到f()末尾时,按构建的逆序,全体局部对象都会被销毁。因此类型thread_guard的对象g首先被销毁,在其析构函数中,新线程汇合。即便do_something_in_current_thread()抛出异常,函数f()退出,以上行为仍然会发生。
函数抛出异常,运行时系统接管,搜寻匹配的 catch 块过程中,若当前函数没有匹配的catch块,则会释放当前函数内创建的局部对象(调用其析构函数),然逆着调用链向上回溯到上一层(stack unwinding),直到找到匹配的catch块,将控制权转交给它。
若一直回溯到最顶层的main函数或线程的entry point函数仍是uncatch,则std::terminate()被调用,直接终止程序。
RAII(将资源封装在对象内,对象销毁时,资源也被释放)和 stack unwinding 可以保证即使在出现异常的情况下,资源也能被正确释放,不出现资源泄露。
复制构造函数和复制赋值操作符都以“=delete”标记,限令编译器不得自动生成相关代码。复制这类对象或向其赋值均有可能带来问题,因为所产生的新对象的生存期也许更长,甚至超过了与之关联的线程。
四、向线程函数传递参数
4.1 普通参数传递
直接向std::thread的构造函数增添更多参数即可,例如:
void f(int i,std::string const& s);
std::thread t(f,3,"hello");
线程具有内部存储空间,参数会按照默认方式先复制到该处,新创建的执行线程才能直接访问它们。然后,这些副本被当成临时变量,以右值形式传给新线程上的函数或可调用对象。
请注意,尽管函数f()的第二个参数属于std::string类型,但字符串的字面内容仍以指针char const*的形式传入,进入新线程的上下文环境以后,才转换为std::string类型
4.2 传递指针
如果给新线程传递的参数是指针,而新线程在使用指针的数据之前该指针就可能已经销毁,就会引发悬空指针问题。
例如:
void f(int i,std::string const& s);
void oops(int some_param)
{
char buffer[1024];
sprintf(buffer, "%i",some_param);
std::thread t(f,3,buffer);
t.detach();
}
向新线程传递的是指针buffer(数组在传参时退化为指针),指向一个局部数组变量buffer。我们原本设想,buffer会在新线程内转换成std::string对象,但在此完成之前,oops()函数极有可能已经退出,导致局部数组被销毁而引发未定义行为。这一问题的根源在于:我们本来希望将指针buffer隐式转换成std::string对象,再将其用作函数参数,可惜转换未能及时发生,原因是std::thread的构造函数原样复制所提供的值,并未令其转换为预期的参数类型。解决方法是,在buffer传入std::thread的构造函数之前,就把它转化成std::string对象。
void f(int i,std::string const& s);
void not_oops(int some_param)
{
char buffer[1024];
sprintf(buffer,"%i",some_param);
std::thread t(f,3,std::string(buffer)); // 使用std::string避免悬空指针
t.detach();
}
4.3 传递引用
线程库的内部代码会直接复制参数的值到线程内部的存储空间,并把参数的副本当成move-only型别(只能移动,不可复制),并以右值的形式传递。这就导致如果要直接传递引用的话,线程函数的参数只能是const引用(非const引用不能接受右值传递)。
例如下例代码,会编译失败。
void update_data_for_widget(widget_id w,widget_data& data); //
void oops_again(widget_id w)
{
widget_data data;
std::thread t(update_data_for_widget,w,data); // 预期接受非const引用,编译失败
display_status();
t.join();
process_widget_data(data);
}
解决方法就显而易见:若需按引用方式传递参数,只要用std::ref()函数加以包装即可。把创建线程的语句改写成:
std::thread t(update_data_for_widget,w,std::ref(data));
那么,传入update_data_for_widget()函数的就不是变量data的临时副本,而是指向变量data的引用,代码遂能成功编译。但是与传递指针相同,同样会遇到一个变量在两个线程中的生存期可能不同的问题。
4.4 类成员函数做线程入口函数如何传参
若要将某个类的成员函数设定为线程函数,我们则应传入一个函数指针,指向该成员函数。此外,我们还要给出合适的对象指针,作为该函数的第一个参数。例如:
class X
{
public:
void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work,&my_x);
上述对象指针由my_x的地址充当,这段代码将它传递给std::thread的构造函数,因此新线程会调用my_x.do_lengthy_work()。我们还能为成员函数提供参数:若给std:: thread的构造函数增添第3个参数,则它会传入成员函数作为第1个参数,以此类推.
4.5 传递unique_ptr
这是一种值得提倡的参数传递方式,不仅利用unique_ptr的移动语义节省了复制开销,也解决了变量的归属权和生存期问题。例如:
void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object,std::move(p));
在调用std::thread的构造函数时,依据std::move(p)所指定的操作,big_object对象的归属权会发生转移,先进入新创建的线程的内部存储空间,再转移给process_big_object()函数。
五、移交线程归属权
虽然std::thread类的实例并不拥有动态对象(这与std::unique_ptr不同),但它们拥有另一种资源:每份实例都负责管控一个执行线程。因为std::thread类的实例能够移动(movable)却不能复制(not copyable),故此线程的归属权可以在其实例之间转移。这就保证了,对于任一特定的执行线程,任何时候都只有唯一的std:::thread对象与之关联,还准许程序员在其对象之间转移线程归属权。
5.1 将线程归属权在实例之间多次转移
void some_function();
void some_other_function();
std::thread t1(some_function);
std::thread t2=std::move(t1);
t1=std::thread(some_other_function);
std::thread t3;
t3=std::move(t2);
t1=std::move(t3); //该赋值操作会终止整个程序
在最后一次转移中,运行some_function()的线程的归属权转移到t1,该线程最初由t1启动。但在转移之时,t1已经关联运行some_other_function()的线程。因此std::terminate()会被调用,终止整个程序。该调用在std::thread的析构函数中发生,目的是保持一致性.
5.2 std::thread做函数返回值
std::thread支持移动操作的意义是,函数可以便捷地向外部转移线程的归属权,
std::thread f()
{
void some_function();
return std::thread(some_function);
}
std::thread g()
{
void some_other_function(int);
std::thread t(some_other_function,42);
return t;
}
5.3 std::thread作为函数参数
若归属权可以转移到函数内部,函数就能够接收std::thread实例作为按右值传递的参数,如下所示:
void f(std::thread t);
void g()
{
void some_function();
f(std::thread(some_function));
std::thread t(some_function);
f(std::move(t));
}
5.4 std::jthread
jthread是c++20所支持的新的线程类型,jthread = joinable thread, 即可以自动join的线程。
c++11中thread对象如果在销毁之前处于可join的状态,却没有join的话,将会引发一个异常.另外,td::thread的析构时执行join有时不仅可能导致程序表现异常,还可能导致程序挂起。解决方案是此类程序应该和异步执行的lambda通信,告诉它不需要执行了,可以直接返回,但是C++11中不支持可中断线程(interruptible threads)。
jthread的由来就很好理解了,jthread除了拥有std::thread 的行为外,主要增加了以下两个功能:
- jthread 对象被析构时,会自动调用join,等待其所表示的执行流结束。
- jthread支持外部请求中止(通过 get_stop_source、get_stop_token 和 request_stop )。
5.4.1 jthread的基础使用
在jthread对象销毁时没有join也不会引发异常
#include <iostream>
#include <thread>
int main(){
std::cout << std::endl;
std::cout << std::boolalpha;
std::jthread thr{[]{ std::cout << "Joinable std::thread" << std::endl; }};
std::cout << "thr.joinable(): " << thr.joinable() << std::endl;
std::cout << std::endl;
}
5.4.2 jthread的复杂应用
使用request_stop向线程发出停止运行的请求
#include <thread>
#include <iostream>
using namespace std::chrono_literals;
void test_jthread01() {
std::jthread jt{ [](std::stop_token stoken) {
while (!stoken.stop_requested()) {
std::cout << "Doing work\n";
std::this_thread::sleep_for(1s);
}
}};
sleep(5);
jt.request_stop(); // 请求线程停止,因有响应停止请求而终止线程
jt.join();
}
int main()
{
test_jthread01();
}
注意lambda函数的入参中需要添加std::stop_token类型的入参, 并且需要在线程中添加检查stop_token的代码
使用ThreadRAII来保证在std::thread的析构时执行join有时不仅可能导致程序表现异常,还可能导致程序挂起。
jthread同样可以解决上述问题,jthread对象在析构时会调用request_stop,jthread的析构函数伪代码可能是下面这样的:
~jthread() {
if(joinable()) {
request_stop(); //More on stop request below.
join();
}
}
而在线程的内部可以使用std::condition_variable_any去进行配合。condition_variable_any和一样条件变量不同的是, 其wait时会接受一个stop_token,当收到request_stop时,wait会直接返回,不再进行等待。下面是condition_variable_any进行wait时的伪代码:
template<class Lock, class Predicate>
bool wait(Lock& lock, std::stop_token stoken, Predicate stop_waiting){
while (!stoken.stop_requested()) {
if (stop_waiting()) return true;
wait(lock);
}
return stop_waiting();
}
结合jthread和condition_variable_any就可以很好的解决上述问题。如果发出了request_stop,那么condition_variable_any类型的条件变量将会唤醒。下面是一个使用jthread和condition_variable_any的完整例子,jthread对象析构时,不再会陷入无穷的等待中。
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <condition_variable>
using namespace std::chrono_literals;
std::condition_variable_any cond;
int main()
{
std::jthread waiting_worker([](std::stop_token stoken) {
std::mutex mutex;
std::unique_lock lock(mutex);
std::cout << "wait" << std::endl;
cond.wait(lock, stoken,
[] { return false; });
if (stoken.stop_requested()) {
std::cout << "Waiting worker is requested to stop\n";
return;
}
});
std::this_thread::sleep_for(100s);
std::cout << "destroy jthread object, and call request_stop" << std::endl;
// Or automatically using RAII:
// waiting_worker's destructor will call request_stop()
// and join the thread automatically.
}
六、运行时选择线程数量
C++标准库的std::thread::hardware_concurrency()函数,在多核系统上,该值可能就是CPU的核芯数量。下列代码是并行版的std::accumulate()的简单实现。下列代码将工作分派给各线程,并设置一个限定量,每个线程所负责的元素数量都不低于该限定量,从而防止创建的线程过多,造成额外开销。
template<typename Iterator,typename T>
struct accumulate_block
{
void operator()(Iterator first,Iterator last,T& result)
{
result=std::accumulate(first,last,result);
}
};
template<typename Iterator,typename T>
T parallel_accumulate(Iterator first,Iterator last,T init)
{
unsigned long const length=std::distance(first,last);
if(!length)
return init; ⇽--- ①
unsigned long const min_per_thread=25;
unsigned long const max_threads=
(length+min_per_thread-1)/min_per_thread; ⇽--- ②
unsigned long const hardware_threads=
std::thread::hardware_concurrency();
unsigned long const num_threads=
std::min(hardware_threads!=0?hardware_threads:2,max_threads); ⇽--- ③
unsigned long const block_size=length/num_threads; ⇽--- ④
std::vector<T> results(num_threads);
std::vector<std::thread> threads(num_threads-1); ⇽--- ⑤
Iterator block_start=first;
for(unsigned long i=0;i<(num_threads-1);++i)
{
Iterator block_end=block_start;
std::advance(block_end,block_size); ⇽--- ⑥
threads[i]=std::thread( ⇽--- ⑦
accumulate_block<Iterator,T>(),
block_start,block_end,std::ref(results[i]));
block_start=block_end; ⇽--- ⑧
}
accumulate_block<Iterator,T>()(
block_start,last,results[num_threads-1]); ⇽--- ⑨
for(auto& entry: threads)
entry.join(); ⇽--- ⑩
return std::accumulate(results.begin(),results.end(),init); ⇽--- ⑪
}
接下来给出代码解释
- 假如传入的区间为空①,就直接返回初始值,该值由参数init给出。这样区间至少含有一个元素
- 我们将元素总量除以每个线程处理元素的最低限定量,得出线程的最大数量②
- 对比算出的最小线程数量和硬件线程数量,较小者即为实际需要运行的线程数量③
- 将目标区间的长度除以线程数量,得出各线程需分担的元素数量④
- 需要发起的线程数量比前面所求的num_threads少一个⑤,因为主线程本身就算一个(parallel_accumulate()函数在其上运行)。
- 每轮循环中,迭代器block_end前移至当前小块的结尾⑥,新线程就此启动,计算该小块的累加结果⑦。下一小块的起始位置即为本小块的末端⑧。
- 发起全部线程后,主线程随之处理最后一个小块⑨。我们知道,无论最后的小块含有多少元素,迭代器last必然指向其结尾,这正好解决了上述无法整除的问题。
- 最后小块的累加一旦完成,我们就在一个循环里等待前面所有生成的线程⑩
- 调用std::accumulate()函数累加中间结果⑪
七、线程ID
7.1 线程ID的获取方式
线程ID所属型别是std::thread::id,实例常用于识别线程,以判断它是否需要执行某项操作,它有两种获取方法。
- 首先,在与线程关联的std::thread对象上调用成员函数get_id(),即可得到该线程的ID。
- 当前线程的ID可以通过调用std::this_thread::get_id()获得,函数定义位于头文件<thread>内。
7.2 线程ID的作用
- 用作关联容器(associative container)的键值,或用于排序。
- 也可以用作新标准的无序关联容器(unordered associative container)的键值。
- 令数据结构聚合std::thread::id型别的成员,以保存当前线程的ID,作为操作的要素以控制权限。凡涉及该数据结构的后续操作,都要对比执行线程的ID与预存的ID,从而判断该项操作是否得到了许可,或是否按要求进行。
2014

被折叠的 条评论
为什么被折叠?



