《白话C++》第10章 STL和boost,Page92 并发下shared_ptr实例

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;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值