C++多线程

本文围绕C++11多线程编程展开,介绍了多线程的概念、优点及C++11对多线程的支持。详细阐述了创建线程的方法,以及join和detach的区别。还讲解了mutex、atomic、condition_variable、future等相关类和函数的功能与使用,为C++多线程编程提供了全面指导。

1. 多线程

  多线程(Multithreading)是指从软件或者硬件上实现多个线程并发执行的技术。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。

多线程的优点包括:

  • 提高程序的响应速度:多线程可以将程序的任务分成多个部分并行执行,从而减少等待时间,提高程序的响应速度。
  • 充分利用多核处理器:多线程可以充分利用多核处理器的并行计算能力,提高程序的运行效率。
  • 提高系统资源的利用率:多线程可以更充分地利用系统的CPU、内存等资源,提高系统资源的利用率。
  • 支持复杂的并发控制:多线程可以方便地实现复杂的并发控制,如同步、互斥等操作。

  在C++11之前并没有引入线程的支持,如果我们想要在C++中实现多线程,就需要借助操作系统提供的API或者使用第三方库,比如Linux中的pthread库、windows中使用CreateThread函数、Qt、boost库等。

  C++11 引入了多线程支持,该功能在 <thread> 头文件中提供。这一特性不仅解决了跨平台问题,还为线程管理、数据保护、同步操作和原子操作等提供了丰富的类和函数。这些功能使得多线程编程更加简洁、高效,并减少了出错的可能性。在C++11中引入了<thread>、<mutex>、<atomic>、<condition_variable>、<future>5个头文件来支持多线程编程,如下图所示:

thread
thread
mutex
atomic
condition_variable
future
thread
this_thread
mutex
timed_mutex
recursive_mutex
recursive_timed_mutex
lock_guard
uniqure_lock
atomic
atomic_flag
condition_variable
condition_variable_any
future
shared_future
promise
packaged_task
join
detach
sleep_for
sleep_until
get_id
yield

2. thread

2.1 创建线程

在C++中创建线程非常方便,总体来说有3种方法来指定线程线程函数。

2.1.1 全局函数

#include <thread>
#include <iostream>
using namespace std;
void workThread1() {
	for (int i=0; i<10; ++i) {
		cout << i << endl;
	}
}

void workThread2(int n) {
	for (int i=0; i<10; ++i) {
		cout << n << "-" << i << endl;
	}
}

int main() {
	thread t1(workThread1);    //没有参数
	t1.join(); //等待线程执行结束
	
	thread t2(workThread2, 2); //有参数
	t2.join(); //等待线程执行结束
	return 0;
}

g++编译

g++ main.cpp -lpthread

执行结果:
在这里插入图片描述

2.1.2 成员函数

#include <thread>
#include <iostream>
using namespace std;

class TaskRunner {
public:
	void run(int n) {
		for (int i=0; i<10; ++i) {
			cout << n << "-" << i << endl;
		}
	}
};

int main() {
	TaskRunner runner;
	thread t1(&TaskRunner::run, runner, 1);
	t1.join(); //等待线程执行结束
	
	thread t2(&TaskRunner::run, &runner, 2);
	t2.join(); //等待线程执行结束
	return 0;
}

g++编译

g++ main.cpp -lpthread

执行结果:
在这里插入图片描述

2.1.3 Lambda表达式

#include <thread>
#include <iostream>
using namespace std;

int main() {
	thread t1([](int n){
		for (int i=0; i<10; ++i) {
			cout << n << "-" << i << endl;
		}
	}, 1);
	t1.join(); //等待线程执行结束
	return 0;
}

g++编译

g++ main.cpp -lpthread

执行结果:
在这里插入图片描述

2.2 join和detach的区别

