基于C++11的多线程编程系列文章目录

基于C++11的多线程编程系列文章目录


前言

很长时间一直忙着投标写材料了,编程写代码已经生疏了。最近由于要干一些文档索引的事情,于是又开始督促自己,写点代码,至少让工作更有效率,或者不那么蠢!最近看了看多线程,C++的多线程编程由于C++11的出现,据说更稳定了,至少比之前windows API 的多线程效率更高,更稳定(也不知道谁说的,有没有科学根据),所以以下就用C++11进行多线程学习吧。参考的书用的是《C++ Concurrency In Action》大家可以自己去找,有英文的有中文的,都挺好。

一、入门从Hello world开始

1.运行环境

  • IDE工具:windows 下 visual studio 2019,C++ windows 控制台

我们还是从那个经典的例子开始学习。
编写一个打印“Hello World”的程序。作为从简到多线程的基础,先看看运行在单线程上的简单程序代码:

#include <iostream>
int main()
{
std::cout<<"Hello World\n";
}

此程序用于将“Hello world”写入给标准输出流当中。让我们将其与下面程序清单中所示的简单Hello Concurrent World程序进行比较。

2.多线程程序代码

#include <iostream>
#include <thread>//①
void hello()
{
    std::cout << "Hello Concurrent world!\n";//②
}
int main()
{
    std::thread t(hello);//③
    t.join();//④
    }

开始找单线程和多线程编程的不同:

  1. 第一个不同: 包含头文件 #include< thread>。 标准c++库中,对多线程支持的声明放在此新的头文件(< thread>)中。用于管理线程的函数和类在< thread>中进行声明,而用于保护共享数据的函数和类则在其他头文件中进行声明。
  2. 其次,写此消息“hello world”的代码被移到了一个单独的函数中。这是因为每个线程都必须有一个初始函数,新线程从这里开始执行。对于应用程序中的初始线程,也就是main(),但对于每一个其他线程(非初始线程),它是在std::thread对象的构造函数中指定的——在本例中,名为t的std::thread对象有新的hello()函数作为其初始函数。
    3.另一个区别就是:此程序不是象单线程程序那样直接向标准输出写入信息或从main()调用hello()函数,而是启动一个新线程来执行此操作,这使得线程总数数达到2,包括了从main()开始的初始线程和从hello()开始的新线程。
    新线程启动后,初始线程也会同时继续执行。如果初始线程没有等待新线程完成,而继续执行到main()的末尾并结束程序,这很可能使得在新线程运行之前,初始线程就已经结束了,从而使得新线程失去了运行的机会。这也就是为什么要在这里调用jion()函数(在第二章详细介绍),此函数就是让调用线程(初始线程,将jion()函数放在main()函数内)等待与std::thread线程对象(本例中,就是t对象)关联的线程(新线程)执行完毕。

二、线程管理的基础

1.启动线程

线程在 std::thread 对象创建(为线程指定任务)时启动;
最简单的情况下, 任务也会很简单, 通常是无参数无返回(void-returning)的函数。 这种函数在其所属线程上运行, 直到函数执行完毕, 线程也就结束了。
总之, 使用C++线程库启动线程, 可以归结为构造 std::thread 对象

void do_some_work();//创建函数do_some_work
std::thread my_thread(do_some_work);//创建线程对象my_thread
  • std::thread 可以用可调用( callable) 类型构造, 将带有函数调用符类型的实例传入 std::thread 类中, 替换默认的构造函数。
class background_task
{ 
public:
  void operator()() const
  {
    do_something();
    do_something_else();
  }
};
background_task f;
std::thread my_thread(f);

代码中, 提供的函数对象会复制到新线程的存储空间当中, 函数对象的执行和调用都在线程
的内存空间中进行。 函数对象的副本应与原始函数对象保持一致, 否则得到的结果会与我们
的期望不同。

  • 有件事需要注意, 当把函数对象传入到线程构造函数中时, 需要避免“最令人头痛的语法解析”(C++’s most vexing parse, 中文简介)。 如果你传递了一个临时变量, 而不是一个命名的变量; C++编译器会将其解析为函数声明, 而不是类型对象的定义。
    例如:
