1. 传统的生产者消费者模型

生产者-消费者模式是一个十分经典的多线程并发协作的模式,弄懂生产者-消费者问题能够让我们对并发编程的理解加深。所谓生产者-消费者问题,实际上主要是包含了两类线程,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能:如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;如果共享数据区为空的话,阻塞消费者继续消费数据。

2. 非多线程实现生产者消费者模型

下面的示例代码不是说的多线程问题,而是为了完成一个功能,设置一个大小固定的工厂,生产者不断的往仓库里面生产数据,消费者从仓库里面消费数据,功能类似于一个队列,每一次生产者生产数据放到队尾,消费者从头部不断消费数据,如此循环处理相关业务。

示例代码
下面是一个泛型的工厂类,可以不断的生产数据,消费者不断的消费数据。

#include <iostream>
#include <vector>

using namespace std;

#ifndef LNRT_FACTORY_H
#define LNRT_FACTORY_H

template<typename T>
class Factory {
private:
    vector<T> _factory;  //用vector模拟队列
    int _size = 5;  //容量
    bool logEnable = false; //状态
public:
    void produce(T item);  //生产
    T consume();  //消费
    void clear();  //清空剩余产品
    void configure(int cap, bool log = false);	//配置参数
};

template<typename T>
void Factory<T>::configure(int cap, bool log) {
    this->_size = cap;
    this->logEnable = log;
}

template<typename T>
//生产
void Factory<T>::produce(T item) {
    if (this->_factory.size() < this->_size) {
        this->_factory.push_back(item);
        if (logEnable) cout << "produce product " << item << endl;
        return;
    }
	//用vector模拟队列行为
	//在队头消费一件产品
    if (logEnable) cout << "consume product " << this->consume() << endl;
    //在队尾生产一件产品
    this->_factory[this->_size - 1] = item;
    if (logEnable) cout << "produce product " << item << endl;
}
template<typename T>
//消费
T Factory<T>::consume() {
    T item = this->_factory[0];
    
    for (int i = 1; i < this->_size; i++) 
    {
    	this->_factory[i - 1] = this->_factory[i];   //元素位移
    }
    return item;
}
template<typename T>
//不再生产了,直接清空队列剩余产品
void Factory<T>::clear() {
    for (int i = 0; i < this->_size; i++) if (logEnable) cout << "consume product " << this->consume() << endl;
}
#endif //LNRT_FACTORY_H

测试

int main(int argc, char* argv[])
{
    Factory<int> factory;
    factory.configure(5, true);

    for (int i = 0; i < 10; ++i) {
        factory.produce(i);
    }
    factory.clear();
    system("pause");
    return  0;
}

结果
在这里插入图片描述
用途
该类可以很方便的实现分组问题,比如处理视频序列时候将第i帧到第j帧数据作为一个分组处理任务,可以用下面的方法来实现。

参考


C++11实现生产者和消费者
生产者-消费者C++实现(一)


3. 多线程的理解与应用

3.1 互斥量

互斥量是一种同步原语,是一种线程同步的手段,用来保护多线程同时访问的共享数据。
C++11中提供了如下4种语义的互斥量(mutex):
std::mutex:独占的互斥量,不能递归使用。·
std::timed_mutex:带超时的独占互斥量,不能递归使用。·
std::recursive_mutex:递归互斥量,不带超时功能。·
std::recursive_timed_mutex:带超时的递归互斥量。

3.1.1 独占互斥量std::mutex的基本用法

#include <iostream>
#include <thread>
#include <mutex>
#include <deque>
#include <vector>
#include <condition_variable>

#ifdef WIN32
#include <windows.h>
#define sleep Sleep
#include "unistd.h"
#else
#include <pthread.h>
#include <unistd.h>
#endif 

using namespace std;

class Mutex
{
public:
	Mutex() {}
	~Mutex() {}

