C++11多线程thread、互斥量、条件变量、原子变量

226 阅读9分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

前言

  本文重点理解:线程thread、互斥量mutex、条件变量condition。API Reference Document。文章Demo源码地址git gopherWxf

1. 线程thread

#include <thread> // 头文件
std::thread(...)

1.1 线程的构造函数

  1. 默认构造函数
//创建一个空的 thread 执行对象。
thread() _NOEXCEPT {
	// construct with no thread
	_Thr_set_null(_Thr);
}
  1. 带参构造函数
//创建std::thread执行对象,该thread对象可被joinable
//新产生的线程会调用threadFun函数,该函数的参数由 args 给出
template<class Fn, class... Args>
explicit thread(Fn&& fn, Args&&... args);
  1. 拷贝构造函数
// 拷贝构造函数(被禁用),意味着 thread 不可被拷贝构造。
thread(const thread&) = delete;
//Demo
thread t1;
thread t2 =t1; // 错误
  1. 移动构造函数
//move 移动构造函数,调用成功之后 x 不代表任何 thread 执行对象。
//注意:可被 joinable 的 thread 对象必须在他们销毁之前被主线程 join
//或者将其设置为detached。
thread(thread&& x)noexcept
//Demo
thread t1;
thread t2 =move(t1); // 可以
  1. 创建线程Demo演示
#include<iostream> 
#include<thread>
using namespace std;
void threadFun(int &a) { // 引用传递
	cout << "this is thread fun !" <<endl;
	cout <<" a = "<<(a+=10)<<endl;
}
int main() {
	int x = 10;
	//带参构造函数
	thread t1(threadFun, std::ref(x));//std::ref以显示引用的方式传参
	//移动构造函数
	thread t2(std::move(t1)); // t1 线程失去所有权
	//默认构造函数
	thread t3;
	//移动语义,走重载的=(&&)
	t3 = std::move(t2); // t2 线程失去所有权
	//t1.join(); // t1和t2都不能join,因为它们不管控任何线程,join会异常
	t3.join();
	cout<<"Main End "<<"x = "<<x<<endl;
	return 0;
}

1.2 线程的成员函数

  • get_id()
获取线程ID,返回类型std::thread::id对象。
http://www.cplusplus.com/reference/thread/thread/get_id/
  • joinable()
判断线程是否可以加入等待
http://www.cplusplus.com/reference/thread/thread/joinable/
  • join()
阻塞等该线程执行完成后才返回。
http://www.cplusplus.com/reference/thread/thread/join/
  • detach()   detach调用之后,目标线程就成为了守护线程,驻留后台运行,与之关联的std::thread对象失去对目标线程的关联,无法再通过std::thread对象取得该线程的控制权。当线程主函数执行完之后,线程就结束了,运行时库负责清理与该线程相关的资源。
调用 detach 函数之后:
- *this 不再代表任何的线程执行实例。
- joinable() == false
- get_id() == std::thread::id()
http://www.cplusplus.com/reference/thread/thread/detach/

1.3 线程的创建Demo

  第一个参数可以写&,也可以不写&,函数名本身就是一个地址。推荐还是写&,因为使用&有更好的编译器兼容性。有些编译器不支持没写&的方式。

std::thread t1(func1);
std::thread t1(&func1);

Demo

  1. 传入0个值
  2. 传入2个值
  3. 传入引用
  4. 传入类函数
  5. detach
  6. move
#include <iostream>
#include <thread> // 头文件
using namespace std;