std::thread my_thread(background_task());

这里相当与声明了一个名为my_thread的函数, 这个函数带有一个参数(函数指针指向没有参
数并返回background_task对象的函数), 返回一个 std::thread 对象的函数, 而非启动了一个线程。使用在前面命名函数对象的方式, 或使用多组括号①, 或使用新统一的初始化语法②, 可以避免这个问题。

std::thread my_thread((background_task())); // 1
std::thread my_thread{background_task()}; // 2

2.等待线程完成

  • 如果需要等待线程, 相关的 std::tread 实例需要使用join()。

join()是简单粗暴的等待线程完成或不等待。 当你需要对等待中的线程有更灵活的控制时, 比如, 看一下某个线程是否结束, 或者只等待一段时间(超过时间就判定为超时), 想要做到这些, 你需要使用其他机制来完成, 比如条件变量和期待(futures)。
调用join()的行为, 还清理了线程相关的存储部分, 这样 std::thread 对象将不再与已经完成的线程有任何关联。 这意味着, 只能对一个线程使用一次join();一旦已经使用过join(), std::thread 对象就不能再次加入了, 当对其使用joinable()时, 将返回否( false) 。

3.特殊情况下的等待

如前所述, 需要对一个还未销毁的 std::thread 对象使用join()或detach()。 如果想要分离一个线程, 可以在线程启动后, 直接使用detach()进行分离。 如果打算等待对应线程, 则需要细心挑选调用join()的位置。 当线程运行之后产生了异常, 而异常在join()调用之前就被抛出, 就意味着很这次join()调用会被跳过,从而导致线程(没有进行jion()操作)发生不期待的异常。
为避免应用被抛出的异常所终止, 就需要这样操作:

  • 在无异常的情况下使用join()时, 需要在异常处理过程中调用join(), 从而避免生命周期的问题。
struct func; // 定义在清单2.1中
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(); // 1
	throw;
} 
t.join(); // 2
}

代码使用了 try/catch 块确保访问本地状态的线程退出后, 函数才结束。 当函数正常退出时, 会执行到②处; 当函数执行过程中抛出异常, 程序会执行到①处。
如需确保线程在函数(结束)之前结束,而后再确定一下程序可能会退出的途径, 无论正常与否, 可以提供一个简洁的机制, 来做解决这个问题。

  • 一种方式是使用“资源获取即初始化方式”(RAII, Resource Acquisition Is Initialization), 并且提供一个类, 在析构函数中使用join():
class thread_guard
{
	std::thread& t;
public:
	explicit thread_guard(std::thread& t_):
	t(t_)
	{}
	~thread_guard()
	{
		if(t.joinable()) // 1
		{
		t.join(); // 2
		}
	}
	thread_guard(thread_guard const&)=delete; // 3
	thread_guard& operator=(thread_guard const&)=delete;
};
struct func; // 定义在清单2.1中
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();
} // 4

当线程执行到④处时, 局部对象就要被逆序销毁了。 因此, thread_guard对象g是第一个被销毁的, 这时线程在析构函数中被加入②到原始线程中。 即使do_something_in_current_thread抛出一个异常, 这个销毁依旧会发生。
在thread_guard的析构函数的测试中, 首先判断线程是否已加入①, 如果没有,会调用join()②进行加入。 这很重要, 因为join()只能对给定的对象调用一次, 所以对给已加入的线程再次进行加入操作时, 将会导致错误。
拷贝构造函数和拷贝赋值操作被标记为 =delete ③, 是为了不让编译器自动生成它们。 直接对一个对象进行拷贝或赋值是危险的, 因为这可能会弄丢已经加入的线程。 通过删除声明,任何尝试给thread_guard 对象赋值的操作都会引发一个编译错误。
如果不想等待线程结束, 可以分离(detaching)线程, 从而避免异常安全(exception-safety)问题。 不过, 这就打破了线程与 std::thread 对象的联系, 即使线程仍然在后台运行着, 分离操作也能确保 std::terminate() 在 std::thread 对象销毁才被调用。

