6.并发下shared_ptr示例
例子代码子在主线程中循环创建1000个“DataABC”对象,每个对象包含三个整数成员。每个对象都被丢给两个处理者,一个是“Printer”,负责将收到的对象内容输出到屏幕,另一个是“Counter”,负责累加所有对象的三个整数。
以“Printer”为例,主线程创建完某个对象之后,将它“丢”给Printer对象。然而,主线程并不等待Printer完成输出,就立即“忙着”去创建下一个队形。
“Printer”和“Counter”类都有一个“队列(queue)”成员,队列像一条管子,一头接受主线程塞过来的DataABC对象;另一头则各有一个专用的线程自个儿抓紧处理。
先加入头文件,并引入名字空间std:
#include <iostream>
#include <memory>
#include <string>
#include <sstream>
#include <queue> //队列
#include <random> //随机
#include <thread> //线程
#include <mutex> //互斥体
using namespace std;
不仅Printer需要输出,为了直观,我们让Counter在计算过程中也输出一些中间结果,由于二者各自使用独立的线程,意味着至少有两个线程可能同时王屏幕上输出,所以需要加上互斥,为此我们对带锁的cout操作加一个简单的封装:
struct COutWithMutex
{
public:
static COutWithMutex& Get()
{
static COutWithMutex instance;
return instance;
}
void PrintLn(string const& line)
{
lock_guard <mutex> g(_m); //守护代码块
cout << line << endl;
}
private:
COutWithMutex() = default;
mutex _m;
};
cout是全局唯一对象,所以COutWithMutex也被设计成单例,构造函数私有化,然后提供一个静态成员“Get()”以返回全局唯一的对象,它来自该函数内部定义的一个局部静态数据。
重点是“PrintLn()”成员,在输出line之前,使用守护锁保证守护区域内的代码,同一时间内只有一个线程通过。建议复习第七章《语言》第16小节的“并行流程”。
接着是DataABC的定义,一个简单的结构体:
struct DataABC
{
DataABC() = default;
DataABC(int a, int b, int c)
: a(a), b(b), c(c)
{
}
int a, b, c;
};
所有DataABC对象,都将被扔到“Printer”或“Counter”中的队列中去。
标准库<queue>中有线程的队列容器,不过由于主线程负责“塞入数据”,其他线程负责“取出数据”,意味着至少有两个线程会操作同一个队列,所以同样需要为标准库的队列加上锁,以下是我们的封装:
class DataABCQueue
{
public:
void Push(shared_ptr <DataABC> data)///向队列中压入数据
{
lock_guard <mutex> g(_m);
_q.push(data);
}
shared_ptr <DataABC> Pop() ///从队列中取出数据
{
lock_guard <mutex> g(_m);
if(_q.empty())
{
return shared_ptr <DataABC> (nullptr);
}
//返回队列中的第一个元素,并没有把它剔除出队列
shared_ptr <DataABC> r = _q.front();
//将队列中最靠前位置的元素拿掉,返回void
_q.pop();
return r;
}
bool IsEmpty()
{
lock_guard <mutex> g(_m);
//如果队列为空,返回true
return _q.empty();
}
private:
mutex _m;
queue <shared_ptr <DataABC>> _q;
};
DataABCQueue包含一个queue<T> 类型的对象,T代表队列所要存储的元素类型,重点就在此:队列中存储的是shared_ptr<DataABC>类型的元素,即DataABC的“共享型”智能指针。DataABCQueue提供了三个对外接口:Push()、Pop()和IsEmpty(),分别发挥“塞入”、“弹出”和“判断是否为空”的作用,全部借由_q成员实现,只不过响应操作之前都加了守护锁。
class Printer
{
public:
void Append(shared_ptr <DataABC> data)///将数据添加队列
{
_q.Push(data);
}
void PrintOne() ///从队列中取出一个数据,然后打印出来
{
shared_ptr <DataABC> data = _q.Pop();
if(! data)
{
COutWithMutex::Get().PrintLn("printer Waitting...");
return;
}
stringstream ss;
ss << "a = " << data->a
<< ",\tb = " << data->b
<< ",\tc = " << data->c
<< ".";
COutWithMutex::Get().PrintLn(ss.str());
}
bool IsEmpty() ///打印机是否为空
{
return _q.IsEmpty();
}
private:
DataABCQueue _q;
};
接着定义“Printer(打印机)”,它只有一个成员数据:DataABCQueue _q,可以称为“打印机队列”。“Printer”提供一个Append接口,当需要本“打印机”输出某个数据时就调用它,我们已经知道数据类型是shared_ptr <DataABC>。不过,我们这打印机是半自动的,光把数据塞入只是让“打印机”记下数据,想要打印出来,需要不断地调用PrintOne()方法,正如它的名字,这个方法每次把打印机队列中排在最前头的数据取出,执行打印。“Printer”的最后一个成员是“IsEmpty()”,用于判断当前打印机队列是否还有数据。
打印机在打印时(往屏幕上输出),调用了“COutWithMutex”单例的功能。
先不管“Counter(计数器)”的事,下面是main函数。它将创建“打印机”,创建驱动打印机所需要的独立线程,然后创建1000个DataABC的对象,将它一个个送个打印机:
int main()
{
Printer printer; //打印机对象
bool exit = false; //告诉打印机完事了,别再死等数据了
//创建线程,我们使用了“lambda”函数
std::thread trd1([&printer, &exit]() //没有参数,捕获printer和exit
{
while(!exit || !printer.IsEmpty())
{
printer.PrintOne();
}
});
std::random_device rd; //随机数发生器
for(int i = 0; i < 1000; ++i)
{
//创建一个DataABC对象,使用智能指针包装
auto data = make_shared <DataABC> (rd(), rd(), rd());
///传给打印机
printer.Append(data);
}
//至此,1000个数据都产生了,
//所以现在只需等printer输出队列中的所有数据
//请认真分析线程任务中的while循环的条件
exit = true;
trd1.join(); //必须等线程结束循环
return 0;
}
请直面一个现实:在创建DataABC数据之前,负责打印的trd1线程对象就已经创建并且执行了,那时候其内部队列肯定是空的,只不过exit为假,所以它不得不无聊地空转,这很浪费CPU,幸好应该是千分之一甚至万分之一秒后,for循环启动了。依据经验,可以预测将创建1000个数据加入队列,比排队输出1000个数据肯定要快,这一点,请想办法验证。
循环结束后,将exit标志为“真”这点很重要,不然线程就会陷入死循环,有可能出大事。通过join()方法安心等待线程确实推出,这一点也重要,不然子线程还在执行,主线程(main函数所在线程)就退出了,这也可能酿成大祸。join()的调用必须在exit设置为“真”之后进行,这一点也很重要,不然……可以试试。
通观一下代码,我们造了很多堆对象并在线程间传播,既不写new也不写delete,但没有内存泄露发生。for循环语句域内,每循环一次,都会创建一个data(shared_ptr),然后销毁它,但它并不会真正地去释放所持有的DataABC堆对象,因为在析构之前,该智能指针已经通过函数传递的方式,共享给另一个智能指针,那个智能指针继续传递,进入Printer对象的队列中存着,一直到另外一线程将它“抓”出去打印。
现在给出“Counter”的定义:
class Counter
{
public:
void Append(shared_ptr <DataABC> data) ///将数据添加到Counter
{
_q.Push(data);
}
void CountOne() ///计算当前数据的和并打印出来
{
shared_ptr <DataABC> data = _q.Pop();
if(!data)
{
COutWithMutex::Get().PrintLn("counter Waitting...");
return;
}
_sum += (data->a + data->b + data->c);
stringstream ss;
ss << "current sum is: " << _sum << ".";
COutWithMutex::Get().PrintLn(ss.str());
}
bool IsEmpty() ///当前Counter是否为空
{
return _q.IsEmpty();
}
long long GetSum() const
{
return _sum;
}
private:
DataABCQueue _q;
long long _sum = 0;
};
请各位在main函数中,再建一个线程加以累计。
int main()
{
Printer printer; //打印机对象
bool exit = false; //告诉打印机完事了,别再死等数据了
//创建线程,我们使用了“lambda”函数
std::thread trd1([&printer, &exit]() //没有参数,捕获printer和exit
{
cout << "printer线程开始工作" << endl;
while(!exit || !printer.IsEmpty()) ///当exit为false时,不管打印机队列是否为空,循环都开始执行
{ ///当exit为true时,打印机队列为空,则停止循环
printer.PrintOne();
}
});
///创建Counter线程
Counter counter; ///计数器对象
bool exit2 = false;
std::thread trd2([&counter, &exit2]()
{
cout << "计数器线程开始工作" << endl;
while(!exit2 || !counter.IsEmpty())
{
counter.CountOne();
}
});
///休眠1秒钟,看看当前printer队列和conter是否会打印Waiting
// this_thread::sleep_for(std::chrono::seconds(1));
std::random_device rd; //随机数发生器
for(int i = 0; i < 1000; ++i)
{
//创建一个DataABC对象,使用智能指针包装
auto data = make_shared <DataABC> (rd(), rd(), rd());
///传给打印机
printer.Append(data);
///传给counter
counter.Append(data);
}
//至此,1000个数据都产生了,
//所以现在只需等printer输出队列中的所有数据
//请认真分析线程任务中的while循环的条件
exit = true;
exit2 = true;
trd1.join(); //必须等线程结束循环
trd2.join();
return 0;
}