// 1.传入0个值
void func1() {
	cout << "func1 into" << endl;
}
// 2.传入2个值
void func2(int a, int b) {
	cout << "func2 a + b = " << a+b << endl;
}
// 3.传入引用
void func3(int &c) { // 引用传递
	cout << "func3 c = " << &c << endl;
	c += 10;
}
//
class A {
	public:
		// 4.传入类函数
		void func4(int a) {
			std::this_thread::sleep_for(std::chrono::seconds(1));
			cout << "thread:" << name_<< ", fun4 a = " << a << endl;
		}
		void setName(string name) {
			name_ = name;
		}
		void displayName() {
			cout << "this:" << this << ", name:" << name_ << endl;
		}
		void play() {
			std::cout<<"play call!"<<std::endl;
		}
	private:
		string name_;
};
//5. detach
void func5() {
	cout << "func5 into sleep " << endl;
	std::this_thread::sleep_for(std::chrono::seconds(1));
	cout << "func5 leave " << endl;
}
// 6. move
void func6() {
	cout << "this is func6 !" <<endl;
}
int main() {
	// 1. 传入0个值
	cout << "\n\n main1--------------------------\n";
	std::thread t1(&func1); // 只传递函数
	t1.join(); // 阻塞等待线程函数执行结束

	// 2. 传入2个值
	cout << "\n\n main2--------------------------\n";
	int a =10;
	int b =20;
	std::thread t2(&func2, a, b); // 加上参数传递,可以任意参数
	t2.join();

	// 3. 传入引用
	cout << "\n\n main3--------------------------\n";
	int c =10;
	std::thread t3(&func3, std::ref(c)); // std::ref显示的传递引用
	t3.join();
	cout << "main3 c = " << &c << ", "<<c << endl;

	// 4. 传入类函数
	cout << "\n\n main4--------------------------\n";
	A * a4_ptr = new A();
	a4_ptr->setName("wxf");
	//a4_ptr相当于this指针
	std::thread t4(&A::func4, a4_ptr, 10);
	t4.join();
	delete a4_ptr;

	// 5.detach
	cout << "\n\n main5--------------------------\n";
	std::thread t5(&func5); // 只传递函数
	t5.detach(); // 脱离,脱离就不能join了
	cout << "\n main5 end\n";

	// 6.move
	cout << "\n\n main6--------------------------\n";
	int x = 10;
	thread t6_1(&func6);
	thread t6_2(std::move(t6_1)); // t6_1 线程失去所有权
	//t6_1.join(); // 抛出异常
	t6_2.join();
	return 0;
}
 main1--------------------------
func1 into


 main2--------------------------
func2 a + b = 30


 main3--------------------------
func3 c = 0x75fd7c
main3 c = 0x75fd7c, 20


 main4--------------------------
thread:wxf, fun4 a = 10


 main5--------------------------

 main5 end


 main6--------------------------
func5 into sleep
this is func6 !

1.4 线程的封装

  • zero_thread.h
#ifndef ZERO_THREAD_H
#define ZERO_THREAD_H

#include <thread>

class ZERO_Thread {
	public:
		ZERO_Thread(); // 构造函数
		virtual ~ZERO_Thread(); // 析构函数
		bool start();

		void stop();

		bool isAlive() const; // 线程是否存活.
		std::thread::id id() {
			return th_->get_id();
		}

		std::thread *getThread() {
			return th_;
		}

		void join();  // 等待当前线程结束, 不能在当前线程上调用
		void detach(); //能在当前线程上调用
		static size_t CURRENT_THREADID();

	protected:
		void threadEntry();

		virtual void run() = 0; // 运行
	protected:
		bool running_; //是否在运行
		std::thread *th_;
};

#endif // ZERO_THREAD_H

  • zero_thread.cpp
#include "zero_thread.h"
#include <sstream>
#include <iostream>
#include <exception>

ZERO_Thread::ZERO_Thread() :running_(false), th_(NULL) {
}

ZERO_Thread::~ZERO_Thread() {
	if (th_ != NULL) {
		//如果到调用析构函数的时候,调用者还没有调用join则触发detach,
		//此时是一个比较危险的动作,用户必须知道他在做什么
		if (th_->joinable()) {
			std::cout << "~ZERO_Thread detach\n";
			th_->detach();
		}

		delete th_;
		th_ = NULL;
	}
	std::cout << "~ZERO_Thread()" << std::endl;
}

bool ZERO_Thread::start() {
	if (running_) {
		return false;
	}
	try {
		th_ = new std::thread(&ZERO_Thread::threadEntry, this);
	} catch (...) {
		throw "[ZERO_Thread::start] thread start error";
	}
	return true;
}

