2-线程管理
2.1 线程管理的基础
2.1.1 启动线程
使用C++线程库启动线程,可以归结为构造 std::thread 对象:
void do_some_work();
std::thread my_thread(do_some_work);
std::thread
可以用可调用类型构造,也就是伪函数
class background_task
{
public:
void operator()()const
{
do_somthing();
do_somthing_else();
}
};
background_task f;
std::thread my_thread(f);
启动了线程,你需要明确是要等待线程结束(加入式——参见2.1.2节),还是让其自主运行(分离式——参见2.1.3节)
如果 std::thread
对象销毁之前还没有做出决定,程序就会终止( std::thread
的析构函数会调用 std::terminate()
)
Tips
对象销毁之前做出决定,否则你的程序将会终止(std::thread
的析构函数会调用std::terminate()
,这时再去决定会触发相应异常)
在线程还没结束,函数已经退出的时候,这时线程函数还持有函数局部变量的指针或引用。下面的清单中就展示了这样的一种情况。
清单2.1 函数已经结束,线程依旧访问局部变量
struct func
{
int& i;
func(int& i_) : i(i_) {}
void operator() ()
{
for (unsigned j=0 ; j<1000000 ; ++j)
{
do_something(i); // 1. 潜在访问隐患:悬空引用
}
}
};
void oops()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.detach(); // 2. 不等待线程结束
} // 3. 新线程可能还在运行
2.1.2 等待线程完成
如果需要等待线程,相关的 std::thread
实例需要使用join()
。清单2.1中,将 my_thread.detach()
替换为 my_thread.join()
,就可以确保局部变量在线程完成后,才被销毁
调用join()
的行为,还清理了线程相关的存储部分,这样 std::thread
对象将不再与已经
完成的线程有任何关联。这意味着,只能对一个线程使用一次join()
;一旦已经使用过
join()
, std::thread
对象就不能再次加入了,当对其使用joinable()
时,将返回false
。
2.1.3 特殊情况下的等待
清单 2.2 等待线程完成
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
}
另外一种方式是使用“资源获取即初始化方式”(RAII,Resource Acquisition Is Initialization),并且提供一个类,在析构函数中使用join()
,如同下面清单中的代码。看它如何简化f()
函数。
清单 2.3 使用RAII等待线程完成
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()
只能对给定的对象调用一次,所以对给已加入的线程再次进行加入操作时,将会导致错误。
2.1.4 后台运行线程
使用detach()
会让线程在后台运行,这就意味着主线程不能与之产生直接交互。也就是说,不会等待这个线程结束;如果线程分离,那么就不可能有 std::thread
对象能引用它,分离线程的确在后台运行,所以分离线程不能被加入
通常称分离线程为守护线程(daemon threads),UNIX中守护线程是指,没有任何显式的用户接口,并在后台运行的线程。这种线程的特点就是长时间运行;线程的生命周期可能会从某一个应用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化。另一方面,分离线程的另一方面只能确定线程什么时候结束,发后即忘(fire and forget)的任务就使用到线程的这种方式。
2.2 向线程函数传递参数
向 std::thread
构造函数中的可调用对象,或函数传递一个参数很简单。需要注意的是,默认参数要拷贝到线程独立内存中,即使参数是引用的形式,也可以在新线程中进行访问.
1.即使参数是引用形式,也可以在新线程中进行访问
void f(int i, std::string& s);
std::thread t(f, 3, "hello");
注意,函数f
需要一个 std::string
对象作为第二个参数,但这里使用的是字符串的字面值,也就是 char const *
类型。之后,在线程的上下文中完成字面值向 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();
}
2.期望传递一个引用,但整个对象被复制了
void update_data_for_widget(widget_id w,widget_data& data); // 1
void oops_again(widget_id w)
{
widget_data data;
std::thread t(update_data_for_widget,w,data); // 整个都被复制
std::thread t(update_data_for_widget,w,std::ref(data)); // 用std::ref将参数转化成引用的形式
display_status();
t.join();
process_widget_data(data); // 3
}
3.提供的参数可以移动,但不能拷贝
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
函数。
2.3 转移线程所有权
- 显式使用
std::move()
,原std::thread
对象的所有权就转移给了新的对象。之后,元对象和执行线程已经没有关联了 - 只能转移到没有关联的线程
std::thread
支持移动的好处是可以创建thread_guard
类的实例(定义见 清单2.3),并且拥有其线程的所有权
清单2.6 scoped_thread的用法
class scoped_thread
{
std::thread t;
public:
explicit scoped_thread(std::thread t_): // 1
t(std::move(t_))
{
if(!t.joinable()) // 2
throw std::logic_error(“No thread”);
}
~scoped_thread()
{
t.join(); // 3
}
scoped_thread(scoped_thread const&)=delete;
scoped_thread& operator=(scoped_thread const&)=delete;
};
struct func; // 定义在清单2.1中
void f()
{
int some_local_state;
scoped_thread t(std::thread(func(some_local_state))); // 4
do_something_in_current_thread();
} // 5
2.4 运行时决定线程数量
std::thread::hardware_concurrency()
在新版C++标准库中是一个很有用的函数。这个函数将返回能同时并发在一个程序中的线程数量。例如,多核系统中,返回值可以是CPU核芯的数量。返回值也仅仅是一个提示,当系统信息无法获取时,函数也会返回0(比如单核)
清单2.8 原生并行版的 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) // 1 如果输入的范围为空,就会得到init的值
return init;
unsigned long const min_per_thread=25;
unsigned long const max_threads= (length+min_per_thread-1)/min_per_thread; // 2 用范围内元素的总数量除以线程(块)中最小任务数,从而确定启动线程的最大数量
unsigned long const hardware_threads= std::thread::hardware_concurrency();
unsigned long const num_threads= std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads); // 3 计算量的最大值和硬件支持线程数中,较小的值为启动线程的数量(当 std::thread::hardware_concurrency() 返回0,你可以选择一个合适的数作为你的选择;在本例中,我选择了"2"。你也不想在一台单核机器上启动太多的线程)
unsigned long const block_size=length/num_threads; // 4 每个线程中处理的元素数量,是范围中元素的总量除以线程的个数得出的
std::vector<T> results(num_threads);
std::vector<std::thread> threads(num_threads-1); // 5 启动的线程数必须比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); // 6 使用简单的循环来启动线程:block_end迭代器指向当前块的末尾
threads[i]=std::thread( // 7 启动一个新线程为当前块累加结果
accumulate_block<Iterator,T>(),
block_start,block_end,std::ref(results[i]));
block_start=block_end; // 8 当迭代器指向当前块的末尾时,启动下一个块
}
accumulate_block<Iterator,T>()(
block_start,last,results[num_threads-1]); // 9 启动所有线程后,线程会处理最终块的结果
std::for_each(threads.begin(),threads.end(),
std::mem_fn(&std::thread::join)); // 10 当累加最终块的结果后,可以等待 std::for_each 创建线程的完成
return std::accumulate(results.begin(),results.end(),init); // 11 使用 std::accumulate 将所有结果进行累加
}
2.5 识别线程
线程标识类型是 std::thread::id
,可以通过两种方式进行检索
- 调用
std::thread
对象的成员函数get_id()
来直接获取。如果std::thread
对象没有与任何执行线程相关联,get_id()
将返回std::thread::type
默认构造值,这个值表示“没有线程” - 当前线程中调用
std::this_thread::get_id()
(这个函数定义在头文件中)也可以获得线程标识
2.6 本章总结
本章讨论了C++标准库中基本的线程管理方式:启动线程,等待结束和不等待结束(因为需要它们运行在后台)。并了解应该如何在线程启动前,向线程函数中传递参数,如何转移线程的所有权,如何使用线程组来分割任务。最后,讨论了使用线程标识来确定关联数据,以及特殊线程的特殊解决方案。虽然,现在已经可以纯粹的依赖线程,使用独立的数据,做独立的任务(如同清单2.8),但在某些情况下,线程确实需要有共享数据。第3章会讨论共享数据和线程的直接关系。第4章会讨论在(有/没有)共享数据情况下的线程同步操作。