4.后台运行线程

使用detach()会让线程在后台运行, 这就意味着主线程不能与之产生直接交互。 也就是说,主线程不会等待这个线程结束; 如果线程分离, 那么就不可能有 std::thread 对象能引用它, 分离线程的确在后台运行, 所以分离线程不能被加入。 不过C++运行库保证, 当线程退出时, 相关资源的能够正确回收, 后台线程的归属和控制C++运行库都会处理。

通常称分离线程为守护线程(daemon threads),UNIX中守护线程是指, 且没有任何用户接口,并在后台运行的线程。 这种线程的特点就是长时间运行; 线程的生命周期可能会从某一个应用起始到结束, 可能会在后台监视文件系统, 还有可能对缓存进行清理, 亦或对数据结构进行优化。分离线程只能确定线程什么时候结束

为了从 std::thread 对象中分离线程(前提是有可进行分离的线程):不能对没有执行线程的 std::thread 对象使用detach(),这也是join()的使用条件, 并且要用同样的方式进行检查——当 std::thread 对象使用t.joinable() 返回的是true, 就可以使用t.detach().
试想如何能让一个文字处理应用同时编辑多个文档。 无论是用户界面, 还是在内部应用内部进行, 都有很多的解决方法。 虽然, 这些窗口看起来是完全独立的, 每个窗口都有自己独立的菜单选项, 但他们却运行在同一个应用实例中。 一种内部处理方式是, 让每个文档处理窗口拥有自己的线程; 每个线程运行同样的的代码, 并隔离不同窗口处理的数据。 如此这般,打开一个文档就要启动一个新线程。 因为是对独立的文档进行操作, 所以没有必要等待其他线程完成。 因此, 这里就可以让文档处理窗口运行在分离的线程上。

清单2.4 使用分离线程去处理其他文档:

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); // 1
			t.detach(); // 2
		} 
		else
		{
			process_user_input(cmd);
		}
	}
}

如果用户选择打开一个新文档, 为了让迅速打开文档, 需要启动一个新线程去打开新文档①,并分离线程②。 与当前线程做出的操作一样, 新线程只不过是打开另一个文件而已。 所以,edit_document函数可以复用, 通过传参的形式打开新的文件
本文编者注:这里居然用了嵌套的方式进行新线程的创建,值得学习。
这个例子也展示了传参启动线程的方法: 不仅可以向 std::thread 构造函数①传递函数名, 还可以传递函数所需的参数(实参)。 当然, 也有其他方法完成这项功能, 比如:使用一个带有数据成员的成员函数, 代替一个需要传参的普通函数

三、向线程函数传递参数

清单2.4中,向可调用对象或函数传递一个参数就像向std::thread 构造函数传递一个附加参数一样简单又基础。但请务必记住,默认情况下,参数会被复制到内部存储中,新创建的执行线程可以在此内部存储中访问这些参数,然后将它们作为rvalue传递给可调用对象或函数,就像它们是临时的一样。

左值(lvalue)包括所有的 “非const” 变量.
右值(rvalue)包括所有"不能赋值"的东西, 包括const 变量, 临时变量, 常数等等.
之所以叫这么个名字, 是因为 左值 可以放在 = 的左边, 而右值不可以.
int a;
const int b = 3;
a = 10; // a可以放在=的左边, 也就是说可以赋值, 那么就是"左值"
b = 10; // const 类型不可以赋值, 也就是说不可以放在=的左边, 所以是"右值"

再来看一个例子:

void f(int i, std::string const& s);
std::thread t(f, 3, "hello");