void ZERO_Thread::stop() {
	running_ = false;
}

bool ZERO_Thread::isAlive() const {
	return running_;
}

void ZERO_Thread::join() {
	if (th_->joinable()) {
		th_->join();  // 不是detach才去join
	}
}

void ZERO_Thread::detach() {
	th_->detach();
}

size_t ZERO_Thread::CURRENT_THREADID() {
	// 声明为thread_local的本地变量在线程中是持续存在的,不同于普通临时变量的生命周期,
	// 它具有static变量一样的初始化特征和生命周期,即使它不被声明为static。
	static thread_local size_t
	threadId = 0;
	if (threadId == 0) {
		std::stringstream ss;
		ss << std::this_thread::get_id();
		threadId = strtol(ss.str().c_str(), NULL, 0);
	}
	return threadId;
}

void ZERO_Thread::threadEntry() {
	running_ = true;

	try {
		run();   // 函数运行所在 调用子类的run函数
	} catch (std::exception &ex) {
		running_ = false;
		throw ex;
	} catch (...) {
		running_ = false;
		throw;
	}
	running_ = false;
}

  • main.cpp
#include <iostream>
#include <chrono>
#include "zero_thread.h"
using namespace std;

class A: public ZERO_Thread {
	public:
		void run() {
			while (running_) {
				cout << "print A " << endl;
				std::this_thread::sleep_for(std::chrono::seconds(5));
			}
			cout << "----- leave A " << endl;
		}
};
class B: public ZERO_Thread {
	public:
		void run() {
			while (running_) {
				cout << "print B " << endl;
				std::this_thread::sleep_for(std::chrono::seconds(2));
			}
			cout << "----- leave B " << endl;
		}
};
int main() {
	{
		A a;
		a.start();
		
		B b;
		b.start();
		
		std::this_thread::sleep_for(std::chrono::seconds(5));
		
		a.stop();
		a.join();
		
		b.stop();
		b.join(); // 需要我们自己join
	}
	cout << "Hello World!" << endl;
	return 0;
}

2. 互斥量

  mutex又称互斥量,C++ 11中与 mutex相关的类(包括锁类型)和函数都声明在 头文件中,所以如果你需要使用 std::mutex,就必须包含 #include <mutex> 头文件。

C++11提供如下4种语义的互斥量(mutex):

  • std::mutex,独占的互斥量,不能递归使用。
  • std::time_mutex,带超时的独占互斥量,不能递归使用。
  • std::recursive_mutex,递归互斥量,不带超时功能。
  • std::recursive_timed_mutex,带超时的递归互斥量。

2.1 独占的互斥量std::mutex

2.1.1 std::mutex介绍

  std::mutex 是C++11 中最基本的互斥量,std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁,而 std::recursive_lock 则可以递归地对互斥量对象上锁。

  • 构造函数:std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于unlocked 状态的。

  • lock():调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:

