《C++并发编程实战》读书笔记——chapter 2_线程管理

52 篇文章 3 订阅
4 篇文章 0 订阅

更多的阅读笔记,及示例代码见 Github:

https://github.com/anlongstory/C-_Concurrency_in_Action_reading_notes

本章主要内容:

  • 启动新线程
  • 等待线程与分离线程
  • 线程唯一标识符

2.1 线程管理基础

每个程序至少有一个线程: 执行 main() 函数的线程,其余线程有各自的入口函数。 线程执行完入口函数后,线程也会推出。

####2.1.1 启动线程

启动线程可以使用如下语法:

void do_some_work();
std::thread thread_name(do_some_work);

也可以使用可调用类型构造,将带有函数调用符类型的实例传入 srd::thread 类中,替换默认的构造函数。

class MyClass
{
public:
	void operator()() const
	{
		do_some_thing();
		do_some_thing_else();
	}
};

MyClass f;
std::thread thread_name(f);

注意事项: 这里如果你传入的是一个临时变量,而不是一个命名的变量,C++编译器会将其解析为函数声明,而不是类型对象的定义。

如:

std::thread thread_name(Myclass());

这里相当与声明了一个名为thread_name的函数,这个函数带有一个参数(函数指针指向没有参
数并返回 Myclass 对象的函数),返回一个 std::thread 对象的函数,而非启动了一个线程。

上述问题解决办法:

  • 使用多组括号或者使用新的初始化方法
   std::thread thread_name ((Myclass()));
   std::thread thread_name{Myclass()};
  • 使用 lamda 表达式

lamda 表达式允许使用一个可以捕获局部变量的局部函数(可以避免传递参数)

std::thread thread_name([]{
				do_something();
			    do_something_else();
				});

启动线程以后需要注意的问题是: 你需要明确是要等待线程结束(joined),还是让其自主运行(detached)。 若是选择自主运行,则必须保证线程结束前,可访问数据的有效性。

反例见下:

struct func
{
	int &i;
	func(int &i_) : i(i_){}

	void operator()()
	{
		for (unsigned j = 0; j < 100000; j++)
		{
			do_something(i);
		}
	}
};

void oops()
{
	int locate_state = 0;
	func my_func(locate_state);
	std::thread thread_name(my_func);
	thread_name.detach();  // 不等待线程结束
}

这里选择不等待线程结束,当新的线程 thread_name 启动以后,oops()函数继续运行,后面不进行其他操作,所以很快就会结束,而 thread_name 线程里通过多次调用 do_something(i) 可能还在运行,这时局部变量 locate_state 就会被销毁,再掉用可能就会出错。

####2.1.2 等待线程完成

这里就是调用thread_name.join(),就可以确保局部变量在线程完成后,才被销毁。 其是简单粗暴的等待线程完成或不等待,当需要对等待中的线程有更灵活的控制时,例如,查看某个线程是否结束,或者只等待一段时间,就需要其他机制来完成,比如 条件变量和期待(future)。只能对抑恶个线程使用一次 join()

####2.1.3 特殊情况下的等待

####2.1.4 后台运行线程

使用 detach() 会让线程在后台运行,这就意味着主线程不能与之产生直接交互,所以分离线程不能被加入(再调用 join())。所以可用同样的方式进行检查——当 std::thread 对象使用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);  // 1 
			t.detach();  // 2
		}
		else
		{
			process_user_input(cmd);
		}
	}
}

如果用户选择打开一个新文档,需要启动一个新线程去打开新文档(1 处),并分离线程(2 处)。与当前线程做出的操作一样,新线程只不过是打开另一个文件而已。所以,edit_document函数可以复用,通过传参的形式打开新的文件。

2.2 向线程函数传递参数

向 std::thread 构造函数中的可调用对象,或函数传递一个参数很简单,默认参数要拷贝到线程独立内存中,即使参数是引用的形式,也可以在新线程中进行访问。
void f(int i, std::string const& s);
std::thread t(f, 3, “hello”);

这里使用的是 "hello" 字面值,是 char const* 类型,在线程上下文中完成字面值向 string 对象的转化。 一定要保证在 std::thread 构造函数之前就将传入的数据转成理想格式。

