C++11多线程

线程

​ 在C++11之前,C/C++一直是一种顺序的编程语言。顺序是指所有指令都是串行执行的,即在相同的时刻,有且仅有单个CPU的程序计数器执行代码的代码段,并运行代码段中的指令。而C/C++代码也总是对应地拥有一份操作系统赋予进程的包括堆、栈、可执行的(代码)及不可执行的(数据)在内的各种内存区域。

​ 在C++11中,一个相当大的变化就是引入了多线程的支持。这使得C/C++语言在进行线程编程时,不必依赖第三方库。

线程创建

​ 利用 std::thread threadX 创建线程,只需提供线程函数或函数对象即可,并且可以同时指定线程函数的参数。

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

void func1() {
	while (1) {
		cout << __func__ << endl;
	}
}

void func2() {
	while (1) {
		cout << __func__ << endl;
	}
}

void func3(int a, char b, const char* str) {
	while (1) {
		cout << a << " " << b << " " << str << endl;
	}
}

int main() {
	int test = 0;
	thread t1(func1);
	thread t2(func2);
	// 创建线程t3绑定线程函数func3, 并传入线程函数参数
	thread t3(func3,10, 'H', "nice to meet you"); 

	return 0;
}

回收线程资源

​ std::thread::join 等待线程结束(此函数会阻塞)才继续后面的代码运行,并回收线程资源,如果线程函数有返回值,返回值将被忽略。用该函数会阻塞当前线程,直到由 *this 所标示的线程执行完毕 join 才返回。

​ std::thread::joinable() 返回一个布尔值,判断该线程是否可以调用 join 或 detach。

​ std::ref(arg) 可以利用 ref 将 临时变量转化为引用类型变量

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

void pause_thread(int n) {
	// 指定当前线程休眠一定的时间
	this_thread::sleep_for(chrono::seconds(n));
	cout << "pause of " << n << " seconds ended\n";
}

int main() {
	cout << "spawning 3 threads...\n";
	thread t1(pause_thread, 1);
	thread t2(pause_thread, 2);
	thread t3(pause_thread, 4); // 休眠4秒

	cout << "Done spawing threads.Now waiting for them to joinL\n";
	t1.join(); // 等待线程结束(此函数会阻塞)
	t2.join();
	t3.join();
	cout << "All threads joined!\n";

	return 0;
}

​ std::thread::detach 如果不希望线程被阻塞执行,可以调用线程的detach,将线程和线程对象分离,让线程作为后台线程去执行,调用该线程的主线程不用阻塞等待该线程,继续执行自己的代码。需要注意的是,detach之后就无法再和线程发生联系了,比如 deach 之后就不能在通过 join 来等待执行完,线程何时执行完 我们也无法控制。

int main() {
	cout << "spawning and detaching 3 threads...\n";
	thread(pause_thread, 1).detach();
	thread(pause_thread, 2).detach();
	thread(pause_thread, 4).detach();
	cout << "Done spawning threads\n";
	cout << "the main thread will now pause for 6 seconds \n";

	pause_thread(5);

	return 0;
}

​ 利用 get_id() 和 thread::hardware_concurrency() 获取线程ID和CPU核心数

void func1() {
	this_thread::sleep_for(chrono::seconds(1)); //休眠1秒
	cout << "func id = " << this_thread::get_id() << endl; // 获取线程ID
}

int main() {
	thread t(func1);
	// 线程t的ID
	cout << "t.get_id()= " << t.get_id() << endl; 
	//主线程ID
	cout << "main id =" << this_thread::get_id() << endl;
	// 获取CPU核心数,失败返回0
	cout << "CPU num= " << thread::hardware_concurrency() << endl;

	t.join();	// 线程阻塞

	return 0;
}

互斥量 mutex

​ 为什么需要互斥量?

​ 在多线程中共享数据时,需要注意线程安全问题。如果多个线程同时访问同一个变量,并且其中至少有一个线程对该变量进行了写操作,那么就会出现数据竞争问题,数据竞争会导致程序奔溃或者产生错误的结果。为了避免数据竞争问题,需要使用同步机制确保多个线程之间对共享数据的访问是安全的。常见的同步机制包括互斥量 mutex,条件变量 condition_variable,原子操作 automic。

void printer(const char* str) {
	while (*str != '\0') {
		cout << *str;
		str++;
		this_thread::sleep_for(chrono::seconds(1));
	}
	cout << endl;
}

void func1() {
	const char* str = "hello";
	printer(str);
}

void func2() {
	const char* str = "world";
	printer(str);
}

int main() {
	thread t1(func1);
	thread t2(func2);
	t1.join();
	t2.join();

	//whoelrlldo 输出内容 无顺序可言
	return 0;
}