(1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
(2). 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
  • unlock(): 解锁,释放对当前互斥量的所有权。
  • try_lock():尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况:
(1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
(2). 如果当前互斥量被其他线程锁住,则当前调用线程返回false,而并不会被阻塞掉。
(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

2.1.2 std::mutex使用Demo

#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex

volatile int counter(0); // non-atomic counter

std::mutex mtx; // locks access to counter

void increases_10k() {
	for (int i=0; i<10000; ++i) {
		//1. 使用lock的情况
		mtx.lock();
		++counter;
		mtx.unlock();
	}
}
int main() {
	std::thread threads[10];
	//计数,10 * 10k = 100 k
	for (int i=0; i<10; ++i) {
		threads[i] = std::thread(increases_10k);
	}

	for (auto& th : threads) {
		th.join();
	}
	std::cout << " successful increases of the counter " << counter <<std::endl;
	return 0;
}

 successful increases of the counter 100000

2.2 递归的互斥量std::recursive_mutex

2.2.1 std::recursive_mutex介绍

  递归锁允许同一个线程多次获取该互斥锁,可以用来解决同一线程需要多次获取同一个互斥量时死锁的问题

  虽然递归锁能解决这种情况的死锁问题,但是尽量不要使用递归锁,主要原因如下:

  1. 需要用到递归锁的多线程互斥处理本身就是可以简化的,允许递归很容易放纵复杂逻辑的产生,并且产生晦涩,当要使用递归锁的时候应该重新审视自己的代码是否一定要使用递归锁。

  2. 递归锁比起非递归锁,效率会低。

  3. 递归锁虽然允许同一个线程多次获得同一个互斥量,但可重复获得的最大次数并未具体说明,一旦超过一定的次数,再对lock进行调用就会抛出std::system错误。

2.2.2 std::recursive_mutex使用Demo

  下面代码用std::mutex来实现一个死锁。在调用both时获取了互斥量,在调用mul时又要获取互斥量,但both的并没有释放,从而产生死锁。

#include <iostream>
#include <thread>
#include <mutex>
struct Complex {
	std::mutex mutex;
	int i;
	Complex() : i(0) {}
	void mul(int x) {
		std::lock_guard<std::mutex> lock(mutex);
		i *= x;
	}
	void div(int x) {
		std::lock_guard<std::mutex> lock(mutex);
		i /= x;
	}
	void both(int x, int y) {
		std::lock_guard<std::mutex> lock(mutex);
		mul(x);
		div(y);
	}
};
int main(void) {
	Complex complex;
	complex.both(32, 23);
	return 0;
}

  使用std::recursive_mutex递归锁来解决这个问题

#include <iostream>
#include <thread>
#include <mutex>
struct Complex {
	std::recursive_mutex mutex;
	int i;
	Complex() : i(0) {}
	void mul(int x) {
		std::lock_guard<std::recursive_mutex> lock(mutex);
		i *= x;
	}
	void div(int x) {
		std::lock_guard<std::recursive_mutex> lock(mutex);
		i /= x;
	}
	void both(int x, int y) {
		std::lock_guard<std::recursive_mutex> lock(mutex);
		mul(x);
		div(y);
	}
};
int main(void) {
	Complex complex;
	complex.both(32, 23);
	std::cout << "main finish\n";
	return 0;
}

2.3 带超时的互斥量std::timed_mutex

2.3.1 std::timed_mutex介绍

  std::timed_mutex比std::mutex多了两个超时获取锁的接口:try_lock_for和try_lock_until

  • try_lock_for:尝试获取互斥锁。阻塞直到指定timeout_duration时间过去或获得锁。

  • try_lock_until:尝试获取互斥锁。阻塞直到timeout_time达到指定或获得锁

2.3.2 std::timed_mutex使用Demo

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::timed_mutex mutex;
void work() {
	std::chrono::milliseconds timeout(100);
	while (true) {
		if (mutex.try_lock_for(timeout)) {
			std::cout << std::this_thread::get_id() << ": do work with the mutex" << std::endl;
			std::chrono::milliseconds sleepDuration(250);
			std::this_thread::sleep_for(sleepDuration);
			mutex.unlock();
			std::this_thread::sleep_for(sleepDuration);
		} else {
			std::cout << std::this_thread::get_id() << ": do work without the mutex" << std::endl;
			std::chrono::milliseconds sleepDuration(100);
			std::this_thread::sleep_for(sleepDuration);
		}
	}
}
int main(void) {
	std::thread t1(work);
	std::thread t2(work);
	t1.join();
	t2.join();
	std::cout << "main finish\n";
	return 0;
}

2.4 lock_guard和unique_lock

2.4.1 lock_guard和unique_lock的介绍

  在前面的Demo中,使用的上锁有的是lock_guard,没有解锁的痕迹,那么这个到底是什么意思呢?相对于手动lock和unlock,我们可以使用RAII(通过类的构造和析构)来实现更好的编码方式。RAII:资源获取就是初始化

  换句话说, lock_guard和unique_lock ,在构造这个变量的时候,就会去尝试上锁lock,在作用域消失,生命周期结束的时候会析构,这个时候就会解锁,unlock。

2.4.2 lock_guard和unique_lock的使用

这里的lock_guard换成unique_lock是一样的。

#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::lock_guard
#include <stdexcept> // std::logic_error

std::mutex mtx;

void print_even (int x) {
	if (x%2==0) {
		std::cout << x << " is even\n";
	} else {
		//奇数throw 
		throw (std::logic_error("not even"));
	}
}

void print_thread_id (int id) {
	try {
		//std::lock_guard<std::mutex> lck (mtx);
		std::unique_lock<std::mutex> lck (mtx);
		print_even(id);
	} catch (std::logic_error&) {
		//捕获异常
		std::cout << "[exception caught]\n";
	}
}

int main () {
	std::thread threads[10];
	// spawn 10 threads:
	for (int i=0; i<10; ++i) {
		threads[i] = std::thread(print_thread_id,i+1);
	}

	for (auto& th : threads) {
		th.join();
	}
	return 0;
}

2.4.3 lock_guard和unique_lock的区别

  • unique_lock与lock_guard都能实现自动加锁和解锁,但是前者更加灵活,能实现更多的功能。

  • unique_lock可以进行临时解锁和再上锁,如在构造对象之后使用lck.unlock()就可以进行解锁,lck.lock()进行上锁,而不必等到析构时自动解锁

  我们发现lock_guard并没有提供unlock的接口,那么如果我们想减小锁的粒度,只能去写{lock_guard;临界资源},这样才能缩小,很麻烦。可以改用unique_lock,使用它提供的unlock接口

#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <unistd.h>
std::deque<int> q;
std::mutex mu;
std::condition_variable cond;
int count = 0;
void fun1() {
	while (true) {
		std::unique_lock<std::mutex> locker(mu);
		
		q.push_front(count++);
		
		locker.unlock(); 
		
		cond.notify_one();
		sleep(1);
	}
}
void fun2() {
	while (true) {
		std::unique_lock<std::mutex> locker(mu);
		
		cond.wait(locker, []() {
			return !q.empty();
		});
		auto data = q.back();
		q.pop_back();
		
		//locker.unlock(); 可以调用也可以不调用,下一次循环之前的locker会析构,里面也会调用unlock
		std::cout << "thread2 get value form thread1: " << data << std::endl;
	}
}
int main() {

	std::thread t1(fun1);
	std::thread t2(fun2);
	t1.join();
	t2.join();
	return 0;
}

  可以看到代码里面使用了条件变量。使用条件变量的目的是为了在没有获得某种提醒时进行休眠,不占用CPU。如果不这样做,我们只能一直循环(+sleep), 这样的问题就是有CPU消耗+时延的问题。

  条件变量的意思是在cond.wait这里一直休眠直到cond.notify_one唤醒才开始执行下一句; 还有cond.notify_all()接口用于唤醒所有等待的线程,cond.notify_one是唤醒一个等待的线程。

  在这个Demo里面可以使用lock_guard吗?语义上不可以,实际上可以。在下面的Demo1剖析里面会详细说明,在此处,我推荐直接理解为不可以,因为条件变量在wait时会进行unlock再进入休眠, lock_guard并无该操作接口,理解为不可以,避免在编码过程中出现危险操作。

  • wait: 如果线程被唤醒或者超时那么会先进行lock获取锁, 再判断条件(传入的参数)是否成立, 如果成立则wait函数返回否则释放锁继续休眠

  • notify: 进行notify动作并不需要获取锁

2.4.4 lock_guard和unique_lock的总结

  使用场景:需要结合notify+wait的场景使用unique_lock; 如果只是单纯的互斥使用lock_guard

lock_guard

1.std::lock_guard 在构造函数中进行加锁,析构函数中进行解锁。
2.锁在多线程编程中,使用较多,因此c++11提供了lock_guard模板类;在实际编程中,我们也可以根据自己的场景编写resource_guard RAII类,避免忘掉释放资源。

unique_lock

1. unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。
2. unique_lock比lock_guard使用更加灵活,功能更加强大。
3. 使用unique_lock需要付出更多的时间、性能成本。

3. 条件变量

  互斥量是多线程间同时访问某一共享变量时,保证变量可被安全访问的手段。但单靠互斥量无法实现线程的同步。线程同步是指线程间需要按照预定的先后次序顺序进行的行为。C++11对这种行为也提供了有力的支持,这就是条件变量。条件变量位于#include <condition_variable>下。

http://www.cplusplus.com/reference/condition_variable/condition_variable

条件变量使用过程:

  1. 拥有条件变量的线程获取互斥量;
  2. 循环检查某个条件,如果条件不满足则阻塞直到条件满足;如果条件满足则向下执行;
  3. 某个线程满足条件执行完之后调用notify_one或notify_all唤醒一个或者所有等待线程。

  条件变量提供了两类操作:wait和notify。这两类操作构成了多线程同步的基础。

3.1 条件变量的成员函数

3.1.1 wait函数

函数原型:

void wait (unique_lock<mutex>& lck);

template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);

  wait 包括两种重载,第一种只包含unique_lock,第二种多了一个predicate(等待条件)。这里都使用unique_lock的原因,是因为wait函数的工作流程导致的(里面有unlock,但是用lock_guard没有该接口)。

wait函数的工作流程:

  1. 当前线程条用wait后解锁互斥量并休眠,等待别的线程调用用notify_one或者notify_all唤醒当前线程。一旦当前线程获得通知(notify),那么会自动调用lock对互斥量加锁。

  2. 如果wait函数没有第二个等待条件的参数,那么在调用wait时默认条件不成立,直接解锁互斥量并休眠,直到被notify唤醒为止。被唤醒后,wait会自动加锁,如果得不到则阻塞等待锁

  3. 如果wait包含第二个等待条件的参数,调用wait后解锁互斥量并休眠,直到被notify唤醒为止。被唤醒后,wait会自动加锁,如果得不到则阻塞等待锁,直到获得锁。获得到该互斥量之后,开始判断第二个参数的条件,如果不满足条件表达式返回false,则wait对互斥量解锁并休眠。如果满足条件表达式返回true,则wait函数调用结束,走下面的代码流程。

// condition_variable example
#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
#include <unistd.h>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id (int id) {
	while (1) {
		std::unique_lock<std::mutex> lck(mtx);
		cv.wait(lck);

		std::cout << "thread " << id << '\n';

		lck.unlock();
	}
}

int main () {
	std::thread th[10];
	for(int i=0; i<10; i++) {
		th[i] = std::thread(print_id,i);
	}

	while(1) {
		sleep(1);
		std::cout<<"notify_one"<<std::endl;
		cv.notify_one();
	}
	for(int i=0; i<10; i++) {
		th[i].join();
	}
	

	return 0;
}

3.1.2 wait_for函数

函数原型:

template <class Rep, class Period>
cv_status wait_for (unique_lock<mutex>& lck, const chrono::duration<Rep,Period>& rel_time);

template <class Rep, class Period, class Predicate>
bool wait_for (unique_lock<mutex>& lck, const chrono::duration<Rep,Period>& rel_time, Predicate pred);

  wait_for和wait函数相比,多了一个rel_time参数。wait_for可以执行一个时间段,在线程收到唤醒通知或者时间超时之前,该线程都会处于休眠状态,如果收到唤醒通知或者时间超时,wait_for返回,剩下操作和wait类似。

// condition_variable example
#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
#include <unistd.h>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id (int id) {
	while (1) {
		std::unique_lock<std::mutex> lck(mtx);

		std::chrono::milliseconds timeout(1000);
		std::cv_status status=cv.wait_for(lck,timeout);
		
		if(status==std::cv_status::no_timeout) {
			std::cout << "thread " << id << '\n';
			lck.unlock();
			
		} else if (status==std::cv_status::timeout) {
			std::cout<<"time out\n";
		}
	}
}