	void func()
	{
		g_lock.lock();
		cout << "entered thread " << this_thread::get_id() << endl;
		this_thread::sleep_for(chrono::seconds(1));
		cout << "leaving thread " << this_thread::get_id() << endl;
		g_lock.unlock();
	}

	void Start()
	{
		thread t1(&Mutex::func, this);
		thread t2(&Mutex::func, this);
		thread t3(&Mutex::func, this);
		t1.join();
		t2.join();
		t3.join();
	}
private:
	mutex g_lock;
};

在这里插入图片描述
使用lock_guard可以简化lock/unlock的写法,同时也更安全,因为lock_guard在构造时会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记unlock操作,因此,应尽量用lock_guard。lock_guard用到了RAII技术,这种技术在类的构造函数中分配资源,在析构函数中释放资源,保证资源在出了作用域之后就释放。上面的例子使用lock_guard后会更简洁,代码如下:

void func()
{
	//g_lock.lock();
	lock_guard<mutex> locker(g_lock);
	cout << "entered thread " << this_thread::get_id() << endl;
	this_thread::sleep_for(chrono::seconds(1));
	cout << "leaving thread " << this_thread::get_id() << endl;
	//g_lock.unlock();
}

3.1.2 递归互斥量std::recursive_mutex

递归锁允许同一线程多次获得该互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。在代码清单5-2中,一个线程多次获取同一个互斥量时会发生死锁。

代码清单5-2使用std::mutex发生死锁的示例

class Complex {
public:
	Complex():i(0) {}
	~Complex() {}
	void mul(int x)
	{
		lock_guard<mutex> lock(_mutex);
		i *= x;
	}

	void div(int x)
	{
		lock_guard<mutex> lock(_mutex);
		i /= x;
	}

	void both(int x,int y)
	{
		lock_guard<mutex> lock(_mutex);
		mul(x);
		div(y);
	}

private:
	int i;
	mutex _mutex;
};

int main(int argc, char* argv[]){
	Complex complex;
	complex.both(32, 23);
	return 0;
}

这个代码清单5-2例子运行起来后就会发生死锁,因为在调用both时获取了互斥量,之后再调用mul又要获取相同的互斥量,但是这个互斥量已经被当前线程获取了,无法释放,这时就会发生死锁。要解决这个死锁的问题,一个简单的办法就是用递归锁:std::recursive_mutex,它允许同一线程多次获得互斥量,如代码清单5-3所示。

代码清单5-3 std::recursive_mutex的基本用法

#define  mutex recursive_mutex
class Complex {
public:
	Complex():i(0) {}
	~Complex() {}
	void mul(int x)
	{
		lock_guard<mutex> lock(_mutex);
		i *= x;
	}

	void div(int x)
	{
		lock_guard<mutex> lock(_mutex);
		i /= x;
	}

	void both(int x,int y)
	{
		lock_guard<mutex> lock(_mutex);
		mul(x);
		div(y);
	}

private:
	int i;
	mutex _mutex;
};

int main(int argc, char* argv[]){
	Complex complex;
	complex.both(32, 23); //因为同一线程可以多次获取同一互斥量,不会发生死锁
	return 0;
}

输出

0
0

需要注意的是尽量不要使用递归锁好,主要原因如下:
(1)需要用到递归锁定的多线程互斥处理往往本身就是可以简化的,允许递归互斥很容易放纵复杂逻辑的产生,从而导致一些多线程同步引起的晦涩问题。
(2)递归锁比起非递归锁,效率会低一些。
(3)递归锁虽然允许同一个线程多次获得同一个互斥量,可重复获得的最大次数并未具体说明,一旦超过一定次数,再对lock进行调用就会抛出std::system错误。

3.1.3 带超时的互斥量std::timed_mutex和std::recursive_timed_mutex

std::timed_mutex是超时的独占锁,std::recursive_timed_mutex是超时的递归锁,主要用在获取锁时增加超时等待功能,因为有时不知道获取锁需要多久,为了不至于一直在等待获取互斥量,就设置一个等待超时时间,在超时后还可以做其他的事情。