thread 提供了四种不同的互斥量:

  • std::mutex :独占式互斥量 。独占互斥量加解锁是成对的,同一个线程内独占式互斥量在没有解锁的情况下,再次对其加锁是不正确的,会得到一个未定义的行为。
  • std::recursive_mutex :递归式互斥量 。递归式互斥量是在同一个线程内互斥量没有解锁的情况下可以再次对其进行加锁,但其加解锁的次数需要保持一致。这种互斥量平时用的比较少。
  • std::timed_mutex :允许超时的独占式互斥量
  • std::recursize_timed_mutex :允许超时的递归式互斥量
lock()&unlock()

独占互斥量,互斥量的接口都类似,一般用法通过 lock() 方法来阻塞线程,知道获得互斥量的所有权为止。在线程获得互斥量并完成任务后,就必须通过 unlock() 来解除对互斥量的占用, lock() 和 unlock() 必须成对出现。try_lock() 尝试锁定互斥量,成功返回true,失败返回false,它是非阻塞的。

#include<mutex>
mutex g_lock;	// 全局互斥锁对象

void printer(const char* str) {
	g_lock.lock(); // 上锁
	while (*str != '\0') {
		cout << *str;
		str++;
		this_thread::sleep_for(chrono::seconds(1));
	}
	cout << " ";
	g_lock.unlock(); // 解锁
} 
// 输出 hello world 或者 world hello
lock_guard

​ 可以简化 lock/unlock 的写法,同时也更安全,因为 lock_guard 内部实现在构造时会自动锁定互斥量,而在退出作用域后进行析构时自动解锁,从而避免忘了 unlock 的操作

#include<mutex>
mutex g_lock;	// 全局互斥锁对象

void printer(const char* str) {
	lock_guard<mutex> locker(g_lock); // 自动上锁解锁
	while (*str != '\0') {
		cout << *str;
		str++;
		this_thread::sleep_for(chrono::seconds(1));
	}
	cout << " ";
} 

unique_lock 是 lock_grard 的升级版,不仅可以自动加锁和解锁,还可以延迟加锁,条件变量,超时等。同时作为代价,所占资源也多。

死锁

​ 两个线程同时对两个互斥量进行访问,但是两个互斥量所有权的获取顺序相斥,造成互相等待对方释放所有权的状态。

​ 比如线程 t1 和 t2,对两个互斥量 mtx1 和 mtx2 进行访问,而且需要按照一下顺序获取互斥量的所有权:

  • t1 先获取 mtx1 所有权(加锁),再获取 mtx2 的所有权。
  • t2 先过去 mtx2 所有权,再获取 mtx1 的所有权。

当两个进程同时执行的时候,会出现死锁的情况,因为 t1 获取了 mtx1,t2获取了 mtx2,t1 获取不到 mtx2,t2 获取不到 mtx1。都在等对方释放所有权,导致死锁。

解决方案:

两个线程都改成 先mtx1加锁,在mtx2加锁,这样其中一个线程执行完毕解锁后,其他进程再获取互斥量所有权执行。

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

mutex m1, m2;
void func1() {
	for (int i = 1; i < 100; i++) {
		m1.lock();
		m2.lock();
		m1.unlock();
		m2.unlock();
	}
}

void func2() {
	for (int i = 1; i < 100; i++) {
		m2.lock();
		m1.lock();
		m2.unlock();
		m1.unlock();
	}
}

int main() {
	thread t1(func1);
	thread t2(func2);
	t1.join();
	t2.join();

	cout << "over" << endl;
	return 0;
}

原子操作 atomic

​ 所谓原子操作,取的就是"原子是最小的,不可分割的最小个体"的意义,它表示在多个线程访问同一个全局资源的时候,能够确保所有其他的线程都不在同一时间内访问相同的资源。也就是确保了在同一时刻只有唯一的线程对这个资源进行访问。这有点类似互斥对象对共享资源的访问的保护。但是原子操作更加接近底层,因此效率更高。

​ 由于线程间对数据的竞争儿导致每次运行的结果都不一样,因此,为了防止数据竞争问题,我们需要对其进行原子操作。通过互斥锁进行原子操作:

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

long total = 0;
mutex g_lock;

void func1() {
	for (int i = 0; i < 10000; ++i) {
		//g_lock.lock();
		total += 1;
		//g_lock.unlock();
	}
}

int main() {
	clock_t start = clock(); // 获得cpu处理此刻时间 计时开始

	// 线程
	thread t1(func1);
	thread t2(func1);

	t1.join();
	t2.join();

	clock_t end = clock();	// 计时结束
	
	// 两个线程各运行一次 total应该是20000,但是不加锁 会导致资源竞争, total<20000
	cout << "total=" << total << endl;
	cout << "time=" << end - start << "ms" << endl;	// 4ms

	return 0;
}

​ C++11新标准,引入了原子操作的概念,将共享资源定义为原子类型,std::atomic是标准库中一个模板类。如果我们在多个线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的,也就是说,确保任意时刻只有一个线程对这个资源进行访问,编译器将保证多个线程访问这个共享资源你的正确性。从而避免了锁的使用,提高了效率。原子操作的实现跟普通数据类型类似,但是他能够在保证结果正确的前提下,提供比 mutex 等锁机制更好的性能,运行时间更短。