int main () {
	std::thread th[10];
	for(int i=0; i<10; i++) {
		th[i] = std::thread(print_id,i);
	}

	while(1) {
		sleep(1);
		std::cout<<"notify_one"<<std::endl;
		cv.notify_one();
	}
	for(int i=0; i<10; i++) {
		th[i].join();
	}


	return 0;
}

3.1.3 wait_until函数

函数原型:

template <class Clock, class Duration>
cv_status wait_until (unique_lock<mutex>& lck,const chrono::time_point<Clock,Duration>& abs_time);

template <class Clock, class Duration, class Predicate>
bool wait_until (unique_lock<mutex>& lck,const chrono::time_point<Clock,Duration>& abs_time,Predicate pred);

  与wait_for类似,只是wait_until可以指定一个时间点,在当前线程收到通知或者指定的时间点超时之前,该线程都会处于阻塞状态。如果超时或者收到唤醒通知,wait_until返回,剩下操作和wait类似。

3.1.4 notify_one函数

函数原型:

void notify_one() noexcept;

  唤醒正在休眠当前条件的线程中的一个,如果没有线程在休眠等待,则函数不执行任何操作,如果正在等待的线程多余一个,则唤醒的线程是不确定的。

3.1.5 notify_all函数

函数原型:

void notify_all() noexcept;

  唤醒正在等待当前条件的所有线程,如果没有正在等待的线程,则函数不执行任何操作。