std::timed_mutex比std::mutex多了两个超时获取锁的接口:try_lock_for和try_lock_until,这两个接口是用来设置获取互斥量的超时时间,使用时可以用一个while循环去不断地获取互斥量。std::timed_mutex的基本用法如代码清单5-4所示。

代码清单5-4 std::timed_mutex的基本用法

#include <iostream>
#include <thread>
#include <mutex>
#include <deque>
#include <vector>
#include <condition_variable>

#ifdef WIN32
#include <windows.h>
#define sleep Sleep
#include "unistd.h"
#else
#include <pthread.h>
#include <unistd.h>
#endif 

using namespace std;

class TimedMutex
{
public:
	TimedMutex() {}
	~TimedMutex() {}
	void Work()
	{
		chrono::milliseconds timeout(100);
		while (1)
		{
			if (mutex.try_lock_for(timeout)) 
			{
				cout << this_thread::get_id() << ": do work with the mutex" << endl;
				chrono::milliseconds sleepDuration(2500);
				this_thread::sleep_for(sleepDuration);
				mutex.unlock();
				this_thread::sleep_for(sleepDuration);
			}
			else
			{
				cout << this_thread::get_id() << ": do work without mutex" << endl;
				chrono::milliseconds sleepDuration(2500);
				this_thread::sleep_for(sleepDuration);
			}
		}
	}

	void Start() {
		thread t1(&TimedMutex::Work,this);
		thread t2(&TimedMutex::Work,this);
		t1.join();
		t2.join();
	}
private:
	timed_mutex mutex;
};

测试

#include "code54.cpp"
int main(int argc, char* argv[])
{
	TimedMutex tm;
	tm.Start();
	return 0;
}

在这里插入图片描述
在上面的代码3例子中,通过一个while循环不断地去获取超时锁,如果超时还没有获取到锁时就休眠100毫秒,再继续获取超时锁。相比std::recursive_mutex,std::recursive_timed_mutex多了递归锁的功能,允许同一线程多次获得互斥量。std::recursive_timed_mutex和std::recursive_mutex的用法类似,可以看作在std::recursive_mutex的基础上加了超时功能。

3.1.4 跨平台调试代码可能出现的问题

Microsoft Visual Studio下编译缺少头文件unistd.h解决办法
https://blog.csdn.net/sinat_36053757/article/details/68487662

对‘pthread_create’未定义的引用的解决办法

(caffe2_env) zhoujianwen@zhoujianwen-System:/mnt/Desktop-tdr4tm2d/projects/code/src/testcpp$ sudo g++ main.cpp -o main -std=c++11
/tmp/ccQGnSUj.o:在函数‘std::thread::thread<void (CThreadMsg::*)(), CThreadMsg*>(void (CThreadMsg::*&&)(), CThreadMsg*&&)’中:
main.cpp:(.text._ZNSt6threadC2IM10CThreadMsgFvvEJPS1_EEEOT_DpOT0_[_ZNSt6threadC5IM10CThreadMsgFvvEJPS1_EEEOT_DpOT0_]+0x93):对‘pthread_create’未定义的引用
sudo g++ main.cpp -o main  -lpthread -std=c++11

3.2 条件变量

条件变量是C++11提供的另外一种用于等待的同步机制,它能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时,才会唤醒当前阻塞的线程。条件变量需要和互斥量配合起来用。C++11提供了两种条件变量:
condition_variable,配合std::unique_lock进行wait操作。
condition_variable_any,和任意带有lock、unlock语义的mutex搭配使用,比较灵活,但效率比condition_variable差一些。
可以看到condition_variable_any比condition_variable更灵活,因为它更通用,对所有的锁都适用,而condition_variable性能更好。我们应该根据具体应用场景来选择条件变量。

