C++并发编程

C++并发编程,第1张

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

第 2 章 线程管理 2.1 线程管理基础 2.1.1 启动线程 - 构造 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

2.1.3 特殊情况下的等待 (1) 避免应用被抛出的异常所终止 - 在异常处理过程中调用 join()
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 是第一个被销毁的,这时线程在析构函数中被加入到原始线程中

2.1.4 分离式 - 后台运行线程

如果需要后台运行线程,需要使用 std::thread 的成员函数 detach()
主线程不能与后台运行线程产生直接交互
注意:不能对没有/执行线程/的 std::thread 对象使用 detach()
std::thread 对象 t 使用 t.joinable(* 返回的是 true,才可以使用 t.detach()

2.2 向线程函数传递参数

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,可以通过两种方式进行检索

  1. 通过调用 std::thread 对象的成员函数 get_id() 直接获取
    (如果 std::thread 对象没有与任何执行线程相关联,get_id() 将返回默认构造值 0)
  2. 当前线程中调用 std::this_thread::get_id() 标准库为 std::thread::id 定义了比较 *** 作,允许将其当作容器的键值,做排序
2.5.2 用线程 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 分级存储到一个数据结构中,就可以实现线程函数运行权限的多级划分

欢迎分享,转载请注明来源:内存溢出

原文地址: http://www.outofmemory.cn/langs/1498783.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-25
下一篇 2022-06-25

发表评论

登录后才能评论

评论列表(0条)

保存