3.2 条件变量Demo

  使用条件变量实现一个同步队列,同步队列作为一个线程安全的数据共享区,经常用于线程之间数据读取。

3.2.1 Demo1代码

  • sync_queue.h
#ifndef SYNC_QUEUE_H
#define SYNC_QUEUE_H

#include<list>
#include<mutex>
#include<thread>
#include<condition_variable>
#include <iostream>

template<typename T>
class SyncQueue {
private:
    bool IsFull() const {
        return _queue.size() == _maxSize;
    }

    bool IsEmpty() const {
        return _queue.empty();
    }

public:
    SyncQueue(int maxSize) : _maxSize(maxSize) {
    }

    void Put(const T &x) {
        std::lock_guard <std::mutex> locker(_mutex);

        while (IsFull()) {
            std::cout << "full wait... size " << _queue.size() << std::endl;
            _notFull.wait(_mutex);
        }

        _queue.push_back(x);
        _notEmpty.notify_one();
    }

    void Take(T &x) {
        std::lock_guard <std::mutex> locker(_mutex);

        while (IsEmpty()) {
            std::cout << "empty wait.." << std::endl;
            _notEmpty.wait(_mutex);
        }

        x = _queue.front();
        _queue.pop_front();
        _notFull.notify_one();
    }

    bool Empty() {
        std::lock_guard <std::mutex> locker(_mutex);
        return _queue.empty();
    }