条件变量的使用过程如下:
(1)拥有条件变量的线程获取互斥量。
(2)循环检查某个条件,如果条件不满足,则阻塞直到条件满足;如果条件满足,则向下执行。
(3)某个线程满足条件执行完之后调用notify_one或notify_all唤醒一个或者所有的等待线程。可以用条件变量来实现一个同步队列,同步队列作为一个线程安全的数据共享区,经常用于线程之间数据读取,比如半同步半异步线程池的同步队列。

3.2.1 同步队列的实现

代码清单5-5

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

#ifdef WIN32
#include <windows.h>
#define sleep(x) Sleep(x*1000)
//Sleep(n),n毫秒
//sleep(n),n秒
#include "unistd.h"
#else
#include <pthread.h>
#include <unistd.h>
#endif 

using namespace std;
#define random(x) rand()%(x)

template<typename T>
class SyncQueue
{
private:
	bool IsFull() const
	{
		return m_maxSize == m_queue.size();
	}
	bool IsEmpty() const
	{
		return m_queue.empty();
	}
public:
	SyncQueue() {};
	SyncQueue(int maxSize) :m_maxSize(maxSize)
	{	

	}
	~SyncQueue()
	{
		
	}

	void Put(const T& x)
	{
		lock_guard<mutex> locker(m_mutex);
		while (IsFull())
		{
			cout << "缓冲区满了,需要等待……" << endl;
			m_notFull.wait(m_mutex);
		}
		m_queue.push_back(x);
		m_notEmpty.notify_one();
		cout << "Put:" << x << endl;
	}

	void Take(T& x)
	{
		lock_guard<mutex> locker(m_mutex);
		while (IsEmpty())
		{
			cout << "缓冲区空了,需要等待……" << endl;
			m_notEmpty.wait(m_mutex);
		}
		x = m_queue.front();
		m_queue.pop_front();
		m_notFull.notify_one();
		cout << "Take:" << x << endl;
	}

	bool Empty()
	{
		lock_guard<mutex> locker(m_mutex);
		return m_queue.empty();
	}

	bool Full()
	{
		lock_guard<mutex> locker(m_mutex);
		return m_queue.size() == m_maxSize;
	}

	size_t Size()
	{
		lock_guard<mutex> locker(m_mutex);
		return m_queue.size();
	}
	
	int Count()
	{
		return m_queue.size();
	}

	//生产
	void Produce()
	{
		int i = 1;
		while (1)
		{
			auto x = random(100);
			Put(x);
			if(i % 10 ==0) //每生产10个产品就休息5秒
				sleep(5);
			i++;
		}
	}

	//消费
	void Consume()
	{
		int x;
		while (true)
		{
			Take(x);
			sleep(5);
		}
	}

