文章目录
内容概要:
- 启动线程,并通过几种方式为新线程指定运行代码
- 等待线程完成和分离线程并运行
- 唯一识别线程
1 线程的基本管控
每个c++程序都含有至少一个线程,即运行main()的线程,它由c++运行时(C++ runtime)系统启动。随后,程序就可以发起更多的线程,它们以别的函数作为入口(entry point)。这些新线程连同起始线程并发运行。
当main()返回时,程序就会退出;同样,当入口函数返回时,对应的线程随之终结。我们会看到,如果借std::thread对象管控线程,即可选择等它自然结束。
1.1 发起线程
线程通过构建std::thread对象而启动,该对象指明线程要运行的任务。
#include <thread>
void do_some_work();
std::thread my_thread(do_some_work);
与c++标准库中的许多类型相同,任何可调用类型(callable type)都适用于std::thread。于是,可以设计一个带有函数调用操作符(function call operator)的类,并将该类的实例传递给std::thread的构造函数:
class background_task
{
public:
void operator()() const
{
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f);
构造std::thread实例时,提供了函数对象f作为参数,它被复制到属于新线程的存储空间中,并在那里被调用,由新线程执行。故此,副本的行为必须与原本的函数对象等效,否则运行结果可能有违预期。
将函数对象传递给std::thread的构造函数时,要注意防范所谓的"C++最麻烦的解释"(C++‘s most vexing parse)。如果,传入的是临时变量,而不是具名变量,那么调用构造函数的语法有可能与函数声明相同。遇到这种情况,编译器就会将其解释成函数声明,而不是定义对象。
例如:std::thread my_thread(background_task());
为临时函数对象命名即可解决问题,做法是多用一对圆括号,或采用新式的统一初始化语法(uniform initialization syntax,又名列表初始化),例如:
std::thread my_thread((background_task()));
std::thread my_thread{background_task()};
为了避免这种问题,还能采用lambda表达式。
std::thread my_thread([]{
do_something();
do_something_else();
});
一旦启动了线程,就需要明确是等待它结束,还是由它独自运行。假如std::thread对象销毁之际还没有决定好,那std::thread的析构函数将调用std::terminate()终止整个程序。
如果选择了分离,且分离时新线程还未结束运行,那它将继续运行,甚至在std::thread对象销毁很久之后依然运行,它只有最终从线程函数返回时才会结束运行。那么在线程运行结束前,我们需保证它所访问的外部数据始终正确,有效。例如:
struct func
{
int& i;
func(int& i_):i(i_){}
void operator()()
{
for(unsigned j=0;j<10000000;++j)
{
do_something(i);
}
}
};
void oops()
{
int some_local_state = 0;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.detach();
}
因此,以下做法极不可取:意图在函数中创建线程,并让线程访问函数的局部变量。除非线程肯定会在该函数退出前结束。
处理上述问题的另一种方法是,汇合新线程,此举可以确保在主线程的函数退出前,新线程执行完毕。
1.2 等待线程完成
若需等待线程完成,可调用std::thread实例的成员函数join()实现。
只要调用了join(),隶属与该线程的任何存储空间即会因此消除,std::thread对象遂不再关联到已结束的线程。事实上,它与任何线程均无关联。其中的意义是,对于某个给定的线程,join()仅能调用一次;只能std::thread对象增经调用过join(),线程就不再可汇合(joinable),成员函数joinable()将返回false。
1.3 在出现异常的情况下等待
join()的调用需要小心地选择执行代码的位置,原因是,如果线程启动以后有异常抛出,而join()尚未执行,则该join()调用会被略过。
一般地,若调用join()仅仅是为了处理没有出现异常的情况,那么万一发生异常,我们也仍需调用join(),以避免意外的生存期问题。
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块的使用则保证了新线程在函数f()退出前终结(无论f()是正常退出还是因为异常而退出)。try/catch块稍显冗余,还容易引发作用域轻微错乱。假如代码必须确保新线程先行结束,之后当前线程的函数才退出,那么关键在于,全部可能得退出路径都必须保证这种先后次序,无论是正常退出,还是抛出异常。
为实现此目标,可以运用标准的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()退出,以上行为仍然会发生。
thread_guard的析构函数先调用joinable(),判别std::thread对象是否能汇合,才调用join()。该检查必不可少,因为在任何执行线程(thread of execution)上,join()只能被调用一次,假若线程已经汇合过,那么再此调用join()则是错误行为。
复制构造函数和复制赋值操作符都以"=delete"标记,限令编译器不得自动生成相关代码。复制这类对象或向其赋值均有可能带来问题,因为所产生的新对象的生存期也许更长,甚至超过了与之关联的线程。在销毁原对象和新对象时,分别发生两次析构,将重复调用join()。
若不需要等待线程结束,也可以将其分离,从而防止异常引发的安全问题。分离操作会切断线程和std::thread对象间的关联。这样,std::thread对象在销毁时肯定不调用std::terminate(),即便线程还在后台运行,也不会调用。
1.4 在后台运行线程
调用std::thread对象的成员函数detach(),会令线程在后台运行,遂无法与之直接通信。假若线程被分离,就无法等待它完结,也不可能获得与它关联的std::thread对象,因而无法汇合该线程。然而分离的线程确实仍在后台运行,其归属权和控制权都转移给C++运行时库(runtime library,又名运行库),由此保证,一旦线程退出,与之关联的资源都会被正确回收。
UNIX操作系统重,有些进程叫做守护进程(daemon process),它们在后台运行且没有对外的用户界面;沿袭这一概念,分离出去的线程常常被称为守护线程(deamon thread)。
- 它们在几乎在整个应用程序的生存期内,一直运行,以执行后台任务,如文件系统监控、从对象缓存中清除无用数据项、优化数据结构等。
- 另有一种模式,就是由分离线程执行"启动后即可自主完成"(a fire-and-forget task)的任务;
- 还能通过分离线程实现一套机制,用于确认线程完成运行。
std::thread对象调用成员函数detach()完成后,对象不再关联实际的执行线程,故它变得从此不可汇合。
std::thread t(do_background_work);
t.detach();
assert(!t.joinable());
如果要把std::thread对象和线程分离,就必须存在与其关联的执行线程,这与调用join()的前提条件毫无二致,只有当t.joinable()返回true时,才能调用t.detach()。
考虑一个应用程序,如文字处理软件。为了令它同时编辑多个文件,在用户界面层面和内部层面都有多种实现方式。一种常见的做法是,创建多个独立的顶层窗口,分别与正在编辑的文件逐一对应。相应的内部处理是,每个文 件的编辑窗口都在各自线程内运行;每个线程运行的代码相同,而处理的数据有别,因为这些数据与各文件和对应窗口的属性关联。
按此,打开一个新文件就需启动一个新线程。新线程只处理打开新文件的请求,并不牵涉等待其他线程的运行和结束。对其他线程而言,该文件由新线程负责,与它们无关。综上,运行分离线程就成了首选方案。
void edit_document(std::string const& filename)
{
open_document_and_display_gui(filename);
while(!done_editing())
{
user_command cmd = get_user_input();
if(cmd.type == open_new_document)
{
std::string const new_name = get_filename_from_user();
std::thread t(edit_document,new_name);
t.detach();
}
else
{
process_user_input(cmd);
}
}
}
2 向线程函数传递参数
若需向新线程上的函数或可调用的对象传递参数,方法相当简单,直接向std::thread的构造函数增添更多参数即可。
不过务必牢记,线程具有内部存储空间,参数会按照默认方式先复制到该处,新创建的执行线程才能直接访问它们。然后,这些副本被当成临时变量,以右值形式传给新线程上的函数或可调用对象。即便函数的相关参数按设想应该是引用,上述过程依然会发生。例如:
void f(int i,std::string const& s);
std::thread t(f,3,"hello");
这两行代码借由构造对象t新创建一个执行线程,它们相互关联,并在新线程上调用f(3,"hello")。请注意,尽管函数f()的第二个参数属于std::string类型,但字符串的字面内容仍以指针char const*的形式传入,进入新线程的上下文环境以后,才转换为std::string类型。
2.1 传入自动变量
如果参数是指针,并且指向自动变量(automatic variable),那么这一过程会产生非常重大的影响,举例说明:
自动变量即为代码块内声明或定义的局部变量,位于程序的栈空间。请注意,C++11标准重新引入了auto关键字,但它的语义与此处完全不同。
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会在新线程内转换成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));
t.detach();
}
2.2 传入非const引用
与oops函数相反的情形是,我们真正想要的是非const引用(non-const reference),而整个对象却被完全复制。但这不可能发生,因为编译根本无法通过。可尝试按引用的方式传入一个数据结构,以验证线程能否对其进行更新,例如:
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);
display_status();
t.join();
process_widget_data(data);
}
根据update_data_for_widget()函数的声明,第二个参数会以引用方式传入,可是std::thread的构造函数对此却并不知情,它全然忽略update_data_for_widget()函数所期望的参数类型,直接复制我们提供的值。
然后,线程库的内部代码会把参数的副本(由对象data复制得出,位于新线程的内部存储空间)当成move-only型别(只能移动,不可复制),并以右值的形式传递。
最终,update_data_for_widget()函数调用会收到一个右值作为参数。这段代码会编译失败。因为update_data_for_widget()预期接收非const引用,我们不能向它传递右值。
若知晓std::bind()函数的工作原理(由c++11引入),解决方法就显而易见:若需按引用方式传递参数,只要用std::ref()函数加以包装即可。把创建线程的语句改写成:
std::thread t(update_data_for_widget,w,std:;ref(data));
那么,传入update_data_for_widget()函数的就不是变量data的临时副本,而是指向变量data的引用,代码遂能成功编译。
2.3 传入成员函数
若要将某个类的成员函数设定为线程函数,应传入一个函数指针,指向该成员函数。此外,我们还要给出合适的对象指针,作为该函数的第一个参数:
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个参数,以此类推。
2.4 移动语义与移动传参
C++11还引入了另一种传递参数的方式:参数只能移动但不能复制,即数据从某个对象转移到另一个对象内部,而原对象则被"搬空"。
这种型别的其中一个例子是std::unique_ptr,它为动态分配的对象提供自动化的内存管理。在任何时刻,对与给定的对象,只可能存在唯一一个std::unique_ptr实例指向它;若该实例被销毁,所指对象亦随之被删除。
通过移动构造(move constructor)函数和移动赋值操作符(move assignment operator),对象的归属权就得以在多个std::unique_ptr实例间转移。移动行为令std::unique_ptr的源对象(source object)的值变成NULL指针。函数可以接收这种类型的对象作为参数,也能将它作为返回值,充分发挥其可移动特性,以提升性能。
转移方式:
-
若源对象是临时变量,移动就会自动发生。
-
若源对象是具名变量,则必须通过调用
std::move()直接请求转移。
下面示范std::move()函数的用法,借助它向线程转移动态对象的归属权:
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(precess_big_object,std::move(p));
在调用std::thread的构造函数时,依据std::move(p)所指定的操作,big_object对象的归属权会发生转移,先进入新创建的线程的内部存储空间,再转移给process_big_object()函数。
在c++标准库中,有几类的归属权语义与std::unique_ptr一样,std::thread类就是其中之一。std::thread类的实例并不拥有动态对象(这与std::unique_ptr不同),但它们拥有另一种资源:每份实例都负责管控一个执行线程。因为std::thread类的实例能够转移(movable)却不能复制(not copyable),故此线程的归属权可以在实例之间转移。这就保证了,对于任一特定的执行线程,任何时候都只有唯一的std::thread对象与之关联,还准许程序员在其对象之间转移线程归属权。
3 移交线程归属权
使用场景:
- 编写函数,功能是创建线程,并置于后台运行,但该函数本身并不等待线程完结,而是将其归属权向上移交给函数的调用者
- 或相反地,创建线程,将归属权传入某个函数,由它负责等待线程该线程结束。
对于一个具体的执行线程,其归属权可以在几个std::thread实例间转移,如下代码,创建2个执行线程和3个std::thread实例t1,t2,t3,并将归属权在实例之间多次转移。
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);
首先,启动新线程,并使之关联t1。接着,构建t2,在其初始化过程中调用std::move(),将新线程的归属权显示地转移给t2。在转移之前,t1关联着执行线程,some_function()函数在其上运行;及至转移时,线程关联的变换为t2。
然后,启动另一新线程,它与一个std::thread类型的临时对象关联。新线程的归属权随机转移给t1。这里无须显示调用std::move(),因为新线程本来就由临时变量持有,而源自临时变量的移动操作会自动地隐式进行。
t3按默认方式构造,换言之,在创建时,它并未关联任何执行线程。然后,t2原本关联的线程的归属权会转移给t3,而t2是具名变量,故需再次显示调用std::move(),先将其转换为右值。经过这些转移,t1与运行some_other_function()的线程关联,t2没有关联线程,而t3与运行some_function的线程关联。
在最后一次转移中,运行some_function()的线程的归属权转移到t1,该线程最初由t1启动。但在转移之时,t1已经关联运行some_other_function()的线程。因此std::terminate()会被调用,终止整个程序。
只要std::thread对象正管控着一个线程,就不能简单地向它赋新值,否则该线程会因此被遗弃。
3.1 转移线程归属权示例
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;
}
类似地,若归属权可以转移到函数内部,函数就能够接受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));
}
3.2 scoped_thread
scoped_thread:它的首要目标,是在离开其对象所在的作用域前,确保线程已经完结。
class scoped_thread
{
std::thread t;
public:
explicit scoped_thread(std::thread t_): ⑴
t(std::move(t_))
{
if(!t.joinable()) ⑵
throw std::logic_error("No thread");
}
~scoped_thread()
{
t.join(); ⑶
}
scoped_thread(scoped_thread const&)=delete;
scoped_thread& operator=(scoped_thread const&) =delete;
};
struct func; ⑹
void f()
{
int some_local_state;
scoped_thread t{std::thread(func(some_local_state)))}; ⑷
do_something_in_current_thread();
} ⑸
代码中直接向scoped_thread的构造函数传入新线程(4),而非单独的具名变量。当起始线程运行至f()末尾时(5),对象t就会被销毁,与t关联的线程随即和起始线程汇合(3),该关联线程与(4)处发起,传递给scoped_thread的构造函数(1),构建出对象t。
3.3 生成多个线程
std::thread支持移动语义,所以只要容器同样知悉移动意图1,就可以装载std::thread对象。
void do_work(unsigned id);
void f()
{
std::vector<std::thread> threads;
for(unsigned i = 0;i<20;++i)
{
threads.emplace_back(do_work,i); //生成线程
}
for(auto& entry:threads) //依次在各线程上调用join()
entry.join();
}
若要运行多线程切分某算法的运算任务,往往采用以上方式;必须等待所有线程完成运行后,运行流程才能返回到调用者。上述代码结构简单,说明了每个线程上的任务都是自含2的,且它们操作共享数据的结果单纯地由副作用3产生。
- 即 move-aware,指容器能够把元素移动或复制到其内部,特别是可以正确处理只移动对象,并且能在容器内部进行移动操作。
- 即 self-contained,指线程内数据齐全,可独立完成子任务,而不依赖外部数据。
- 这里的"副作用"是C++标准中规定的特定术语,意思是执行状态的改变,例如访问volatile变量,修改对象,进行I/O访问,以及调用某个造成副作用的函数,等等,这些操作都有可能影响其它线程的状态。
4 在运行时选择线程数量
std::thread::hardware_concurrency()函数,它的返回值是一个指标,表示程序在各次运行中可真正并发的线程数量。例如,在多核系统上,该值可能就是CPU的核心数量。若信息无法获取,该函数则可能返回0。
并发函数,一般将工作分派给各线程,并设置一个限定量,每个线程所负责的元素数量都不低于该限定量,从而防止创建的线程过多,造成额外的开销。
如下代码,是并行版的std::accumulate()的简单实现,本例意在说明基本思路。在实际代码中,很可能直接使用并行版的std::reduce()函数,而不会亲自手动实现。
请注意,以下代码是假设没有任何操作抛出异常,但在实际环境中可能抛出异常。
tempalte<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] = 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); ⑾
}
实例讲解:
-
参与计算的元素位于目标区间内,范围由参数first和last指定。
-
假如传入的区间为空
(1),就直接返回初始值,该值由参数init给出。 -
元素总量除以每个线程处理元素的最低限定量,得出线程的最大数量
(2)。这是为了预防不合理地发起过多线程。 -
对比算出的最小线程数量和硬件线程数量,较小者即为实际需要运行的线程数量
(3)。 -
硬件支持的线程数量有限,运行的线程数量不应超出该限度(超出的情况称为线程过饱和,即
oversubscription),因为线程越多,上下文切换越频繁,导致性能降低。不能运行太多线程,否则,若代码在单核机器上运行,就会令所有任务变慢;但我们同样不想线程数量过少,因为那样无从发挥可用的并发性能。
-
将目标区间长度除以线程数量,得出各线程需分担的元素数量
(4)。 -
创建
std::vector<T>容器存放中间结果,创建std::vector<std::thread>容器装载线程。发起的线程数量比所求
num_threads少一个,因为主线程本身也进行运算。 -
通过简单的循环,整个目标区间在形式上被切成多个小块,针对每个小块逐一创建线程:每轮循环中,迭代器
block_end前移至当前小块的结尾(6),新线程就此启动,计算该小块的累加结果(7)。下一小块的起始位置即为本小块的末端(8)。发起全部线程后,主线程处理最后一个小块
(9);无论最后的小块含有多少元素,迭代器last必然指向其结尾。 -
最后小块的累加一旦完成,我们就在一个循环里等待前面所生成的线程
(10);最后,调用std::accumulate()函数累加中间结果(11)。
本示例操作局限:
-
某些类型T的加法操作不满足结合律(如float或double类型)。
指精度损失,考虑数量级差别巨大的两个值,以4.5678和9.0e-123为例,它们相加之和的理论值应该是4.5678 + 9.0e-123;若C++的内建double类型进行运算,其小数部分的最末端,因精度有限而被截断丢弃,实际结果为4.5678,导致产生误差。
-
对迭代器有略微有要求,它们至少必须是前向迭代器(
forward iterator),而std::accumlate()函数可以接受单程输入迭代器(single-pass input iterator)。 -
为了创建名为
results的vector容器来存放中间结果,类型T必须支持默认构造(default-constructible)。
5 识别线程
线程ID所属型别是std::thread::id,它有两种获取方法:
- 在与线程关联的
std::thread对象上调用成员函数get_id(),即可得到该线程的ID。如果std::thread对象没有关联任何执行线程,调用get_id()则会返回一个std::thread::id对象,它按默认构造方式生成,表示"线程不存在"。 - 也可以通过调用
std::this_thread::get_id()获得,函数定义位于头文件<thread>内。
std::thread::id型别的对象作为线程ID,可随意进行复制操作或比较运算;否则,它们就没有什么大的用处。如果两个std::thread::id型别的对象:
- 相等,则它们表示相同的线程,或者它们的值都表示"线程不存在";
- 不相等,则它们就表示不同的线程,或者当中一个表示某个线程,而另一个表示"线程不存在"。
C++标准库容许我们随意判断两个线程ID是否相同,没有任何限制;std::thread::id型别具备全套完整的比较运算符,比较运算符就所有不相等的值确立了全序(total order)关系。
全序关系,简称全序,又名线性序、简单序或非严格排序,是在集合上的反对称的、传递的和完全的任何二元关系。全序关系在集合中的元素之间定义了一个排序,使得任何两个元素都可以进行比较。
全序关系可以用符号“≤”表示,对于集合中的任何元素a、b和c,全序关系满足以下性质:
- 自反性:对于集合中的任意元素a,有a≤a。
- 反对称性:如果a≤b且b≤a,则a=b。
- 传递性:如果a≤b且b≤c,则a≤c。
在全序集合中,任意两个元素都可以进行比较。因此,全序关系是一种特殊的偏序关系,其中每个元素都是可比较的。在全序关系中,任何两个元素要么是可比较的(即一个元素小于等于另一个元素),要么是相同的(即相等)。
这使得它们可以用作关联容器(associative container)的键值,或用于排序;标准库的hash模板能够具体化成std::hash<std::thread::id>,因此std::thread::id的值也可以用作新标准的无序关联容器(unordered associative container)的键值。
std::thread::id实例常用于识别线程,以判断它是否需要执行某项操作。一般主线程负责发起其他线程,它可能需要承担稍微不一样的工作;若需识别主线程,可以在发起其他线程之前,在主线程中预先保存std::this_thread::get_id()的结果;算法的核心部分对全体线程毫无区别,由它们共同运行,只要对比各线程预存的值和自身的线程ID,就能作出判断:
std::thread::id master_thread;
void some_core_part_of_algorithm()
{
if(std::this_thread::get_id() == master_thread)
{
do_master_thread_work();
}
do_common_work();
}
std::thread::id的实例写到输出流,如std::cout:
std::cout << std::this_thread::get_id();
C++标准只是保证了比较运算中相等的线程ID应产生一样的输出,而不相等的线程ID则产生不同的输出。这个特性的主要用途是调试除错和记录日志。
6 小结
本章中,讲解了C++标准库
- 如何进行基本的线程管控:启动线程,等待它们完成,或让它们在后台运行而不做等待;
- 如何在启动时向线程函数传递参数;
- 如何让代码的不同部分相互转移管控线程的权力;
- 如何把线程集结成组,以分配工作;
- 如何识别线程,其目的在于,万一其他方法都不够方便,也能为某些特定的线程关联数据,或定制其行为。

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