    bool Full() {
        std::lock_guard <std::mutex> locker(_mutex);
        return _queue.size() == _maxSize;
    }

    size_t Size() {
        std::lock_guard <std::mutex> locker(_mutex);
        return _queue.size();
    }

    int Count() {
        return _queue.size();
    }

private:
    std::list <T> _queue;                  //缓冲区
    std::mutex _mutex;                    //互斥量和条件变量结合起来使用
    std::condition_variable_any _notEmpty;//不为空的条件变量
    std::condition_variable_any _notFull; //没有满的条件变量
    int _maxSize;                         //同步队列最大的size
};

#endif // SYNC_QUEUE_H

  • main.cpp
#include <iostream>
#include "sync_queue.h"
#include <thread>
#include <iostream>
#include <mutex>

using namespace std;
SyncQueue<int> syncQueue(5);

void PutDatas() {
    for (int i = 0; i < 20; ++i) {
        syncQueue.Put(i);
    }
    std::cout << "PutDatas finish\n";
}

void TakeDatas() {
    int x = 0;

    for (int i = 0; i < 20; ++i) {
        syncQueue.Take(x);
        std::cout << x << std::endl;
    }
    std::cout << "TakeDatas finish\n";
}

int main(void) {
    std::thread t1(PutDatas);  // 生产线程
    std::thread t2(TakeDatas); // 消费线程

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

    std::cout << "main finish\n";
    return 0;
}

3.2.2 Demo1剖析

