多线程实现生产者和消费者(同步队列)以及线程异步操作async
传统的生产者消费者模型生产者-消费者模式是一个十分经典的多线程并发协作的模式,弄懂生产者-消费者问题能够让我们对并发编程的理解加深。所谓生产者-消费者问题,实际上主要是包含了两类线程,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要
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::async
比std::promise
、std::packaged_task
和std::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替代线程的创建,让它成为我们做异步操作的首选。
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构建一个线程安全的队列
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)