此代码创建了一个调用f(3, “hello”)的线程。参数有三个,一个是可调用对象或函数"f",另外两个是参数"3"和字符串“hello”,这些参数会被复制到新线程的内部存储当中供新线程的执行函数t来调用。然后参数"3"和字符串“hello”会作为rvalue传递给f()函数。
注意,即便函数f需要一个 std::string 对象作为第二个参数,字符串文字(“hello”)也将作为一个 char const *类型从std::thread t(f, 3, “hello”)中传递给函数f的第二参数,在新线程的上下文中实现从 char const *向std :: string的转换。

但是,当遇见指向动态变量的指针作为参数传递给线程的情况,需要特别要注意:

void f(int i,std::string const& s);
void oops(int some_param)
{
	char buffer[1024]; // 1
	sprintf(buffer, "%i",some_param);
	std::thread t(f,3,buffer); // 2
	t.detach();
}

(教程还是用英文版吧,中文版真是莫名其妙,虽然开始就预料到这个结果,最终还是再次应验)

这种情况下, buffer②是一个指针变量, 指向本地变量缓冲区, 通过指针"buffer"将参数传递到新线程中②。 并且oops函数有很大的可能在缓冲区的被转化成新线程的 std::string 对象之前就结束了, 而因为新线程t当中函数f没有得到 std::string 对象参数的缓存值,而只是一个指针,所以当oops当中定义的实际内存buffer[]随oops函数的结束而注销的时候,t线程以及其中的函数f并不知道这些事情的发生,从而导致t线程的一些未定义行为。 解决方案就是在传递到 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();
}

在本示例当中,问题就出在,我们本来期望将指向缓存的指针隐性的转换为作为新线程函数参数的std::string对象,但是由于新线程的构造函数要拷贝缓存内容,导致这个转换太迟了,当not_oops函数都结束了,这个隐性转换都没有完成,于是新线程中的函数f就没有得到期望的参数值s。
相反的场景是不可能实现的,即对象被拷贝,你需要一个非常量的引用,因为它不需要编译。
如果线程正在更新通过引用传入的数据结构,则可以尝试这样做。例如:

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”期望第二参数(widget_data& data)通过引用进行传递,但是线程std::thread的构造函数(std::thread t(update_data_for_widget,w,data))却不知道这个;这里没有考虑函数所期望的参数类型,而盲目地拷贝了提供的值。但是内部代码(std::thread t(update_data_for_widget,w,data))以rvalue的模式传递拷贝的参数值(t(update_data_for_widget,w,data)中的data参数)以便以只移动类型工作,并因此试着用rvalue的方式调用update_data_for_widget 函数。这个将编译失败,因为不能传递一个rvalue参数(data)给一个期望非常量引用的函数(void update_data_for_widget(widget_id w,widget_data& data))。对于熟悉std::bind的人来说,解决方案很明显:需要将在std::ref中被引用的参数包装起来。在这种情况下,如果您将线程调用更改为如下:

std::thread t(update_data_for_widget,w,std::ref(data));

那么上句中的update_data_for_widget将正确地传递一个对数据的引用,而不是数据的临时副本,代码现在将成功编译。
如果您熟悉std::bind,那么参数传递语义就不足为奇了,因为std::thread构造函数的操作和std::bind的操作都是根据相同的机制定义的。这意味着,例如,只要你提供一个合适的对象指针作为第一个参数,那么你就可以传递一个成员函数指针作为函数:

class X
{
public:
void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work,&my_x);

