C++并发编程(中文版)(C++ Concurrency In Action)
- 第 2 章 线程管理
- 2.1 线程管理基础
- 2.1.1 启动线程 - 构造 std::thread 对象
- (1) 使用线程函数构造
- (2) 使用可调用类型构造
- (3) 使用 lambda 表达式构造
- (4) 明确 std::thread 的运行方式
- 2.1.2 加入式 - 等待线程完成
- 2.1.3 特殊情况下的等待
- (1) 避免应用被抛出的异常所终止 - 在异常处理过程中调用 join()
- (2) 资源获取即初始化方式(RAII,Resource Acquisition Is Initialization)
- 2.1.4 分离式 - 后台运行线程
- 2.2 向线程函数传递参数
- 2.2.1 不要传递指向动态变量的指针
- 2.2.2 使用 std::ref 包装引用
- 2.2.3 传递成员函数指针
- 2.2.4 移动参数
- 2.3 转移线程所有权
- 2.3.1 std::thread 对象具有可移动且不可复制性
- 2.3.2 不能对已有关联线程的 std::thread 对象赋值
- 2.3.3 std::thread 实例可作为参数进行传递
- 2.4 运行时决定线程数量
- 2.5 识别线程
- 2.5.1 获取线程 ID
- 2.5.2 用线程 ID 区分线程函数的运行权限
标准库头文件 thread
标准库线程类 std::thread
使用可调用对象(函数、类、lambda)构造一个线程对象
(1) 使用线程函数构造void do_some_work();
std::thread my_thread(do_some_work);
(2) 使用可调用类型构造
class background_task
{
public:
void operator()() const
{
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f);
注意:如果可调用类型是临时变量,而不是一个命名的变量,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
(3) 使用 lambda 表达式构造
std::thread my_thread([]{
do_something();
do_something_else();
});
(4) 明确 std::thread 的运行方式
启动了线程,你需要明确是要等待线程结束(加入式),还是让其自主运行(分离式)
加入式和分离式分别需要使用 std::thread 的成员函数 join() 和 detach()
- 注意1:必须在 std::thread 对象销毁之前做出决定,否则你的程序将会终止(std::thread 的析构函数会调用 std::terminate(),这时再去决定会触发相应异常)
- 注意2:保证线程结束之前,可访问的数据的有效性
对象销毁之后再去访问,会产生未定义行为,如下所示:
struct func {
int* i;
func(int* i_) : i(i_) {}
void operator() ()
{
for (unsigned j = 0; j < 1000000; ++j)
{
do_something(*i);
}
}
};
void oops_join()
{
int* some_local_state = new int(0);
func my_func(some_local_state);
std::thread my_thread(my_func);
delete some_local_state;
my_thread.join();
// 新线程试图访问已销毁的对象(some_local_state)
}
这种情况还可能发生在线程还没结束,函数已经退出的时候,这时线程函数还持有函数局部变量的指针或引用,下面展示了这样的一种情况:
void oops_detach()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.detach();
// 不等待线程结束,新线程可能还在运行
}
2.1.2 加入式 - 等待线程完成
如果需要等待线程完成,需要使用 std::thread 的成员函数 join()
加入式可以确保局部变量在线程完成后,才被销毁
只能对一个线程使用一次 join(),一旦已经使用过 join(),std::thread 对象就不能再次加入了,当对其使用 joinable() 时,将返回 false
void f() {
auto my_func = []() {
// do_something();
};
std::thread t(my_func);
try
{
// do_something_in_current_thread();
}
catch (...)
{
t.join();
throw;
}
t.join();
}
(2) 资源获取即初始化方式(RAII,Resource Acquisition Is Initialization)
class thread_guard {
std::thread& t;
public:
explicit thread_guard(std::thread& t_) : t(t_) {}
~thread_guard() {
if (t.joinable()) // 判断线程是否已加入
t.join(); // 如果没有,调用 join() 进行加入
}
// 直接对一个 std::thread 对象进行拷贝或赋值是危险的
thread_guard(thread_guard const&) = delete;
thread_guard& operator=(thread_guard const&) = delete;
};
void f() {
auto my_func = []() {
// do_something();
};
std::thread t(my_func);
thread_guard g(t);
// do_something_in_current_thread();
}
当线程执行完 do_something_in_current_thread() 后,局部对象就要被逆序销毁了
因此,thread_guard 对象 g 是第一个被销毁的,这时线程在析构函数中被加入到原始线程中
如果需要后台运行线程,需要使用 std::thread 的成员函数 detach()
主线程不能与后台运行线程产生直接交互
注意:不能对没有/执行线程/的 std::thread 对象使用 detach()
当 std::thread 对象 t 使用 t.joinable(* 返回的是 true,才可以使用 t.detach()
向 std::thread 构造函数中的可调用对象传递参数很简单:
void f(int i, string const& s);
std::thread t(f, 3, "hello");
2.2.1 不要传递指向动态变量的指针
当指向动态变量的指针作为参数传递给线程,很有可能导致一些未定义的行为,如下所示:
void f(int i, string const& s);
char buffer[1024];
std::thread t(f, 3, buffer);
解决方案就是在传递到 std::thread 构造函数之前就将字面值转化为 string 对象:
void f(int i, string const& s);
char buffer[1024];
std::thread t(f, 3, string(buffer));
2.2.2 使用 std::ref 包装引用
直接传递一个引用给线程,构造函数可能无视函数期待的参数类型,盲目拷贝已提供的变量,并将内部拷贝的引用传递给函数:
void reverse_str(string& str) {
reverse(str.begin(), str.end());
}
int main() {
string str = "Hello,World!";
std::thread my_thread(reverse_str, str);
my_thread.join();
}
直接传递引用会引发一系列的问题,可以使用 std::ref 包装引用,从而可将线程的调用改为以下形式:
std::thread my_thread(reverse_str, std::ref(str));
在这之后,reverse_str 就会接收到一个 str 变量的引用,而非拷贝的引用
2.2.3 传递成员函数指针注意:以下传递成员函数指针的做法是错误的
class my_class {
public:
void print() {
cout << "Hello,World!" << endl;
}
};
int main() {
my_class x;
std::thread my_thread(x.print);
my_thread.join();
}
要将成员函数作为线程函数,就应该提供相应的函数指针,如下所示:
int main() {
my_class x;
std::thread my_thread(&my_class::print, &x);
my_thread.join();
}
std::thread 构造函数的第一个参数传递一个成员函数指针,第二个参数提供一个合适的对象指针
向成员函数传递参数很简单:
class my_class {
public:
void reverse_str(string& str) {
reverse(str.begin(), str.end());
}
};
int main() {
string str = "Hello,World!";
my_class x;
std::thread my_thread(&my_class::reverse_str, &x, std::ref(str));
my_thread.join();
}
2.2.4 移动参数
(我们不考虑引用的移动,因为这没有意义 )
有时我们希望提供给线程的资源被线程独占,这时候就需要使用 std::move
void reverse_print(string str) {
reverse(str.begin(), str.end());
cout << str << endl;
}
int main() {
string str = "Hello,World!";
std::thread my_thread(reverse_print, std::move(str));
if (str.empty())
cout << "Empty." << endl;
else cout << str << endl;
my_thread.join();
}
运行结果:
Empty.
!dlroW,olleH
注意:一般的函数调用,参数是值传递的(引用除外),而 std::thread 的构造函数中,提供给线程函数的参数是移动传递的(引用包装器除外)
struct para_class {
para_class() = default;
para_class(para_class&&)noexcept {
cout << "move-constructor" << endl;
}
para_class(para_class const&) {
cout << "copy-constructor" << endl;
}
};
void tester1(para_class x) {
// do_something();
}
int main() {
para_class x;
std::thread my_thread(tester1, x); // 1
my_thread.join();
}
运行结果:
copy-constructor
move-constructor
上述结果表面,参数值传递给 std::thread 的构造函数,移动传递给线程函数
将 1 处代码改写成以下形式:
std::thread my_thread(tester1, std::move(x));
将有以下运行结果:
move-constructor
move-constructor
通过参数的两次移动,就实现了线程对资源的独占
2.3 转移线程所有权 2.3.1 std::thread 对象具有可移动且不可复制性void do_something() {
// insert code
}
int main() {
std::thread t1(do_something);
std::thread t2 = std::move(t1); // 正确,t1 线程的所有权转移给了 t2
std::thread t3 = t2; // 错误,std::thread 对象不可复制
}
2.3.2 不能对已有关联线程的 std::thread 对象赋值
std::thread t1(do_something);
std::thread t2;
t2 = std::move(t1); // 正确,t2 没有关联线程
std::thread t3(do_something);
t3 = std::move(t2); // 错误,t3 已有关联线程
2.3.3 std::thread 实例可作为参数进行传递
void print() {
cout << "Hello,World!" << endl;
}
void exe_thread(std::thread t) {
cout << "thread begin." << endl;
t.join();
cout << "thread end." << endl;
}
int main() {
std::thread t(print);
exe_thread(std::move(t));
}
2.4 运行时决定线程数量
并行运算前决定线程数量可以避免产生太多的线程,实际启动线程数需要综合考虑并行运算效率和硬件线程数
标准库函数 std::thread::hardware_concurrency() 返回能同时并发在一个程序中的线程数量
例:多核系统中,返回值可以是 CPU 核芯的数量
注意:返回值仅仅是一个提示,当系统信息无法获取时,函数也会返回 0
hardware_concurrency 函数对启动线程数量有很大帮助,如下所示:
// 并行版的 std::accumulate(不考虑异常)
int concurrent_accumulate(int* _First, int* _Last, int _Val) {
// 决定线程数量
const size_t length = _Last - _First; // 元素数量
const size_t min_per_thread = 25; // 每个线程最小计算量
const size_t max_threads = (length + min_per_thread - 1) / min_per_thread; // 最大线程数
const size_t hardware_threads = std::thread::hardware_concurrency(); // 硬件线程数
const size_t num_threads = min(max(hardware_threads, 1U), max_threads); // 实际线程数
// 量产线程
const size_t block_size = length / num_threads; // 每个线程平均计算量
auto accumulate = [&](int* _Beg, int* _End) {
_Val += std::accumulate(_Beg, _End, 0); }; // 线程函数
vector<std::thread> threads(num_threads);
for (size_t i = 0; i < num_threads - 1; i++)
threads[i] = std::thread(accumulate, _First + i * block_size, _First + (i + 1) * block_size);
threads.back() = std::thread(accumulate, _First + (num_threads - 1) * block_size, _Last);
// 并行运算
for (auto& t : threads)
t.join();
return _Val;
}
2.5 识别线程
2.5.1 获取线程 ID
线程标识类型是 std::thread::id,可以通过两种方式进行检索
2.5.2 用线程 ID 区分线程函数的运行权限
- 通过调用 std::thread 对象的成员函数 get_id() 直接获取
(如果 std::thread 对象没有与任何执行线程相关联,get_id() 将返回默认构造值 0)- 当前线程中调用 std::this_thread::get_id() 标准库为 std::thread::id 定义了比较 *** 作,允许将其当作容器的键值,做排序
当用线程来分割一项工作,主线程可能要做一些与其他线程不同的工作
即,主线程的权限高于其他线程,如下所示:
std::thread::id master_thread;
void some_core_part_of_algorithm()
{
if(std::this_thread::get_id()==master_thread)
{
do_master_thread_work();
}
else do_common_work();
}
另外,将线程 ID 分级存储到一个数据结构中,就可以实现线程函数运行权限的多级划分
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)