#include<atomic>
atomic<long> total = { 0 };
void func1() {
	for (int i = 0; i < 10000; ++i) {
		total += 1;
	}
}
// 结果  20000  3ms  运行效率更高了

​ atomicX.load() 加载到当前线程的本地缓存中,并返回这个值。相当于打印

​ atomicX.store(val) 修改atomicX的值成val

条件变量condition_variable

​ 条件变量使用场景

  1. 创建一个 condition_variable 对象。
  2. 创建一个互斥锁 mutex对象,用来保护共享资源的访问。
  3. 在需要等待条件变量的地方,用 unique_lock 对象锁定互斥锁,并调用 condition_variable::wait(), condition_variable::wait_for() 或 condition_variable::wait_util()函数等待条件变量。
  4. 在其他线程中需要通知等待的线程时,调用 condition_variable::notify_one() 或 condition_variable::notify_all() 函数通知等待的线程停止等待
#include<iostream>
#include<thread>
#include<mutex>
#include<string>
#include<condition_variable>
#include<queue>
#include<chrono>
using namespace std;

queue<int> g_queue;	// 任务队列
condition_variable g_cv;
mutex mtx; // 同时添加和取任务时,产生数据竞争,需要加锁

// 生产者添加任务
void producter() {
	for (int i = 0; i < 10; i++) {
		unique_lock<mutex> locker(mtx);
		g_queue.push(i);
		g_cv.notify_one(); // 加任务通知消费者取任务 不用等待了
		cout << "producter:" << i << endl;
	}
	this_thread::sleep_for(chrono::microseconds(100));
}

// 消费者取任务
void consumer() {
	while (1) {
		unique_lock<mutex> locker(mtx);
		// 队列为空时,需要利用条件变量进行阻塞等待wait,不为空时,不用等待
		g_cv.wait(locker, []() {return !g_queue.empty(); });
		int value = g_queue.front();
		g_queue.pop();
		cout << "consumer:" << value << endl;
	}
}

int main() {
	thread t1(producter);
	thread t2(consumer);
	t1.join();
	t2.join();

	return 0;
}

线程池

​ 为什么要使用线程池,因为线程的开辟和销毁都是十分消耗资源的,利用线程池提前开辟好线程等待任务,提高了运行效率。

​ 概述,建立线程数组,从任务队列中取任务执行

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<queue>
#include<chrono>	
#include<vector>
#include<functional>
using namespace std;

class ThreadPool {
private:
	vector<thread> threads;
	queue<function<void()>> tasks;	// 任务队列 存放任务
	mutex mtx;	// 互斥锁 解决数据竞态
	condition_variable condition;	// 条件变量 管理生产者消费者模型
	bool stop; // 记录线程池终止

public:
	// 构造函数指定开辟的线程数
	ThreadPool(int numThreads) :stop(false) { // stop默认为false
		for(int i = 0; i < numThreads; i++){
			//threads.push_back(this); // push_back 不支持线程 只能用 emplace_back
			threads.emplace_back([this] {	// lambda表达式
				while (1) {	// 线程不能终止 需要一直循环
					unique_lock<mutex> lock(mtx);
					condition.wait(lock, [this] {
						return !tasks.empty() || stop;	// 任务队列为空或者线程终止 则阻塞等待
						});

					if (stop && tasks.empty()) {
						return;	// 终止
					}
					// 取任务
					function<void()> task(move(tasks.front()));
					tasks.pop(); // 拿出
					lock.unlock();
					task();	// 完成任务
				}
				});
		}
	}
	~ThreadPool() {
		{
			unique_lock<mutex> lock(mtx);
			stop = true;
		}
		condition.notify_all(); // 线程要停止了 通知所有所有线程将任务队列里的任务取完
		for (auto& t : threads) {	// 遍历线程数组 全部阻塞 等待线程结束
			t.join();
		}
	}

	template<class F, class...Args>	// 可变参数模板
	// 手动添加任务
	void enqueue(F&& f, Args&&...args) {	// 右值引用即万能引用
		function<void()> task = bind(forward<F>(f), forward<Args>(args)...);
		{ // 指定锁的范围
			unique_lock<mutex> lock(mtx);
			tasks.emplace(move(task));	// 加任务
		}
		condition.notify_one(); // 通知线程去完成
	}

};

int main() {
	ThreadPool pool(4);
	for (int i = 0; i < 10; i++) {
		pool.enqueue([i] {
			cout << "task:" << i << " is running " << endl;
			this_thread::sleep_for(chrono::seconds(1));
			cout << "task:" << i << " is done " << endl;
			});
	}

	return 0;
}

/*
	总结:线程池需要维护两个东西,线程数组和任务队列,构造线程池类时,根据传入参数确定开辟的
	线程数组数量,每个线程循环地在在任务队列中取任务完成,提供一个接口enqueue供手动添加任务
	每次加任务的时候通过条件变量通知线程取任务完成。

*/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值