join和detach是std::thread的两个重要成员函数,它们用于控制线程的生命周期。以下是它们之间的主要区别:

  • join():

    • 当一个线程完成其执行后,它会自动退出并释放其资源。然而,如果一个线程在某个点上被阻塞或无限循环,那么它不会自动退出。在这种情况下,可以使用join()函数来等待线程完成执行。
    • join()会阻塞当前线程,直到被调用的线程对象所代表的线程完成执行。换句话说,它会让当前线程等待,直到被调用join()的线程执行完毕。
    • 如果一个线程对象已经调用过join(),并且再次尝试调用join(),那么会抛出std::system_error异常。
  • detach():

    • 与join()不同,detach()函数会允许线程独立地运行。当一个线程被分离后,它将继续运行,直到完成,而不会影响调用detach()的线程。
    • 分离的线程会继续在后台运行,直到完成。这意味着即使主程序结束了,分离的线程仍然会继续执行。需要注意的是,如果主程序结束了,那么所有分离的线程也会被操作系统自动终止。
    • 与join()不同,多次调用detach()对同一个线程对象没有影响。

总结:

  • join()就行等待,用于等待线程完成执行。如果当前线程被阻塞或需要等待某个线程完成,可以使用join()。
  • detach()就是分离,分离线程对象和被其管理的线程,用于允许线程独立地运行。如果线程需要在后台执行并且不需要等待其完成,可以使用detach()。

2.2.1 join示例

#include <thread>
#include <iostream>
using namespace std;

void workThread() {
	for (int i=0; i<10; ++i) {
		cout << i << endl;
	}
}

int main() {
	thread t1(workThread);
	t1.join(); //等待线程执行结束
	return 0;
}

g++编译

g++ main.cpp -lpthread

2.2.1 detach示例

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void workThread() {
	for (int i=0; i<5; ++i) {
		cout << i << endl;
		std::this_thread::sleep_for(1s);
	}
}

void buttonClick() {
	thread t(workThread);
	t.detach(); //不需要阻塞当前函数,线程对象t马上要析构了,分离对象和正在运行的线程,让其在后台运行。
}

int main() {
	buttonClick();
	for (int i=0; i<10; ++i) {
		std::this_thread::sleep_for(1s);
	}
	cout << "程序结束" << endl;
	return 0;
}

g++编译

g++ main.cpp -lpthread

2.3 this_thread

this_thread 是 C++ 标准库中 <thread> 头文件中的一个命名空间,它提供了与当前线程相关的函数。这些函数主要用于执行与当前线程相关的操作,如获取当前线程的 ID、比较两个线程的执行顺序等。

2.3.1 get_id

返回当前线程的唯一标识符。这个 ID 是一个 std::thread::id 对象,可以用于比较两个线程的执行顺序或检查线程是否已经终止。

#include <iostream>
#include <thread>
using namespace std;
 
int main() {
	std::thread::id  id = std::this_thread::get_id();
	cout << "线程id=" << id << endl;
	return 0;
}

g++编译

g++ main.cpp -lpthread

2.3.2 sleep_for

使当前线程睡眠指定的时间段。这个函数接受一个时间间隔(以 std::chrono 库中的时间单位表示),使线程在指定的时间段内处于休眠状态。

#include <iostream>
#include <thread>
using namespace std;

int main() {
	for (int i = 0; i < 5; ++i) {
		cout << i << endl;
		this_thread::sleep_for(chrono::seconds(1)); // 线程休眠1秒
		//this_thread::sleep_for(1s); 也可以这样写
	}
	return 0;
}

g++编译

g++ main.cpp -lpthread

2.3.3 sleep_until

使当前线程在指定的时间点之前一直处于休眠状态。这个函数接受一个时间点(以 std::chrono 库中的时间单位表示),使线程在该时间点之前一直处于休眠状态。

#include <iostream>
#include <thread>
#include <ctime>
#include <iomanip>
using namespace std;

int main() {
	std::time_t t = std::time(nullptr);
	std::tm tm = *std::localtime(&t);
	cout << "开始 " << std::put_time(&tm, "%H:%M:%S") << endl;
	
	auto sleep_time = chrono::system_clock::now() 
		+ std::chrono::seconds(3);
	this_thread::sleep_until(sleep_time); // 线程在3秒后醒来

	t = std::time(nullptr);
	tm = *std::localtime(&t);
	cout << "结束 " << std::put_time(&tm, "%H:%M:%S") << endl;
	return 0;
}

g++编译

g++ main.cpp -lpthread

2.3.4 yield