	void Start()
	{		
		thread t1(&SyncQueue::Produce,this);   //负责生产
		t1.detach();
		thread t2(&SyncQueue::Consume,this);   //负责消费
		t2.detach();
		//还可以多设置4个消费行为,目前设置了一个
		//thread t3(&SyncQueue::Consume, this);
		//t3.detach();
		//thread t4(&SyncQueue::Consume, this);
		//t4.detach();
		//thread t5(&SyncQueue::Consume, this);
		//t5.detach();
		//thread t6(&SyncQueue::Consume, this);
		//t6.detach();
		
	}
private:
	list<T> m_queue; //缓冲区
	mutex m_mutex;	//互斥量和条件变量结合起来使用
	condition_variable_any m_notEmpty;	//不为空的条件变量
	condition_variable_any m_notFull;	//没有满的条件变量
	int m_maxSize;	//同步队列最大的容量
};
int main(int argv,char* argv[])
{
	SyncQueue<int> sq(5);  //同步队列缓存5个数量
	sq.Start();
	while (1){//阻塞不让主线程退出}
	return 0;
}

在这里插入图片描述
这个同步队列在没有满的情况下可以插入数据,如果满了,则会调用m_notFull阻塞等待,待消费线程取出数据之后发一个未满的通知,然后前面阻塞的线程就会被唤醒继续往下执行;如果队列为空,就不能取数据,会调用m_notempty条件变量阻塞,等待插入数据的线程发出不为空的通知时,才能继续往下执行。以上过程是同步队列的工作过程。
代码清单5-5用到了std::lock_guard,它利用了RAII机制可以保证安全释放mutex。从代码清单5-5中还可以看到,std::unique_lock和std::lock_guard的差别在于前者可以自由地释放mutex,而后者则需要等到std::lock_guard变量生命周期结束时才能释放。条件变量的wait还有一个重载方法,可以接受一个条件。比如下面的代码:

lock_guard<mutex> locker(m_mutex);
while (IsFull())
{
	cout << "缓冲区满了,需要等待……" << endl;
	m_notFull.wait(m_mutex);
}

可以改成这样:

lock_guard<mutex> locker(m_mutex);
m_notFull.wait(locker, [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释放锁从语义上看更加准确。
我们可以改写一下代码清单5-5,把std::lock_guard改成std::unique_lock,把std::condition_variable_any改为std::condition_variable,并且用等待一个判断式的方法来实现一个简单的线程池,如代码清单5-6所示。

参考


C ++ 11线程调用对象的成员函数
C++11多线程std::thread 调用某个类中函数的方法
C/C++线程退出的四种方法
C++11 - thread多线程编程,线程互斥和同步通信,死锁问题分析解决
c++多线程thread操作(七)父进程获取子进程变量的结果
C++并发实战3:向thread传递参数
C++多线程(一)thread类基础知识介绍
c++11 使用detach()时,主线程和detach线程的同步控制
C++11多线程 unique_lock详解
C++并发实战9:unique_lock


3.2.2 使用unique_lock和condition_variable实现的同步队列

代码清单5-6
主要在代码清单5-5基础上修改Put、Take、Produce、Consume的函数

	void Put(const T& x)
	{
		unique_lock<mutex> locker(m_mutex);
		/*while (IsFull())
		{
			cout << "缓冲区满了,需要等待……" << endl;
			m_notFull.wait(m_mutex);
		}*/
		m_notFull.wait(locker, [this] {
			if (IsFull()) 
			{ 
				cout << "缓冲区满了,需要等待……" << endl; 
				return false; 
			}
			return true;});
		m_queue.push_back(x);
		m_notEmpty.notify_one();
		cout << "Put:" << x << endl;
	}
	void Take(T& x)
	{
		unique_lock<mutex> locker(m_mutex);
		/*while (IsEmpty())
		{
			cout << "缓冲区空了,需要等待……" << endl;
			m_notEmpty.wait(m_mutex);
		}*/
		m_notEmpty.wait(locker, [this] {
			if (IsEmpty())
			{
				cout << "缓冲区空了,需要等待……" << endl;
				return false;
			}
			return true; });
		x = m_queue.front();
		m_queue.pop_front();
		m_notFull.notify_one();
		cout << "Take:" << x << endl;
	}
//生产
	void Produce()
	{
		//int i = 1;
		while (true)
		{
			auto x = random(100);
			Put(x);
			//if(i % 10 ==0) //每生产10个产品就休息5秒
			//	sleep(5);
			//i++;
		}
	}

	//消费
	void Consume()
	{
		int x;
		//int i = 1;
		while (true)
		{
			Take(x);
			//if(i % 10 ==0) //每消费10个产品就休息2秒
			//	sleep(2);
			//i++;
		}
	}

相比于代码清单5-5,代码清单5-6用unique_lock代替lock_guard,使语义更加准确,用性能更好的condition_variable替代condition_variable_any,对程序加以优化,这里使用condition_variable_any也是可以的。执行wait时不再通过while循环来判断,而是通过lambda表达式来判断,写法上更简洁了。

3.3 线程异步操作函数async

std::asyncstd::promisestd::packaged_taskstd::thread更高一层,它可以用来直接创建异步的task,异步任务返回的结果也保存在future中,当需要获取异步任务的结果时,只需要调用future.get()方法即可,如果不关注异步任务的结果,只是简单地等待任务完成的话,则调用future.wait()方法。

现在看一下std::async的原型async(std::launch::async| std::launch::deferred,f,args…),第一个参数是线程的创建策略,有两种策略,默认的策略是立即创建线程。

std::launch::async:在调用async时就开始创建线程。
std::launch::deferred:延迟加载方式创建线程。调用async时不创建线程,直到调用了future的get或者wait时才创建线程。

第二个参数是线程函数,第三个参数是线程函数的参数。

std::async的基本用法如下代码所示。

async5.cpp

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

#ifndef ASYNC5_CPP
#define ASYNC5_CPP

inline void transData(int &i)
{
	future<int> future = std::async(std::launch::async, []() { 
		this_thread::sleep_for(chrono::seconds(5));	//当前线程阻塞5秒,模拟数据正在接收
		return 8;
		});
	cout << "waiting...\n" << endl;
	future_status status;
	do {
		status = future.wait_for(chrono::seconds(1));
		if (status == future_status::deferred)
		{
			cout << "\ndeferred" << endl;
		}
		else if (status == future_status::timeout)
		{
			cout << "\ntimeout" << endl;
		}
		else if (status == future_status::ready)
		{
			cout << "\nready!" << endl;
		}

	} while (status != future_status::ready);
	i = future.get();
	cout << "\nresult is " << i  << endl;
	
}

inline void fun510()
{
	future<int> f1 = std::async(std::launch::async, []() { return 8; });
	cout << f1.get() << endl;
	future<void> f2 = std::async(std::launch::async, []() { cout << 8 << endl; });
	f2.wait(); // output:8
	int x = 0;
	thread t1(transData,ref(x)); //子线程
	t1.detach(); //非阻塞模式

	while (!x)
	{
		//主线程每2秒阻塞一次,模拟正在处理其它数据,并且等待接收f2的数据结果
		this_thread::sleep_for(chrono::seconds(1));
		cout << "doing other something!,but x = "<< x << endl;
	}
}
#endif // !ASYNC5_CPP

main.cpp

#include "async5.cpp"
int main()
{
	fun510();
	return 0;
}

在这里插入图片描述
std::async是更高层次的异步操作,使得我们不用关注线程创建内部细节,就能方便地获取异步执行状态和结果,还可以指定线程创建策略:应该用std::async替代线程的创建,让它成为我们做异步操作的首选。

链接器工具错误 LNK2005

4. 多线程参数传递

1,值传递,拷贝一份新的给新的线程。线程1中有个int变量a,在线程1中启动线程2,参数是a的值,这时就会拷贝a,线程1和线程2不共享a。
2,引用传递,不拷贝一份新的给新的线程。线程1中有个int变量a,在线程1中启动线程2,参数是a的引用,这时就不会拷贝a,线程1和线程2共享a。※传递参数时,必须明确指出使用std::ref函数,不写std::ref,编译不过。
3,指针传递,浅拷贝原来的指针给新的线程。线程1中有个指向int变量a的指针,在线程1中启动线程2,参数是a的地址,这时就不会拷贝a,只是浅拷贝指向a的指针,线程1和线程2共享a。
4,unique_ptr作为参数传递,必须使用move函数
5,函数的指针作为参数传递
引用传递,指针传递的注意事项:因为线程2里使用的是线程1的变量a,所以如果线程1比线程2提前结束了,结束的同时就会释放变量a的内存空间,可是这时线程2还没结束,再去访问线程1中的变量a的话,就会发生意想不到的错误!!!

4.1 引用传递例子

一共3个线程,main函数是一个线程,在main函数里启动了线程2(f1函数),在线程2(f1函数)里启动了线程3(f2函数)。

#include <iostream>
#include <thread>
#include <string>
#include <unistd.h>

using namespace std;

void f2(int& i){
  cout << "f2:" << i << endl;
}
void f1(int& i){
  cout << "f1:" << i << endl;
  int j = 11;
  thread t(f2, ref(j));//-------------->②
  t.detach();
}
int main(){
  int i = 10;
  thread t(f1, ref(i));
  t.detach();//-------------->①
  pthread_exit(NULL);
}

执行结果:

f1:10
f2:0

执行结果分析:
打印出【f1:10】的原因可能是,①处分离线程后,main函数所在的线程还没有结束,所以i还没有被释放掉,所以能打印出10;还有可能是main函数所在的线程虽然已经结束了,但是巧合的是值还是10。
打印出【f2:0】的原因是,②处分离线程后,线程f1已经结束了,所以函数f1里的j已经被释放了,这时线程f2再访问j的时候就是0了。

4.2 指针传递例子

一共3个线程,main函数是一个线程,在main函数里启动了线程2(f1函数),在线程2(f1函数)里启动了线程3(f2函数)。

#include <iostream>
#include <thread>
#include <string>
#include <unistd.h>

using namespace std;

void f2(int* i){
  cout << "f2:" << *i << endl;
}
void f1(int& i){
  cout << "f1:" << i << endl;
  int j = 11;
  thread t(f2, &j);
  t.detach();//-------------->②
}
int main(){
  int i = 10;
  thread t(f1, ref(i));
  t.detach();//-------------->①
  pthread_exit(NULL);
}

执行结果:

f1:10
f2:0

执行结果分析:
打印出【f1:10】的原因可能是,①处分离线程后,main函数所在的线程还没有结束,所以i还没有被释放掉,所以能打印出10;还有可能是main函数所在的线程虽然已经结束了,但是巧合的是值还是10。
打印出【f2:0】的原因是,②处分离线程后,线程f1已经结束了,所以函数f1里的j已经被释放了,这时线程f2再访问j的时候就是0了。

4.3 unique_ptr作为参数传递,必须使用move函数

#include <iostream>
#include <thread>
#include <string>
#include <unistd.h>

using namespace std;

void f1(unique_ptr<int> upt){
  cout << *upt << endl;
}

int main(){
  unique_ptr<int> upt(new int(10));
  //必须使用move函数,否则编译不过
  thread t(f1, move(upt));
  t.detach();
  pthread_exit(NULL);
}

4.4 函数的指针作为参数传递

#include <iostream>
#include <thread>
#include <string>
#include <unistd.h>

using namespace std;

class Test{
public:
  void func(int& i){
    cout << i << endl;
  }
};
int main(){
  Test da;
  int i = 10;
  //&Test::func为对象的方法的指针,&da为对象的指针,ref(i)是方法func的参数
  thread t(&Test::func, &da, ref(i));
  t.detach();
  pthread_exit(NULL);
}

5. 总结

C++11以前是没有内置多线程的,使得跨平台的多线程开发不方便,现在C++11增加了很多线程相关的库,使得我们能很方便地开发多线程程序了。

线程的创建和使用简单方便,可以通过多种方式创建,还可以根据需要获取线程的一些信息及休眠线程。

互斥量可以通过多种方式来保证线程安全,既可以用独占的互斥量保证线程安全,又可以通过递归的互斥量来保护共享资源以避免死锁,还可以设置获取互斥量的超时时间,避免一直阻塞等待。

条件变量提供了另外一种用于等待的同步机制,它能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时,才会唤醒当前阻塞的线程。条件变量的使用需要配合互斥量。

原子变量可以更方便地实现线程保护。call_once保证在多线程情况下函数只被调用一次,可以用在某些只能初始化一次的场景中。

future、promise和std::package_task用于异步调用的包装和返回值。

async更方便地实现了异步调用,应该优先使用async取代线程的创建。

6. 参考

祁宇 深入应用C++11:代码优化与工程级应用
多电梯调度系统
C++基于消息队列的线程池实现
C++11:基于std::queue和std::mutex构建一个线程安全的队列

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