代码中用到了std::lock_guard,它利用RAII机制可以保证安全释放mutex。

std::lock_guard <std::mutex> locker(_mutex);

while (IsFull()) {
    std::cout << "full wait... size " << _queue.size() << std::endl;
    _notFull.wait(_mutex);
}

可以改成

std::lock_guard <std::mutex> locker(_mutex);

_notFull.wait(_mutex, [this] { return !IsFull(); });

  两种写法效果是一样的,但是后者更简洁,条件变量会先检查判断式是否满足条件,如果满足条件则重新获取mutex,然后结束wait继续往下执行;如果不满足条件则释放mutex,然后将线程置为waiting状态继续等待。

  这里需要注意的是,wait函数中会释放mutex,而lock_guard这时还拥有mutex,它只会在出了作用域之后才会释放mutex,所以这时它并不会释放,但执行wait时会提前释放mutex

  从语义上看这里使用lock_guard会产生矛盾,但是实际上并不会出问题,因为wait提前释放锁之后会处于等待状态,在被notify_one或者notify_all唤醒后会先获取mutex,这相当于lock_guard的mutex在释放之后又获取到了,因此,在出了作用域之后lock_guard自动释放mutex不会有问题。

  这里应该用unique_lock,因为unique_lock不像lock_guard一样只能在析构时才释放锁,它可以随时释放锁,因此在wait时让unique_lock释放锁从语义上更加准确。

3.2.3 优化后的Demo2

  • sync_queue2.h
#ifndef SIMPLE_SYNC_QUEUE_H
#define SIMPLE_SYNC_QUEUE_H

#include <thread>
#include <condition_variable>
#include <mutex>
#include <list>
#include <iostream>

template<typename T>
class SimpleSyncQueue {
public:
    SimpleSyncQueue() {}

    void Put(const T &x) {
        std::lock_guard <std::mutex> locker(_mutex);
        _queue.push_back(x);
        _notEmpty.notify_one();
    }

    void Take(T &x) {
        std::unique_lock <std::mutex> locker(_mutex);
        _notEmpty.wait(locker, [this] { return !_queue.empty(); });

        x = _queue.front();
        _queue.pop_front();
    }

    bool Empty() {
        std::lock_guard <std::mutex> locker(_mutex);
        return _queue.empty();
    }

    size_t Size() {
        std::lock_guard <std::mutex> locker(_mutex);
        return _queue.size();
    }

private:
    std::list <T> _queue;
    std::mutex _mutex;
    std::condition_variable _notEmpty;
};

#endif // SIMPLE_SYNC_QUEUE_H

  • main2.cpp
#include <iostream>
#include <thread>
#include <iostream>
#include <mutex>

#include "sync_queue2.h"

using namespace std;
SimpleSyncQueue<int> syncQueue;

void PutDatas() {
    for (int i = 0; i < 20; ++i) {
        syncQueue.Put(888);
    }
}

void TakeDatas() {
    int x = 0;

    for (int i = 0; i < 20; ++i) {
        syncQueue.Take(x);
        std::cout << x << std::endl;
    }
}

int main(void) {
    std::thread t1(PutDatas);
    std::thread t2(TakeDatas);

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

    std::cout << "main finish\n";
    return 0;
}

4. 原子变量

具体参考:www.cplusplus.com/reference/a…

// atomic::load/store example
#include <iostream>       // std::cout
#include <atomic>         // std::atomic, std::memory_order_relaxed
#include <thread>         // std::thread

//std::atomic<int> count = 0;//错误初始化
std::atomic<int> count(0); // 准确初始化

void set_count(int x) {
    std::cout << "set_count:" << x << std::endl;
    count.store(x, std::memory_order_relaxed);     // set value atomically
}

void print_count() {
    int x;
    do {
        x = count.load(std::memory_order_relaxed);  // get value atomically
    } while (x == 0);
    std::cout << "count: " << x << '\n';
}

int main() {
    std::thread t1(print_count);
    std::thread t2(set_count, 10);
    t1.join();
    t2.join();
    std::cout << "main finish\n";
    return 0;
}