此代码将在新的线程中调用my_x.do_lengthy_work()函数,因为 类X的对象my_x 的地址是以对象指针的方式提供的。你也可以为这样的成员函数调用提供参数: std::thread线程构造函数的第三个参数将成为成员函数的第一个参数,如此类推。
关于提供参数的另一个有趣的场景是,参数不能复制,只能移动:一个对象中保存的数据被转移到另一个对象,而原始对象为空。这种类型的一个例子是std::unique_ptr,它为动态分配的对象提供自动的内存管理。一次只能有一个std::unique_ptr实例指向一个给定的对象,当该实例被销毁时,被指向的对象将被删除。move构造函数和move赋值操作符允许对象的所有权在std::unique_ptr 实例之间传递(参见附录A, A.1.1节,了解更多关于move语义的信息)。这样的传输会给源对象留下一个空指针。这种值的移动允许将这种类型的对象作为函数参数接受或从函数返回。 对源对象是临时的情况下,那么移动是自动的,但是如果源是一个命名值,则必须通过调用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(process_big_object,std::move(p));

通过在std::thread构造函数(std::thread t(process_big_object,std::move( p)))中指定std::move( p ), big_object的所有权首先转移到新创建线程的内部存储,然后转移到函数process_big_object()。
c++标准库中的一些类显示了与std::unique_ptr相同的所有权语义,std::thread就是其中之一。虽然std::thread实例不像std::unique_ptr那样拥有动态对象,但它们确实拥有资源:每个实例负责管理一个执行线程。这种所有权可以在实例之间转移,因为std::thread的实例是可移动的,即使它们不能复制。这确保在任何时间只有一个对象与特定的执行线程相关联,同时允许程序员选择在对象之间转移所有权。

四、转移线程所有权

假设你想写一个函数来创建一个线程在后台运行,但是你并不想等待它完成,就将新线程的所有权传递给调用函数;或者你可能想做相反的事情:创建一个线程并将所有权传递给某个函数,该函数本应该等待线程完成的。无论哪种情况,您都需要将所有权从一个地方转移到另一个地方。
这就是std::thread的move支持的由来。如前一节所述,c++标准库中的许多资源拥有类型,如std::ifstream和std::unique_ptr,是可移动但不可复制的,std::thread就是其中之一。这意味着特定执行线程的所有权可以在std::thread实例之间移动,如下面的示例所示。该示例显示了创建两个执行线程以及在三个std::thread实例(t1、t2和t3)之间转移这些线程的所有权:

void some_function();
void some_other_function();
std::thread t1(some_function);//启动一个新线程并与t1关联
std::thread t2=std::move(t1);//调用std::move()显式地转移所有权给t2
t1=std::thread(some_other_function);//启动一个新线程并与一个临时std::thread对象相关联,将所有权转移到t1
std::thread t3;//创建线程t3
t3=std::move(t2);//与t2相关联的线程的所有权被转移到t3
t1=std::move(t3);//将t3相关联的线程的所有权转移给t1,但是t1已经有一个关联的线程,正在运行some_other_function

首先,启动一个新线程并与t1关联。然后,在构造t2时,通过调用std::move()显式地转移所有权,所有权转移给t2。此时,t1不再有一个相关的执行线程;运行some_function的线程现在与t2相关联。
然后,启动一个新线程并与一个临时std::thread对象相关联。随后的所有权转移到t1并不需要调用std::move()来显式地转移所有权,因为所有者是一个临时对象——从临时对象转移是自动和隐式的(参看前面一节“对源对象是临时的情况下,那么移动是自动的,但是如果源是一个命名值,则必须通过调用std::move()来直接请求传输”)。
t3是默认构造的,这意味着它在创建时没有任何相关的执行线程。当前与t2相关联的线程的所有权被转移到t3,同样通过显式调用std::move(),因为t2是一个命名对象。完成所有这些操作后,t1与运行some_other_function的线程相关联,t2没有关联的线程,而t3与运行some_function的线程相关联。
最后的一个移动,将运行some_function函数的线程的所有权转移回此函数开始的地方t1。但是在本例中t1已经有一个关联的线程(它正在运行some_other_function),因此会调用std::terminate()来终止程序。这样做是为了与std::thread析构函数保持一致。在2.1.1节中已经看到,在销毁之前必须显式地等待线程完成或分离它,赋值也一样:不能通过给管理线程的std::thread对象赋新值来删除线程。