提示操作系统当前线程愿意放弃其 CPU 时间片,以便其他线程可以运行。这个函数并不保证其他线程会立即获得执行机会,但它可以用于优化多线程应用程序的性能。

#include <chrono>
#include <iostream>
#include <thread>

using namespace std;
using namespace chrono;

void sleep(seconds s)
{
	auto start = system_clock::now();
	auto end = start + s;
	do
	{
		this_thread::yield(); //放弃其 CPU 时间片
	} while (system_clock::now() < end);
}

int main()
{
	auto start = system_clock::now();

	sleep(chrono::seconds(10));

	auto elapsed = chrono::system_clock::now() - start;
	std::cout << "等待"
		<< std::chrono::duration_cast<seconds>(elapsed).count()
		<< "s" << endl;;
	return 0;
}

g++编译

g++ main.cpp -lpthread

3. mutex

std::mutex 是 C++ 标准库中的一个类,用于实现互斥锁(Mutex)机制。互斥锁是一种常用的同步原语,用于保护共享数据,防止多个线程同时访问和修改同一资源,造成数据的不一致或竞争条件。

主要功能和特点:

  • 互斥访问: std::mutex 用于确保一次只有一个线程可以访问受保护的代码区域。当一个线程进入临界区(critical section)时,它会锁定互斥锁,防止其他线程进入。
  • 条件变量: 与 std::mutex 一起使用的还有 std::condition_variable,用于实现更复杂的同步操作。条件变量允许线程等待某个条件成立时再继续执行。
  • 锁的层级: std::mutex 是一个基本的互斥锁,但它还有更高级的版本,如 std::recursive_mutex(允许同一线程多次获取锁)和 std::timed_mutex(支持超时获取锁)。
  • 异常安全: std::mutex 类具有异常安全性,意味着在异常发生时,锁的行为是确定的,不会导致数据不一致或资源泄漏。
  • 可重入性: 通常,如果一个线程已经拥有了一个互斥锁,那么它不能再次获取同一个锁。但是,通过使用 std::recursive_mutex,可以允许同一个线程多次获取同一把锁。
  • 死锁避免: std::mutex 实现了死锁避免机制。当一个线程尝试获取一个已经被其他线程持有的锁时,它会被阻塞,直到该锁被释放。
  • 跨平台: std::mutex 是标准化的,因此在不同的操作系统和平台上都可以使用。

3.1 mutex

用于确保一次只有一个线程可以访问受保护的代码区域。当一个线程进入临界区(critical section)时,它会锁定互斥锁,防止其他线程进入。

#include <iostream>
#include <map>
#include <string>
#include <mutex>
#include <thread>
#include <chrono>
using namespace std;
using namespace chrono;

map<string, string> g_tasks;
mutex g_tasks_mutex;

void runTask(const std::string& task)
{
	this_thread::sleep_for(seconds(2));
	string res = "done";

	g_tasks_mutex.lock();   //加锁
	g_tasks[task] = res;
	g_tasks_mutex.unlock();//解锁
}

int main()
{
	std::thread t1(runTask, "任务1");
	std::thread t2(runTask, "任务2");
	std::thread t3(runTask, "任务3");
	t1.join();
	t2.join();
	t3.join();

	for (const auto& pair : g_tasks)
		cout << pair.first << " => " << pair.second << '\n';
	return 0;
}
g++ main.cpp -lpthread

执行结果:
在这里插入图片描述

3.2 timed_mutex

与 std::mutex 相比,std::timed_mutex 允许线程尝试获取锁,但只等待指定的时间段。如果在指定的时间内无法获取锁,则尝试获取锁的操作将返回,并且线程可以继续执行其他任务或尝试再次获取锁。

std::timed_mutex 提供了一种更灵活的同步机制,允许线程在等待获取锁时进行其他工作,而不是简单地阻塞等待。这可以提高程序的效率和响应性。

#include <iostream>  
#include <thread>  
#include <chrono>  
#include <mutex>

using namespace std;
using namespace chrono;

timed_mutex mtx; // 全局定时互斥锁  

