IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 【C++并发编程】(一)线程管理 -> 正文阅读

[C++知识库]【C++并发编程】(一)线程管理

一、并发概念

1.1 为什么使用并发

分离关注点

  • 将相关的代码与无关的代码分离,可以使程序更 容易理解和测试,从而减少出错的可能性。
  • 使一些功能区域中的操作需要在同一时刻发生的情况下,使用并发分离不同的功能区域

提高性能

  • 将一个单个任务分成几部分,且各自并行运行,从而降低总运行时间。这就是任务并行(task parallelism)
  • 在数据方面每个线程在 不同的数据部分上执行相同的操作。数据并行(data parallelism)

1.2 不使用并发

  • 收益 比不上成本
  • 性能增益可能会小于预期

1.3 C++新标准中的并发

  • C++11 : 管理线程、保护共享数据、线程间同步操作,以及低层原子操作
  • C++14 : 为并发和并行添加了一个新的互斥量类型,用于保护共享数据
  • C++17 : 一开始就添加了一整套的并行算法

二、线程管理基础

每个程序至少有一个线程:执行main()函数的线程,其余线程有其各自的入口函数。线程与原 始线程(以main()为入口函数的线程)同时运行。如同main()函数执行完会退出一样,当线程执 行完入口函数后,线程也会退出。

2.1 启动线程

使用C++线程库启动线程,可以归结为构造 std::thread 对象

#include <thread>
void do_some_work(); 
std::thread my_thread(do_some_work);

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

class background_task 
{
public: 
	void operator()() const 
	{ 
		do_something(); 
		do_something_else(); 
	} 
};
background_task f; 
std::thread my_thread(f);

void operator()() const
()是操作符的名字, ()的操作将被调用,当对象前面用()时.

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

当把函数对象传入到线程构造函数中时,如果你传递了一个临时变量,而不是一个命名的变 量;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

等待线程完成

**join()**是简单粗暴的等待线程完成或不等待。

#include <iostream> 
#include <thread> // 1 
void hello() // 2 
{ 
	std::cout << "Hello Concurrent World\n"; }
	int main() 
{ 
	std::thread t(hello); // 3 
	t.join(); // 4 
}

等待新线程结束再结束程序

struct func 
{ 
	int& i; 
	func(int& i_) : i(i_) {} 
	void operator() () 
	{ 
		for (unsigned j=0 ; j<1000000 ; ++j) 
		{ 
			do_something(i); // 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 块确保访问本地状态的线程退出后,函数才结束。当函数 正常退出时,会执行到②处;当函数执行过程中抛出异常,程序会执行到①处。

try…catch 语句的执行过程是:

  • 执行 try 块中的语句,如果执行的过程中没有异常拋出,那么执行完后就执行最后一个 catch 块后面的语句,所有 catch 块中的语句都不会被执行;
  • 如果 try 块执行的过程中拋出了异常,那么拋出异常后立即跳转到第一个“异常类型”和拋出的异常类型匹配的 catch 块中执行(称作异常被该 catch 块“捕获”),执行完后再跳转到最后一个 catch 块后面继续执行。

后台运行线程

使用detach()会让线程在后台运行,这就意味着主线程不能与之产生直接交互,不会等待这个线程结束。不可能有 std::thread 对象能引用它。C++运行库保证,当线程退出时,相关资源的能够正确回收,后台线程的归属和控制C++运行库都会处理。
通常称分离线程为守护线程(daemon threads)

  • 长时间运行:线程的生命周期可能会从某 一个应用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化
  • 只能确定线程什么时候结束,**发后即忘(fire and forget)**的任务就使用到线程的这种方式

【例】使用分离线程去处理其他文档

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

如果用户选择打开一个新文档,需要启动一个新线程去打开新文档①,并分离线程②。
传参启动线程的方法: 不仅可以向 std::thread 构造函数①传递函数名,还可以传递函数所需的参数(实参)

2.2 向线程传参

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

代码创建了一个调用f(3, “hello”)的线程。注意,函数f需要一个 std::string 对象作为第二个参数,但这里使用的是字符串的字面值,也就是 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(); 
}

std::thread 的构造函数会复制提供的变量,就只复制了没有转换成期望类型的字符串字面值。函数有很有可能会在字面值转化成 std::string 对象之前崩溃
**解决方法:**将字面值转化为 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,std::string(buffer)); // 使用std::string,避免悬垂指针
	t.detach(); 
}