std::thread中的move()意味着可以很容易地将所有权转移出函数,如下面的清单所示。

Listing 2.5 Returning a std::thread from a function

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));
}

支持std::thread的一个好处是,您可以在清单2.3中的thread_guard类上构建,并让它拥有线程的所有权。这避免了任何不愉快的后果,如果thread_guard对象的寿命超过了它所引用的线程,这也意味着一旦所有权转移到该对象,其他人就不能连接或分离该线程。因为这主要是为了确保线程在退出作用域之前完成,所以我将该类命名为scoped_thread。下面的清单显示了该实现,以及一个简单的示例。

Listing 2.6 scoped_thread and example usage

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();
}

示例与清单2.3类似,但是新线程直接传递给scoped_thread,而不必为它创建一个单独的命名变量。当初始线程到达f结尾时,scoped_thread对象将被销毁,然后与提供给构造函数的线程连接。而对于清单2.3中的thread_guard类,析构函数必须检查线程是否仍然是可连接的,您可以在构造函数中这样做,如果不是,则抛出异常。

c++ 17的建议之一是使用一个类似于std::thread的joining_thread类,除了它会像scoped_thread那样自动连接析构函数之外。这并没有得到委员会的一致意见,所以它没有被纳入标准(尽管它仍然在为c++ 20的std::jthread轨道上),但是它相对来说很容易编写。下一个清单显示了一种可能的实现。

Listing 2.7 A joining_thread class

class joining_thread
{
	std::thread t;
public:
	joining_thread() noexcept=default;
	template<typename Callable,typename ... Args>
	explicit joining_thread(Callable&& func,Args&& ... args):
	t(std::forward<Callable>(func),std::forward<Args>(args)...)
	{}
	explicit joining_thread(std::thread t_) noexcept:
	t(std::move(t_))
	{}
	joining_thread(joining_thread&& other) noexcept:
	t(std::move(other.t))
	{}
	joining_thread& operator=(joining_thread&& other) noexcept
	{
	if(joinable())
	join();
	t=std::move(other.t);
	return *this;
	}
	joining_thread& operator=(std::thread other) noexcept
	{
		if(joinable())
		join();
		t=std::move(other);
		return *this;
	}
	~joining_thread() noexcept
	{
		if(joinable())
		join();
	}
	void swap(joining_thread& other) noexcept
	{
		t.swap(other.t);
	}
	std::thread::id get_id() const noexcept{
		return t.get_id();
	}
	bool joinable() const noexcept
	{
		return t.joinable();
	}
	void join()
	{
		t.join();
	}
	void detach()
	{
		t.detach();
	}
	std::thread& as_thread() noexcept
	{
		return t;
	}
	const std::thread& as_thread() const noexcept
	{
		return t;
	}
};

在std::thread中对move的支持也允许包含std::thread对象的容器,如果这些容器是移动感知的(如更新的std::vector<>)。这意味着您可以编写如下清单所示的代码,这将生成许多线程,然后等待它们完成

Listing 2.8 Spawns some threads and waits for them to finish

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)
	entry.join();
}

我们常常会将一个算法工作用几个线程来分解成几部分来计算,在返回到调用者之前,再将所有细分线程的结果合并起来得到最终算法的结果,各个细分的线程此时必须已经完成。清单2.8的简单结构意味着线程所做的工作是自包含的,它们的操作结果纯粹是对共享数据起副作用。如果f()要向依赖于这些线程执行的操作结果的调用者返回一个值,那么按照写入的方法,这个返回值必须在线程终止后通过检查共享数据来确定。在第4章中讨论了在线程之间传输操作结果的替代方案。 将std::thread对象放入std::vector 是朝着自动化管理这些线程迈出的一步:与其为这些线程分别创建单独的变量并直接的将它们连接,还不如将它们视为一个组。您可以更进一步,在运行的时候来确定线程的动态数量,而不是像清单2.8中那样创建线程的固定数量。

末尾

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值