void print(int n, char c) {
	auto timeout = seconds(1); // 设置超时时间为1秒  
	if (mtx.try_lock_for(timeout)) { // 尝试在1秒内获取锁  
		for (int i = 0; i < n; ++i) { cout << c; }
		cout << endl;
		mtx.unlock(); // 解锁互斥锁  
	}
	else {
		cout << "Timeout occurred!\n";
	}
}

int main() {
	thread th1(print, 50, '*');
	thread th2(print, 50, '$');

	th1.join();
	th2.join();
	return 0;
}
g++ main.cpp -lpthread

3.3 recursive_mutex

与 std::mutex 相比,std::recursive_mutex 允许同一线程多次获取同一把锁,而不会导致死锁。

在许多情况下,当一个线程需要保护共享数据时,它会使用互斥锁来确保一次只有一个线程可以访问受保护的代码区域。然而,如果一个线程已经拥有了一个互斥锁,并且再次尝试获取同一个锁,那么会导致死锁。为了避免这种情况,可以使用 std::recursive_mutex。

主要功能和特点:

  • 可重入性: std::recursive_mutex 允许同一线程多次获取同一把锁。这意味着如果一个线程已经拥有了一个互斥锁,它可以再次获取同一个锁而不会导致死锁。
  • 正常互斥锁兼容性: 尽管 std::recursive_mutex 可以被同一个线程多次获取,但它仍然遵循互斥锁的基本行为,即不允许其他线程在锁被持有时访问受保护的代码区域。
  • 性能: 由于可重入性可能会使线程在短时间内多次获取和释放同一把锁,因此 std::recursive_mutex 的性能可能略低于普通的 std::mutex。
  • 异常安全: std::recursive_mutex 类具有异常安全性,意味着在异常发生时,锁的行为是确定的,不会导致数据不一致或资源泄漏。
  • 跨平台: std::recursive_mutex 是标准化的,因此在不同的操作系统和平台上都可以使用。
#include <iostream>  
#include <thread>   
#include <mutex>

using namespace std; 

recursive_mutex mtx; // 全局可重入互斥锁  

void print(int n, char c) {
	mtx.lock(); // 锁定互斥锁  
	for (int i = 0; i < n; ++i) { cout << c; }
	cout << '\n';
	mtx.unlock(); // 解锁互斥锁  
}

void workThread() {
	for (int i = 0; i < 5; ++i) {
		print(10, '*'); // 可重入地锁定互斥锁  
	}
}

int main() {
	thread th1(workThread);
	thread th2(workThread);

	th1.join();
	th2.join();

	return 0;
}
g++ main.cpp -lpthread

3.4 recursive_timed_mutex

它提供了可重入的、基于时间的互斥锁(Recursive Timed Mutex)机制。与 std::recursive_mutex 相比,std::recursive_timed_mutex 提供了基于时间的锁定,允许线程尝试获取锁,但只等待指定的时间段。

主要功能和特点:

  • 可重入性: 与 std::recursive_mutex 一样,std::recursive_timed_mutex 允许同一线程多次获取同一把锁,而不会导致死锁。
  • 时间限制: std::recursive_timed_mutex 提供了基于时间的锁定,允许线程尝试获取锁,但只等待指定的时间段。如果锁在指定的时间内不可用,则尝试获取锁的操作将返回。
  • 获取锁: 线程可以使用 try_lock_for() 或 try_lock_until() 函数尝试获取锁。前者接受一个时间段作为参数,后者接受一个时间点作为参数。如果锁在指定的时间内可用,则函数返回 true,否则返回 false。
  • 异常安全: std::recursive_timed_mutex 类具有异常安全性,意味着在异常发生时,锁的行为是确定的,不会导致数据不一致或资源泄漏。
  • 跨平台: std::recursive_timed_mutex 是标准化的,因此在不同的操作系统和平台上都可以使用。
#include <iostream>  
#include <thread>  
#include <chrono>  
#include <mutex>  
using namespace std;
using namespace chrono;

recursive_timed_mutex mtx; // 全局可重入定时互斥锁  

void print(int n, char c) {
	auto timeout = seconds(1); // 设置超时时间为1秒  
	if (mtx.try_lock_for(timeout)) { // 尝试在1秒内获取锁  
		for (int i = 0; i < n; ++i) { cout << c; }
		cout << '\n';
		mtx.unlock(); // 解锁互斥锁  
	}
	else {
		cout << "Timeout occurred!\n";
	}
}