使用线程更新一个引用传递的数据结构

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); // 2 
	display_status(); 
	t.join(); 
	process_widget_data(data); 
}

虽然update_data_for_widget①的第二个参数期待传入一个引用,但是 std::thread 的构造函数②并不知晓;构造函数无视函数期待的参数类型,并盲目的拷贝已提供的变量
**解决方法:**可以使用 std::ref 将参数转换成引用的形式

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,std::ref(data)); // 2 
	display_status(); 
	t.join(); 
	process_widget_data(data); 
}

传递一个成员函数指针作为线程函数

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

提供的参数为成员函数和所属的指针对象。

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::move(p)big_object对象的所有权就被首先转移到新创 建线程的的内部存储中,之后传递给process_big_object函数。

2.3 转移线程所有权

std::thread 可移动,但不可拷贝。 这就说明执行线程的所有权可以在 std::thread 实例中移动

void f1()
{
	cout << "f1" << endl;
}
void f2()
{
	cout << "f2" << endl;
}

int main()
{
	std::thread t1(f1);
	cout << "move t1 to t2" << endl;
	std::thread t2 = std::move(t1);
	cout << "create new thread" << endl;
	t1 = std::thread(f2);
	cout << "create t3" << endl;
	std::thread t3;
	cout << "move t2 to t3" << endl;
	t3 = std::move(t2);
	cout << "move t3 to t1" << endl;
	t1 = std::move(t3);
	return 0;
}
  • 创建新线程t1执行f1
  • 使用 std::move() 创建t2,t1的所有权就转移给了t2。之后,t1和执行线程已经没有关联了,执行f1的函数线程与t2关联。
  • 创建一个临时的std::thread 对象,移动操作将会隐式的调用。
  • t3使用默认构造方式创建,与任何执行线程都没有关联。
  • 调用 std::move() 将与t2关联线程的所有权转移到t3中。因为t2是一个命名对象,需要显式的调用 std::move()
  • 将与t3关联线程的所有权转移到t1中。t1已经有了一个关联 的线程,所以这里系统直接调用 std::terminate() 终止程序继续运行。

【函数返回 std::thread 对象】
线程的所有权可以在函数外进行转移

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)); 
}
  • f(std::thread(some_function)); f(std::move(t)); 作为参数调用f(移动)

【 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(); 
}
  • 新线程直接传递到scoped_thread中
  • scoped_thread对象析构时会销毁,然后加入到的构造函数创建的线程对象std::thread t中去
  • 把检查放在了构造函数中,并且当线程不可加入时,抛出异常

【量产线程】

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

函数模板std :: mem_fn生成指向成员的指针的包装对象,该对象可以存储,复制和调用指向成员的指针。
调用std ::mem_fn时,可以使用对象的引用和指针(包括智能指针)。

2.4 标识线程

线程标识类型为 std::thread::id,可以通过两种方式进行检索:

  • 可以通过调 用 std::thread 对象的成员函数 get_id() 来直接获取
  • 当前线程中调用 std::this_thread::get_id() (这个函数定义在 头文件中)也可以 获得线程标识。

std::thread::id 对象可以自由的拷贝和对比,因为标识符就可以复用。如果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(); 
	}
	do_common_work(); 
}
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-09-15 01:47:57  更:2022-09-15 01:50:46 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 13:05:27-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码