std::ref 可以将参数直接转化成引用的形式,因为 std::thread 构造函数和 std::bind 的操作都在标准库中定义好了,可以传递一个成员函数指针作为线程函数,并提供一个合适的对象指针作为第一个参数。 (这里 std::bind 要额外查阅资料)

提供的参数可以移动,但不能拷贝,详见 std::move 的用法。

2.3 转移线程所有权

void some_function();
void some_other_function();

std::thread t1(some_function);
std::thread t2 = std::move(t1); // t1 线程执行的函数与 t2 关联,与 t1 无关
t1 = std::thread(some_other_function); // 临时对象的线程启动,移动操作会隐式调用
std::thread t3;  // 默认方式构造 t3
t3 = std::move(t2); // t2 是一个命名对象,需要显示调用 move()

// 此时,t1 与 some_other_function 线程关联,
//t2 不与任何线程关联, 
//t3与 some_function 线程关联

t1 = std::move(t3);

上述最后一步会使程序直接停止运行,但不会抛出异常,不能通过赋一个新值给 std::thread 对象的方式来"丢弃"一个线程

std::thread 支持移动,就意味着线程的所有权可以在函数外进行转移,说明 Sth::thread 可以作为函数的返回类型,以及进行类似于 push进 std::vector 中,实现线程自动化管理等操作。

std::thread f()
{
	void func1();
	return std::thread(func1);
}

std::thread g()
{
	void func2(int i);
	std::thread t(func2, 42);
	return t;
}

还有:

void do_work(int i);

void h()
{
	std::vector<std::thread> threads;
	for (int i = 0; i < 20; i++)
	{
		threads.push_back(std::thread(do_work, i));
	}
	// 对每个线程调用 join()
	std::for_each(threads.begin(),threads.end(),std::mem_fn(&std::thread::join()) 
}

2.4 运行时决定线程数量

std::thread::hardware_concurrency() 在新版C++标准库中是一个很有用的函数。这个函数将返回能同时并发在一个程序中的线程数量。

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);
	// 如果输入的范围为空,就返回得到的 init 值
	if (!length)  // 1
	{
		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 =     // 3  
	  	std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
	// 每个进程中处理的元素数量
	unsigned long const block_size = length / num_threads; // 4
	// 创建容器存放中间结果
	std::vector<T> results(num_threads);
	// 启动线程数比总数少1,是减去了主线程
	std::vector<std::thread> threads(num_threads - 1);  // 5
	
	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
		// 不断累加中间结果
		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

	return std::accumulate(results.begin(), results.end(), init); // 11
}

2.5 识别线程

线程标志类型是 std::thread::id,第一种检索方式,可以通过调
用 std::thread 对象的成员函数 get_id() 来直接获取。如果 std::thread 对象没有与任何执行线程相关联, get_id() 将返回 std::thread::type 默认构造值。这个值表示“没有线程”。第二种,当前线程中调用 std::this_thread::get_id() (这个函数定义在 头文件中)也可以获得线程标识。

std::thread::id 对象可以自由的拷贝和对比,因为标识符就可以复用。如果两个对象
的 std::thread::id 相等,那它们就是同一个线程,或者都“没有线程”。如果不等,那么就代
表了两个不同线程,或者一个有线程,另一没有。

std::thread::id master_thread;
void some_core_part_of_algrithm()
{
	if (std::this_thread::get_id() == master_thread)
	{
		do_master_thread_work();
	}
	do_common_work();
}

如上述示例,std::thread::id 实例常用作检测线程是否需要进行一些操作,比如:当用线程来分割一项工作,主线程可能要做一些与其他线程不同的工作。这种情况下,启动其他线程前,它可以将自己的线程ID通过std::this_thread::get_id() 得到,并进行存储。就是算法核心部分(所有线程都一样的),每个线程都要检查一下,其拥有的线程ID是否与初始线程的ID相同。另外,当前线程的std::thread::id 将存储到一个数据结构中。之后在这个结构体中对当前线程的ID与存储的线程ID做对比,来决定操作是被“允许”,还是“需要”(permitted/required)。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值