void workThread() {
	for (int i = 0; i < 5; ++i) {
		print(10, '*'); // 可重入地尝试在1秒内锁定互斥锁  
	}
}

int main() {
	thread th1(workThread);
	thread th2(workThread);

	th1.join();
	th2.join();

	return 0;
}
g++ main.cpp -lpthread

3.5 lock_guard

用于管理互斥锁(mutex)的生命周期,以实现线程同步。它是一个RAII(Resource Acquisition Is Initialization)包装器,用于自动管理互斥锁的锁定和解锁操作。

主要特点是它会在构造时自动锁定互斥锁,并在析构时自动解锁互斥锁。这样可以确保即使在异常情况下,互斥锁也能被正确地解锁,从而避免死锁和资源泄漏。

#include <iostream>  
#include <thread>  
#include <mutex>  
using namespace std;

mutex mtx; // 全局互斥锁  

void print(int n, char c) {
	lock_guard<mutex> lock(mtx); // 自动锁定互斥锁  
	for (int i = 0; i < n; ++i) { cout << c; }
	cout << '\n';
	// 在lock_guard对象离开作用域时,互斥锁会自动解锁  
}

void workThread() {
	for (int i = 0; i < 5; ++i) {
		print(10, '*'); // 自动锁定和解锁互斥锁  
	}
}

int main() {
	thread th1(workThread);
	thread th2(workThread);

	th1.join();
	th2.join();

	return 0;
}
g++ main.cpp -lpthread

3.6 uniqure_lock

它提供了比 std::lock_guard 更灵活的互斥锁管理功能。std::unique_lock 提供了更细粒度的控制,允许手动锁定和解锁互斥锁,同时还提供了定时锁定的功能。

主要特点如下:

  • 灵活性:与 std::lock_guard 相比,std::unique_lock 提供了更多的控制。你可以手动锁定和解锁互斥锁,而不是依赖于构造和析构函数。
  • 可移动性:std::unique_lock 可以被移动,允许更高效的资源管理。
  • 可与条件变量配合使用:由于 std::unique_lock 提供了手动锁定和解锁的功能,它可以与 std::condition_variable 配合使用,实现更复杂的线程同步操作。
  • 定时锁定:std::unique_lock 提供了 try_lock_for() 和 try_lock_until() 函数,允许线程尝试在指定的时间段或时间点内获取锁。
#include <iostream>  
#include <thread>  
#include <mutex>  
#include <chrono>  
using namespace std;
using namespace chrono;

mutex mtx; // 全局互斥锁  

void print(int n, char c) {
	unique_lock<mutex> lock(mtx); // 手动锁定互斥锁  
	for (int i = 0; i < n; ++i) { cout << c; }
	cout << '\n';
	// 手动解锁互斥锁  
	lock.unlock();
}

void workThread() {
	for (int i = 0; i < 5; ++i) {
		print(10, '*'); // 手动锁定和解锁互斥锁  
	}
}

int main() {
	thread th1(workThread);
	thread th2(workThread);

	th1.join();
	th2.join();

	return 0;
}
g++ main.cpp -lpthread

4. atomic

4.1 atomic

std::atomic用于提供原子的数据操作。原子操作是一种不可中断的操作,即一旦开始,就会从头到尾执行完毕,不会被其他线程或操作中断。提供了一种在多线程环境中安全访问数据的方式,通过确保操作的原子性来避免数据竞争和不一致的状态。

std::atomic 可以用于各种数据类型,包括内置类型(如 int、char 等)和自定义类型。使用 std::atomic 包装的数据类型可以以线程安全的方式进行读取和写入操作。比如:atomic_bool、atomic_char、atomic_int等待。

#include <iostream>  
#include <thread>  
#include <atomic>  
using namespace std;

atomic_int g_data = 0; // 原子整数  

void add() {
	for (int i = 0; i < 1000; ++i) {
		++g_data; // 原子增加操作  
	}
}

int main() {
	std::thread t1(add);
	std::thread t2(add);
	t1.join();
	t2.join();
	std::cout << "结果: " << g_data << std::endl;
	return 0;
}
g++ main.cpp -lpthread

4.2 atomic_flag

std::atomic_flag 是 C++ 标准库 头文件中定义的最简单的原子类型。它是一个布尔类型的原子变量,但不同于 std::atomic,std::atomic_flag 不提供加载(load)或存储(store)操作,并且保证是免锁的(lock-free)。

std::atomic_flag 只有两种状态:设置(set)和清除(clear),对应着 true 和 false。它不能被拷贝,也不能进行移动赋值。std::atomic_flag 的对象必须用 ATOMIC_FLAG_INIT 宏进行初始化,这会将标志初始化为清除状态(在 C++20 之前)。在 C++20 及以后,std::atomic_flag 的默认构造函数会将其初始化为清除状态,因此 ATOMIC_FLAG_INIT 不再必需且已被弃用。

对于 std::atomic_flag,你可以执行的操作有限,主要包括:

  • 使用默认构造函数或 ATOMIC_FLAG_INIT 初始化。
  • 使用 clear() 成员函数清除标志。
  • 使用 test_and_set() 成员函数尝试设置标志,并返回它之前的值。
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
using namespace std;

// 在 C++20 前需要使用 ATOMIC_FLAG_INIT 初始化 
atomic_flag lock = ATOMIC_FLAG_INIT;

void workThread(int n)
{
	for (int cnt = 0; cnt < 40; ++cnt)
	{
		while (lock.test_and_set(memory_order_acquire)) //获取锁
		{
			;
		}
		static int out{};
		cout << n << ((++out % 40) == 0 ? '\n' : ' ');
		lock.clear(memory_order_release); //释放锁
	}
}

int main()
{
	vector<thread> v;
	for (int n = 0; n < 10; ++n) {
		v.emplace_back(workThread, n);
	}
	
	//等待所有线程
	for (auto& t : v) {
		t.join();
	}
	return 0;
}
g++ main.cpp -lpthread

5. condition_variable

5.1 condition_variable

用于线程间的同步。它允许一个或多个线程等待某个条件成立,而其他线程可以通知这些等待的线程条件已经改变。

std::condition_variable 通常与互斥锁(如 std::mutex)一起使用,以实现更复杂的同步操作。

#include <iostream>  
#include <thread>  
#include <mutex>  
#include <condition_variable>  
using namespace std;

mutex mtx; // 互斥锁  
condition_variable cv; // 条件变量  
bool ready = false; // 初始条件  

void print_id(int id) {
	unique_lock<mutex> lck(mtx); // 锁定互斥锁  
	while (!ready) { // 等待条件成立  
		cv.wait(lck); // 等待通知  
	}
	// 条件成立,执行任务  
	cout << "thread " << id << '\n';
}

void go() {
	unique_lock<mutex> lck(mtx); // 锁定互斥锁  
	ready = true; // 改变条件  
	cv.notify_all(); // 通知所有等待的线程  
}

int main() {
	thread v[10];
	// 启动多个线程等待条件成立  
	for (int i = 0; i < 10; ++i) {
		v[i] = thread(print_id, i);
	} 

	cout << "开始...\n";
	go(); 

	for (auto& t : v) t.join();
	return 0;
}
g++ main.cpp -lpthread

5.2 condition_variable_any

它是 std::condition_variable 的泛化版本。与 std::condition_variable 相比,std::condition_variable_any 的优点在于它可以与任何满足基本可锁定(BasicLockable)要求的锁类型一起工作,而不仅仅是 std::unique_lock<std::mutex>。

这意味着 std::condition_variable_any 可以与自定义的互斥锁类型或其他满足基本锁定要求的锁类型一起使用,提供了更大的灵活性。

#include <iostream>  
#include <thread>  
#include <mutex>  
#include <condition_variable>  
using namespace std;

mutex mtx; // 互斥锁  
condition_variable_any cv; // 条件变量  
bool ready = false; // 初始条件  

void print_id(int id) {
	unique_lock<mutex> lck(mtx); // 锁定互斥锁  
	while (!ready) { // 等待条件成立  
		cv.wait(lck); // 等待通知  
	}
	// 条件成立,执行任务  
	cout << "thread " << id << '\n';
}

void go() {
	unique_lock<mutex> lck(mtx); // 锁定互斥锁  
	ready = true; // 改变条件  
	cv.notify_all(); // 通知所有等待的线程  
}

int main() {
	thread v[10];
	// 启动多个线程等待条件成立  
	for (int i = 0; i < 10; ++i)
		v[i] = thread(print_id, i);

	cout << "开始...\n";
	go();  

	for (auto& th : v) th.join();

	return 0;
}
g++ main.cpp -lpthread

6. future

6.1 promise

std::promise 是 C++11 标准库中提供的一个模板类,用于实现多线程间的异步通信。它通常与 std::future 一起使用,以实现一个线程向另一个线程传递值或异常。

std::promise 的泛型参数 ResultType 表示 std::promise 对象保存的值的类型。例如,std::promise 表示该对象将保存一个整数值。在构造 std::promise 对象时,该对象会与一个新的共享状态(shared state)关联。调用 get_future 方法后,std::promise 对象会返回一个与该共享状态相关联的 std::future 对象。这样,一个线程可以通过 std::promise 设置共享状态的值或异常,而另一个线程则可以通过相应的 std::future 对象来获取这个值或异常。

在实际应用中,通常的做法是在一个线程中创建 std::promise 对象,并通过该对象获取一个 std::future 对象。然后,将 std::future 对象传递给另一个线程,而将 std::promise 对象保留在原始线程中。原始线程可以在某个时刻通过调用 std::promise::set_value 或 std::promise::set_exception 方法来设置共享状态的值或异常。接收线程则可以通过调用 std::future::get 方法来获取该值或异常。

需要注意的是,每个 std::promise 对象只能设置一次值或异常,并且只能与一个共享状态相关联。一旦设置了值或异常,就不能再次修改它。此外,如果接收线程在原始线程设置值或异常之前调用了 std::future::get 方法,那么接收线程将会被阻塞,直到原始线程设置了值或异常为止。

总的来说,std::promise 和 std::future 配合使用提供了一种灵活且高效的方式来实现多线程间的异步通信和数据共享。

6.2 future

std::future 是 C++11 标准库(并发支持库)中的一个模板类,它表示一个异步操作的结果。在多线程编程中使用异步任务时,std::future 能够帮助我们在需要的时候获取任务的执行结果。

std::future 的一个重要特性是能够阻塞当前线程,直到异步操作完成,从而确保我们在获取结果时不会遇到未完成的操作。通常,std::future 对象由某个 Provider(例如 std::async 函数)创建,Provider 在某个线程中设置共享状态的值,与该共享状态相关联的 std::future 对象调用 get(通常在另外一个线程中)获取该值。如果共享状态的标志不为 ready,则调用 std::future::get 会阻塞当前的调用者,直到 Provider 设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get 返回异步任务的值或异常(如果发生了异常)。

需要注意的是,一个有效的 std::future 对象通常由以下三种 Provider 创建,并和某个共享状态相关联:

  • std::async 函数:这是一个模板函数,用于异步调用函数。在某个时候以参数作为参数(可变长参数)调用 Fn,无需等待 Fn 执行完成就可返回,返回结果是个 std::future 对象。Fn 返回的值可通过 std::future 对象的 get 成员函数获取。一旦完成 Fn 的执行,共享状态将包含 Fn 返回的值并 ready。
  • std::future 接口:这是 std::future 的构造函数,禁用拷贝构造,支持移动构造。
  • std::thread:这是一个类,用于支持线程的操作。
#include <future>
#include <iostream>
#include <thread>
using namespace std;

int main()
{
	packaged_task<int()> task([] { return 7; });
	future<int> f1 = task.get_future();
	thread t(move(task));

	future<int> f2 = async(launch::async, [] { return 8; });

	promise<int> p;
	future<int> f3 = p.get_future();
	thread([&p] { p.set_value_at_thread_exit(9); }).detach();

	cout << "开始..." << flush;
	f1.wait();
	f2.wait();
	f3.wait();
	cout << "完成\n: "
		<< f1.get() << ' ' 
		<< f2.get() << ' ' 
		<< f3.get() << '\n';
	t.join();
	return 0;
}
g++ main.cpp -lpthread

6.3 shared_future

std::shared_future 是 C++11 标准库中的一个类,它提供了一种访问异步操作结果的机制,与 std::future 类似,但允许多个线程共享同一个状态。这意味着多个 std::shared_future 对象可以指向同一个共享状态,并且当该共享状态的值或异常被设置时,所有指向该状态的 std::shared_future 对象都会被通知。

std::shared_future 的一个重要特性是它能够让多个线程访问同一共享状态,而不会阻塞等待异步任务的完成。与 std::future 不同,std::shared_future 可以被复制,并且多个 std::shared_future 对象可以指向同一共享状态。这意味着每个线程都可以通过其自己的 std::shared_future 对象副本访问共享状态,从而在多个线程之间安全地共享状态。

std::shared_future 可以通过隐式转换或显示调用 std::future::share() 方法从 std::future 对象获得。在两种情况下,原 std::future 对象都将变得无效。共享状态的生存期至少要持续到与之关联的最后一个对象被销毁为止。与 std::future 不同,通过 std::shared_future::get() 检索的值不会释放共享对象的所有权。

总的来说,std::shared_future 是用于处理并发编程中异步操作结果的强大工具,它允许多个线程共享同一状态,并提供了一种安全的方式来访问和操作这些状态。

#include <chrono>
#include <future>
#include <iostream>
using namespace std;

int main()
{
	promise<void> ready_promise, t1_ready_promise, t2_ready_promise;
	shared_future<void> ready_future(ready_promise.get_future());

	chrono::time_point<chrono::high_resolution_clock> start;

	auto fun1 = [&, ready_future]() -> chrono::duration<double, milli>
	{
		t1_ready_promise.set_value();
		ready_future.wait();
		return chrono::high_resolution_clock::now() - start;
	};


	auto fun2 = [&, ready_future]() -> chrono::duration<double, milli>
	{
		t2_ready_promise.set_value();
		ready_future.wait();
		return chrono::high_resolution_clock::now() - start;
	};

	auto fut1 = t1_ready_promise.get_future();
	auto fut2 = t2_ready_promise.get_future();

	auto result1 = async(launch::async, fun1);
	auto result2 = async(launch::async, fun2);

	fut1.wait();
	fut2.wait();

	start = chrono::high_resolution_clock::now();

	ready_promise.set_value();
	cout << "线程1 "
		<< result1.get().count() << " ms 后开始\n"
		<< "线程2 "
		<< result2.get().count() << " ms 后开始\n";
	return 0;
}
g++ main.cpp -lpthread

6.4 packaged_task

std::packaged_task 是 C++11 标准库中的一个模板类,用于实现多线程间的异步任务执行。它包装了一个可调用的对象(例如函数、lambda表达式、bind表达式或函数对象),并允许异步获取该可调用对象产生的结果。

std::packaged_task 内部包含了两个基本元素:被包装的任务(stored task)和共享状态(shared state)。被包装的任务是一个可调用的对象,如函数指针、成员函数指针或函数对象。共享状态用于保存任务的返回值,可以通过 std::future 对象来异步访问共享状态。

当 std::packaged_task 对象被调用时,与其关联的函数或可调用对象被执行。执行结束后,std::future 对象变为 ready 状态,并保存相关结果。传递给 std::packaged_task 的模板参数是函数签名,如 void() 或 int(std::string&, double*)。构造对象时传入的函数或可调用对象的参数和返回值必须与签名中对应的类型一致。

需要注意的是,std::packaged_task 对象本身并不直接提供线程安全机制。它只是将任务封装为一个可调用的对象,并在执行时将结果存储在共享状态中。要实现线程安全,需要在使用 std::packaged_task 时结合使用其他同步机制,如互斥锁或条件变量。

总的来说,std::packaged_task 是一个方便的类,用于封装异步任务并在多个线程间共享结果。它提供了一种灵活且高效的方式来实现多线程间的异步任务执行和数据共享。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

五轮车

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值