没想到bind的功能这么强大,赶紧来看看

std::bind是C++11中一个函数模版,就像函数适配器,接受一个可调用对象(callable object),生成一个新的可调用对象。通过它,我们可以实现类似传统的函数指针,函数回调等功能,并且能够降低代码的复杂度。

本文首先详细说明std::bind的基本用法以及解释使用过程中疑问点,然后再介绍如何利用传统函数指针搭建基础结构,再说明如何用std::bind来代替函数指针,最后介绍如何用std::bind来实现函数回调的功能。

一、std::bind的基本用法

首先看下std::function, 它就是std::bind返回的新的可调用对象。如下图,定义实现了普通加法函数Add,  然后将该函数指针赋值给std::function类型的变量,这里可以注意到,使用了Add和&Add进行赋值。两者是等效的,这是因为使用Add的时候,会隐式转换成函数指针。

static int Add(int a, int b)
{
    return (a+b);
}

std::function<int (int, int)> fun = Add;
std::function<int (int, int)> fun2 = &Add;
LOG(INFO) << "fun(1, 1):"<< fun(1, 1);
LOG(INFO) << "fun2(1, 2):"<< fun2(1, 2);

运行程序之后的输出信息,可以看出std::function类型的变量的使用与普通函数的使用是一样的。

[2020-01-05 17:43:05,243189] [void JDebugBind::StartDebug():184] fun(1, 1):2
[2020-01-05 17:43:05,243206] [void JDebugBind::StartDebug():185] fun2(1, 2):3

我们不直接采用普通函数对std::function进行赋值,而是采用stb::bind,首先看下简单的实例,其中std::placeholders::_1和std::placeholders::_2是占位符,代表函数的入参。如果调用的时候,需要传递具体实参,那么就需要使用placeholders来占位。这里需要注意std::placeholders::_1并不是代表函数的第一个入参数,至于为什么,请继续往下阅读,下面将会通过实例进行阐述。

std::function<int (int, int)> fun3 = std::bind(Add, std::placeholders::_1, std::placeholders::_2);
std::function<int (int, int)> fun4 = std::bind(&Add, std::placeholders::_1, std::placeholders::_2);

LOG(INFO) << "fun3(1, 3):"<< fun3(1, 3);
LOG(INFO) << "fun4(1, 4):"<< fun4(1, 4);

如果函数的第二个入参是一个固定值,那么第一个入参就需要使用占位符std::placeholders::_1,如下所示,函数第二个参数固定位数值5,那么使用std::function类型变量的时候,也只需要传递一个参数,该参数代表Add函数的第一个参数。

std::function<int (int)> fun5 = std::bind(Add, std::placeholders::_1, 5);
LOG(INFO) << "fun5(1):"<< fun5(1);

如果Add函数的第一个入参是一个固定值,那么第二个入参就需要使用占位符std::placeholders::_1(注意不是std::placeholders::_2),如下所示,函数第一个参数固定位数值6,那么使用std::function类型变量的时候,也只需要传递一个参数,该参数代表Add函数的第二个参数。

std::function<int (int)> fun6 = std::bind(Add, 6, std::placeholders::_1);
LOG(INFO) << "fun6(1):"<< fun6(1);

当然,如果函数Add的两个参数都是固定值,那么使用std::function类型变量的时候,就不需要参数了。

std::function<int()> fun7 = std::bind(Add, 3, 7);
LOG(INFO) << "fun7():"<< fun7();

这里有个小技巧,如果不想要书写std::function那么繁琐的信息表示,那么可以采用auto代替,但是注意不要滥用auto.

auto fun8 = std::bind(Add, std::placeholders::_1, std::placeholders::_2);
LOG(INFO) << "fun8(1, 8):"<< fun8(1,8);

二、std::bind的扩展

上面说明的是stb::bind使用普通函数的方法,那么如果是类的成员函数呢?应该如何使用呢?首先s td::bind的第一个参数是类成员函数指针,第二个参数为类对象的指针,其他的用法与使用普通函数的用法是一样的。

class JBindClass
{
public:
    int Multi(int a, int b)
    {
        return (a * b);
    }
};


JBindClass bind_class;
auto fun9 = std::bind(&JBindClass::Multi, &bind_class, std::placeholders::_1, std::placeholders::_2);
LOG(INFO) << "fun9(1, 9):"<< fun9(1,9);

std::bind参数值是默认按照值传递的,首先实现函数Print, 该函数的入参是一个引用,函数内部将参数自增1,然后输出打印信息。接着再通过输出std::bind使用前后日志信息来确认是否是按照值传递。

static void Print(int &value)
{
    value++;
    LOG(INFO) << value;
}

int i_value = 10;
LOG(INFO) << "before i_value:" << i_value;
std::function<void()> fun10 = std::bind(Print,i_value);
fun10();
LOG(INFO) << "after i_value:" << i_value;

从输入的打印信息看,std::bind使用前后的信息没有发生变化,说明std::bind是默认按照值传递的。

[void JDebugBind::StartDebug():207] before i_value:10
[void Print(int &):24] 11
[void JDebugBind::StartDebug():210] after i_value:10

如果想要按照引用来传递变量,应该如何操作呢,那么就是std::ref登场的时候,std::ref是用于包装引用传递的值。

LOG(INFO) << "before i_value:" << i_value;
std::function<void()> fun11 = std::bind(Print,std::ref(i_value));
fun11();
LOG(INFO) << "after i_value:" << i_value;

从输出打印信息看,采用std::ref传递变量之后,std::bind使用前后的信息发生变化了。

[void JDebugBind::StartDebug():213] before i_value:10
[void Print(int &):24] 11
[void JDebugBind::StartDebug():216] after i_value:11

另外补充一点,std::cref用于包装const引用传递的值。

static void Printc(const int &value)
{
    LOG(INFO) << value;
}

int i_value_c = 12;
std::function<void()> fun12 = std::bind(Printc,std::cref(i_value_c));
fun12();

三、传统函数指针

函数指针变量用于存储函数指针,以便后续的调用。有时候可以利用它实现多个消息对象的处理,并且一定程度满足开闭原则。

首先实现抽象基类JAbstractBaseTest,接着再实现继承JAbstractBaseTest的两个子类JObjA和JObjB

/// 基类
class JAbstractBaseTest
{
public:
    JAbstractBaseTest(){}
    virtual ~JAbstractBaseTest(){}

    virtual void run() = 0;

};

/// 子类JObjA
class JObjA: public JAbstractBaseTest
{
public:
    void run(){LOG(INFO) << "JObjA Run";}

    static JAbstractBaseTest* create_instance()
    {
        return new JObjA();
    }

};

/// 子类JObjB
class JObjB: public JAbstractBaseTest
{
public:
    void run(){LOG(INFO) << "JObjB Run";}

    static JAbstractBaseTest* create_instance()
    {
        return new JObjB();
    }
};

完成上面的测试类,接着实现基础的框架,定义函数指针CreateObj,该函数指针用于动态创建对象,然后再分别实现初始化创建对象的函数指针映射表以及通过id从映射表中获取函数对象的两个函数。

class JDebugMain
{
public:
    JDebugMain()
    {
        InitObj();
    }

    // 定义函数指针
    typedef JAbstractBaseTest* (*CreateObj)();
    
    enum E_OBJ_ID
    {
        E_OBJ_A,
        E_OBJ_B,
    };

    // 初始化创建对象的函数指针映射表
    void InitObj()
    {
         m_mapRegisterClass[E_OBJ_A] = &JObjA::create_instance;
         m_mapRegisterClass[E_OBJ_B] = &JObjB::create_instance;
    }

    // 通过id从映射表中获取函数对象
    JAbstractBaseTest* GetObj(E_OBJ_ID eObjId)
    {
        std::map<E_OBJ_ID,CreateObj>::iterator iter;
        iter = m_mapRegisterClass.find(eObjId);
        if (iter != m_mapRegisterClass.end())
        {
            return m_mapRegisterClass[eObjId]();
        }
        return nullptr;
    }

private:
    std::map<E_OBJ_ID, CreateObj> m_mapRegisterClass;
};

使用调用方式如下,通过id获取对象指针,然后执行对象的run函数。通过这样的方式,可以做到主体循环不变,如果需要添加新的对象处理,那么只要实现新的类,然后添加到映射表中即可。

JDebugMain debug_main;
JAbstractBaseTest* p_obj = debug_main.GetObj(JDebugMain::E_OBJ_A);
if (p_obj)
{
    p_obj->run();
    delete p_obj;
    p_obj = nullptr;
}

四、std::bind代替函数指针

std::bind和std::function的结合,可以实现函数指针的功能。通过using Funtor = std::function<void (void)>来实现类似函数指针的声明。其中Funtor表示std::function<void (void)>的别名。然后在初始化表函数InitTab中,通过使用std::bind将类的函数成员一一映射到map中。

/// 类定义
class JDebugBind
{
public:
    using Funtor = std::function<void (void)>;
    enum
    {
        E_TEST_FUN_01,
        E_TEST_FUN_02,
    };

    JDebugBind();
    /// 根据测试id来执行对应的测试函数
    void RunTest(int iType);

protected:
    void Test01();
    void Test02();

private:
    void InitTab();

private:
    std::map<int, Funtor> m_mapTab;
};

/// 类实现
JDebugBind::JDebugBind()
{
    InitTab();
}

void JDebugBind::InitTab()
{
    m_mapTab.clear();
    m_mapTab[E_TEST_FUN_01] = std::bind(&JDebugBind::Test01, this);
    m_mapTab[E_TEST_FUN_02] = std::bind(&JDebugBind::Test02, this);
}

void JDebugBind::RunTest(int iType)
{
    std::map<int, Funtor>::iterator iter;
    for(iter = m_mapTab.begin(); iter != m_mapTab.end(); iter++)
    {
        if (iType == iter->first)
        {
            iter->second();
        }
    }
}

void JDebugBind::Test01()
{
    LOG(INFO) << "Test01";
}

void JDebugBind::Test02()
{
    LOG(INFO) << "Test02";
}

调用JDebugBind的方式如下,只需要传递函数的id给函数RunTest,即可执行到对应的函数。同样的,后续如果想要添加新的功能,那么只要实现新的函数,并且将其添加到map中即可。

JDebugBind debug_bind;
debug_bind.RunTest(JDebugBind::E_TEST_FUN_01);

五、std::bind实现函数回调

函数回调在编程实现是一个特别重要的特性,它经常会在一些架构中使用到。而std::bind是可以实现函数回调的特性的。下图实现的类JDebugCallback中,构造函数接受一个类型为std::function的参数之后,将其赋值给类的成员函数m_callback,后续调用函数Start的时候,Start函数内部再调用m_callback,从而实现函数回调。这里只是一个简单的例子说明,可能还不能充分看到函数回调的强大。希望这里作为一个引入,后续在实际工作中,再慢慢的体会。

class JDebugCallback
{
public:
    JDebugCallback(std::function<void()> callback)
        : m_callback(callback)
    { }

    void Start()
    {
        m_callback();
    }

private:
    std::function<void()> m_callback;
};

最后看下怎么使用JDebugCallback类,实现类两个函数CallBack01和CallBack02,然后通过std::bind传递给JDebugCallback,接着JDebugCallback对象调用Start来执行传递进来的函数。

static void CallBack01()
{
    LOG(INFO) << "CallBack01";
}

static void CallBack02()
{
    LOG(INFO) << "CallBack02";
}

JDebugCallback debug_cb_01(std::bind(CallBack01));
debug_cb_01.Start();

JDebugCallback debug_cb_02(std::bind(CallBack02));
debug_cb_02.Start();

五、总结

至此,C++11提供的std::bind的用法和扩展已经介绍完毕,虽然工作中有各种各样的需求场景,但是只要掌握了知识的基本原理,就能够以不变应万变。本文介绍了std::bind的各种基本应用场景,并结合了例子进行说明,相信应该已经说明白了。

析构函数的妙用, 让你明白流方式输出日志的实现原理

学习面向对象(如C++编程语言),那么肯定了解析构函数,它在对象销毁的时候被调用,通常我们在构造函数中申请资源,在析构函数中释放资源。那么析构函数在实现以流方式输出日志中有什么妙用呢?接下来请让我一步步为你揭开这层迷雾。

C/C++语言日志输出模式一般有两种,一种类似printf的方式,另一种类似std::cout的方式,这里说的流方式输出日志指的就是类似std::cout的方式,并且自定义日志输出的格式,同时既可以将日志输出到终端,也可以将日志输出到文件。

printf("%s  %d \n", "this is my log", 1);
std::cout << "this is my log " << 1 << std::endl;

一、格式化字符串的输出流

C++语言提供了ostringstream模版,它支持格式化字符串输出流。

  • 首先让我们看看ostringstream的简单使用,定义ostringstream变量oss,然后将当前的线程id以十六进制的方式写入ostringstream变量,  再调用ostringstream的函数str(),将其转换为std::string字符串之后,打印输出到终端。
#include <sstream>

std::ostringstream oss;
oss << std::hex << std::this_thread::get_id();
LOG(INFO) << oss.str();

输出的信息如下所示,当前的线程id是以十六进制的格式输出。

[2019-11-30 22:03:50,124554] [bool JDebugCPPAttr::TestOstringstream():277] 0x7fff9e22c380
  • 上面是ostringstream的简单使用方法,那么下面将说明如何构造输出函数名称和行号的字符串。通过利用系统提供的宏定义__func__和__LINE__来构造所需字符串信息。
std::ostringstream oss2;
oss2 << "[" << __func__ << ":" << __LINE__ << "]";
std::cout << oss2.str() << std::endl;

从输出的格式内容看,ostringstream按照预期的效果输出了正确的字符串格式。

[TestOstringstream:281]

二、资源获取即初始化

RAII全称是“Resource Acquisition is Initialization”,资源获取即初始化”,简单来说,就是说在构造函数中申请分配资源,在析构函数中释放资源。经常使用的方式是:构造函数中通过new申请内存,析构函数中通过delete释放内存。

  • 基于RAII的思想,我们实现资源管理的管理类,管理类ResourceManager构造函数接受std::function类型的变量, 将其赋值给类的私有成员变量exit_handle,析构函数内调用exit_handle,  那么如果想要实现满足RAII, 那么只要构建释放资源的std::function类型的变量,然后传递给 ResourceManager。
class ResourceManager
{
public:
    explicit ResourceManager(std::function<void()> fun):exit_handle(fun)
    {
        std::cout << "call constructor" << std::endl;
    }
    
    ~ResourceManager()
    {
        std::cout << "call destructor " << std::endl;
        exit_handle();
    }
    
private:
    std::function<void()> exit_handle;
};

申请创建内存,然后再创建ResourceManager对象,构造函数的入参是一个匿名函数,函数的功能是释放创建的内存。

{
    int *p_data = new int();
    ResourceManager( [&]()
                   {
                       std::cout << "delete p_data" << std::endl;
                       delete p_data;
                   });
}

运行程序之后,输出打印信息

call constructor
call destructor 
delete p_data
  • 同样的方式,我们可以创建文件之后,再创建ResourceManager对象,构造函数的参数功能是释放文件句柄。
{
    std::ofstream ofs("test.txt");
    ResourceManager( [&]
                   {
                       std::cout << "close ofs" << std::endl;
                       ofs.close();
                   });
}

运行程序之后,输出打印信息

call constructor
call destructor 
close ofs
  • 从上面的两个例子中,可以看出都是利用对象在销毁时会调用析构函数的原理来实现,简单来说,申请资源之后,紧接着设置释放资源,等到申请的资源使用完成之后,资源管理对象在退出作用域之后,就会调用析构函数来释放资源,这样做的好处是,我们不必关注资源什么时候进行释放的问题,同时一定程度上也防止忘记释放资源。

三、利用析构函数来实现日志输出

结合std::ostringstream可以格式化输出流的功能和对象销毁时调用析构函数的原理,我们就可以实现自定义格式,并以流方式输出日志的功能。

  • 实现JWriter类来格式化日志信息并输出,这里我们只是简单输出到终端,当然,你也可以将自定义格式的日志信息写入文件或者写入队列,再由线程将队列中的日志信息写入文件。
  • JWriter类的构造函数接受三个参数:日志等级、函数名称、行号;并且重载了operator<<运算符
///类定义
class JWriter
{
public:
    explicit JWriter(const std::string &strLevel, const std::string &strFun, int iLine);
    ~JWriter();

    // 重载operator<<运算符
    template <typename T>
    inline JWriter& operator<<(const T& log) {
        m_log << log;
        return *this;
    }

private:
    std::string GetSysTimeToMs();

private:
    std::ostringstream m_log;
};


///类实现
#include <iostream>
#include <thread>
#include <chrono>
#include <sys/timeb.h>

JWriter::JWriter(const std::string &strLevel, const std::string &strFun, int iLine)
{
    m_log <<"["<< GetSysTimeToMs() << "]" << "[" << strFun << ":" << iLine << "]" << "[" << strLevel << "] ";
}


JWriter::~JWriter()
{
    m_log << std::endl;
    /// 这里可以实现将日志输出到终端或者写入文件
    std::cout << m_log.str();
}

std::string JWriter::GetSysTimeToMs()
{
    time_t timep;
    struct timeb tb;

    time (&timep);
    char tmp[128] ={0};
    strftime(tmp, sizeof(tmp), "%Y-%m-%d %H:%M:%S",localtime(&timep) );

    char tmp2[128] ={0};
    ftime(&tb);
    snprintf(tmp2,sizeof(tmp2),"%d",tb.millitm);

    std::ostringstream buffer;
    buffer << tmp << "." << tmp2 ;

    return buffer.str();
}

  • 那么如何来使用JWriter类,使用效果又是怎样呢?其实很简单,定义如下所示的宏,该宏只接受日志等级的字符串参数。
#define MyLogJ(LEVEL) JWriter(LEVEL, __func__, __LINE__)
  • 调用方式如下所示,它跟我们熟悉使用的std::cout的方式是一样一样的,只是std::cout换成了我们实现的MyLogJ()宏,因此,不存在需要花费时间来学习它的使用的问题。
MyLogJ("INFO") << "hello " << 123;
MyLogJ("INFO") << "hello " << " world";
  • 如下所示输出的效果,它首先输出日期时间,然后是函数名和对应行号以及日志等级,最后才输出用户输入的日志信息。这样的格式,通常是比较美观,并且利于问题的定位,当然,你也可以根据个人的喜好来修改JWriter的构造函数来自定义自己的日志格式。
[2019-12-01 10:26:00.657][TestMyLog:266][INFO] hello 123
[2019-12-01 10:26:00.657][TestMyLog:267][INFO] hello  world

四、总结

自定义日志格式并以流方式输出的功能已经介绍结束,它是利用了std::ostringstream可以格式化输出流的功能,并且在构造函数格式日志信息,析构函数最后处理日志信息,同时重载了operator<<运算符。

析构函数不只是用于释放资源,我们可以利用它的特性来做其他的运用,就如本文介绍的一样,利用了析构函数实现了流方式的日志功能,如果没有,单纯利用构造函数很难实现流方式的日志功能。当然,析构函数可能还有其他妙用,这需要我们不断去发掘。

 

基于future和promise实现的异步收发数据模版类

std::future和std::promise两者结合可以实现异步的功能场景,本文将介绍的异步收发数据模版类是在实践中结合std::future和std::promise而摸索出来的。

工作过程中,我们可能会经常遇到这样的场景,需要从线程中获取运行的结果。现在我们有两种方式可以实现这样的效果。

  • 第一种方式,属于通用用法,通过使用指针在线程间共享数据。传递指针给新建的线程,主线程使用条件变量等待被唤醒;当线程设置完成数据到传递过来的指针之后,发送条件变量信号,主线程被唤醒之后,从指针中提取数据。这种方式采用条件变量、锁、指针结合才实现了异步功能,比较复杂。
  • 第二种方式,采用std::future和std::promise对象,也就是本文接下来要详细说明的一种异步实现方式。
  • std::future是一个类模版,内部存储一个将来用于分配的值,它提供了get()成员函数来访问该值的机制。如果关联值可用之前,调用了get函数,那么get函数将阻塞直到关联值不可用。
  • std::promise也是一个类模版,它用来设置上面的关联值,每一个stb::promise和一个std::future对象关联,一旦stb::promise设置值之后,std::future对象的get()函数就会获取到值,然后返回。std::promise与它关联的std::future共享数据。

一、阻塞等待获取数据

1、实现线程执行函数,入参是一个std::promise指针,函数内调用std::promise指针设置值

void thread_function(std::promise<std::string>* pPromiseObj)
{
    if(nullptr == pPromiseObj)
    {
        return;
    }
    
    pPromiseObj->set_value("this is my name.");
}

2、定义std::promise对象,从该对象获取关联的std::future对象,启动线程并且传入std::promise对象的指针,调用std::future对象的get()函数阻塞等待,如果返回,那么打印输出返回的字符串信息。

// 定义std::promise对象,从该对象获取关联的std::future
std::promise<std::string> promise_obj;
std::future<std::string> future_obj = promise_obj.get_future();

// 启动线程
std::thread thread_obj(&thread_function, &promise_obj);

// 阻塞等待
std::string str = future_obj.get();
std::cout << "std = " << str << std::endl;

// 等待线程退出
thread_obj.join();

3、运行程序,输出的信息如下所示,从这里可以看出,std::promise在线程中设置值之后,std::future对象的get()函数成功获取并返回。

二、通知线程退出

基于std::promise和std::future的机制,我们可以利用std::promise的set_value来通知运行的线程退出。具体如何做呢,我们接下来给出例子进行说明。

1、实现线程的执行函数,入参为与std::promise关联的std::future对象,执行函数内部调用std::future的wait_for循环超时等待,如果std::future的wait_for在超时时间内没有收到std::promise调用set_value发送的信号,那么继续循环等待,如果在超时时间内收到std::promise调用set_value发送的信号,那么退出循环,同时线程页退出了。

void JThreadFunction(std::future<void> FutureObj)
{
    // 调用std::future的wait_for循环超时等待
    while(FutureObj.wait_for(std::chrono::milliseconds(1))
          == std::future_status::timeout)
    {
        std::cout << "do something" << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }
}

2、创建std::promise对象,从std::promise对象提取关联的future对象,启动线程,并且将上面的future对象传递给线程,主线程休眠一段时间之后,调用std::promise对象的set_value函数来发送信号,通知线程退出。

//创建promise对象
std::promise<void> exit_signal;

//提取future对象
std::future<void> future_obj = exit_signal.get_future();

//启动线程
std::thread thread_obj(JThreadFunction, std::move(future_obj));

//休眠
std::this_thread::sleep_for(std::chrono::seconds(3));

//发送信号
std::cout << "send signal" << std::endl;
exit_signal.set_value();

//等待线程退出
thread_obj.join();
std::cout << "exit function" << std::endl;

3、从输出的结果信息看,线程一直在运行,当收到std::promise对象发送信号的信号之后就退出。

三、异步收发数据

经过上面两个例子的讲解,相信大家对std::future和std::promise已经有了一个大概的了解。下面就给出异步收发数据的模版类。

1、类模版JAsyncSender实现两个函数,一个是Send用于发送数据,它可以在线程中执行,另一个是Wait等待接收数据,如果第三个参数没有输入,那么默认一直等待,否则在指定时间内,没有收到信息,那么返回失败。

#ifndef JASYNCSENDER_H
#define JASYNCSENDER_H

#include <future>
#include <chrono>
#include <thread>
#include "log/easylogging++.h"

///
/// 模版类声明
///
template <class RealT>
class JAsyncSender
{
public:
    JAsyncSender();
    ~JAsyncSender();

    // 发送数据
    bool Send(const RealT &data);
    // 等待接收数据,需要先运行
    bool Wait(std::promise<RealT> promiseObj, RealT &data, unsigned int uiTimeMills = 0);
private:
    std::promise<RealT> m_promiseObj;
};


///
/// 类模版实现
///
template  <typename RealT>
JAsyncSender<RealT>::JAsyncSender()
{

}

template  <typename RealT>
JAsyncSender<RealT>::~JAsyncSender()
{

}

template  <typename RealT>
bool JAsyncSender<RealT>::Send(const RealT &data)
{
    try
    {
        m_promiseObj.set_value(data);
    } catch (const std::exception &e)
    {
        LOG(INFO) << "exception: " << e.what();
    }
    return true;
}


template  <typename RealT>
bool JAsyncSender<RealT>::Wait(std::promise<RealT> promiseObj, RealT &data, unsigned int uiTimeMills)
{
    std::future<RealT> future_obj = promiseObj.get_future();
    m_promiseObj = std::move(promiseObj);
    if (uiTimeMills > 0)
    {
        while(future_obj.wait_for(std::chrono::milliseconds(uiTimeMills))
              == std::future_status::timeout)
        {
            return false;

        }
    }
    data = future_obj.get();
    return true;
}

#endif // JASYNCSENDER_H

2、接下来说明类模版JAsyncSender的使用方法

  • 定义成员变量m_AsyncSendInt,它由主线程和子线程共享。JAsyncSender的type为整型,也可以定义为字符串,甚至是自定义对象,根据具体需求场景具体定义。
    JAsyncSender<int> m_AsyncSendInt;
  • 通过lambda方式创建线程,当然你也可以使用其他方式,线程内部先休眠一段时间,然后发送数据。
// 通过lambda方式创建线程
std::thread thread_obj( [&]{
     LOG(INFO) <<  ": lambda thread executing";
     std::this_thread::sleep_for(std::chrono::seconds(3));
     m_AsyncSendInt.Send(20);
 } ) ;


 std::promise<int> promise_obj;
 int i_data = -1;
 // 等待线程返回数据
 m_AsyncSendInt.Wait(std::move(promise_obj), i_data);
 LOG(INFO) <<  "i_data: " << i_data;
 if (thread_obj.joinable())
 {
     thread_obj.join();
 }
  • 从运行结果看,基于future和promise实现的异步收发数据模版类的功能是正常的。
[2019-11-17 19:29:01,539829] [auto JDebugCPPAttr::TestAsyncSender()::(anonymous class)::operator()() const:235] : lambda thread executing
[2019-11-17 19:29:04,542497] [bool JDebugCPPAttr::TestAsyncSender():244] i_data: 20

四、总结

std::promise与std::future的结合使用,可以更加容易处理异步消息事件,另外C++11标准中提供的 std::asych和std::packaged_task也是结合std::future来处理异步的事件流程。std::promise与std::future虽然功能强大,但是std::promise与std::future是一一对应的,目前没有办法处理一对多的问题,比如一个std::promise对应多个std::future。std::promise如果设置过一次,再次设置会报错,如果需要重新使用,需要再创建std::promise对象。

C++11:启动线程的五种方式

程序开发过程中,面对各种各样的需求场景,其中涉及到线程的应用,本文将总结启动线程的五种方式,方便后续复习以及查阅。

  1. 函数指针方式启动线程
  2. 函数对象方式启动线程
  3. Lambda函数的方式启动线程
  4. 类函数指针的方式启动线程
  5. std::bind的方式启动线程

一、函数指针方式

1、 定义实现线程处理函数,然后启动线程调用该处理函数

void ThreadFunction(void)
{
    std::cout << __func__ << ": thread executing "  << std::endl;
}

// 函数指针方式启动线程
void JTestThread::TestStartThread_Method01(void)
{
    std::thread thread_obj(ThreadFunction);
    if (thread_obj.joinable())
    {
        thread_obj.join();
    }
}

2、 运行结果

ThreadFunction: thread executing

二、函数对象方式

1、创建类ThreadOpeartor,然后重载operator()运算符,然后以仿函数的形式作为线程的参数来启动线程

class ThreadOpeartor
{
public:
    void operator()()
    {
        std::cout << __func__ << ":  thread executing "  << std::endl;
    }
    
};

// 函数对象方式启动线程
void JTestThread::TestStartThread_Method02(void)
{
    std::thread thread_obj( (ThreadOpeartor()) ) ;
    if (thread_obj.joinable())
    {
        thread_obj.join();
    }
}

2、运行结果

operator():  thread executing

三、lambda方式

1、lambda函数即匿名函数作为线程的参数来启动线程

// Lambda函数的方式启动线程
void JTestThread::TestStartThread_Method03(void)
{
    std::thread thread_obj( []{
        std::cout << __func__ << ": lambda thread executing" << std::endl;
    } ) ;
    if (thread_obj.joinable())
    {
        thread_obj.join();
    }
}

2、运行结果

operator(): lambda thread executing

四、类函数指针方式

1、定义类成员函数,然后以函数指针作为参数来启动线程

void JTestThread::TestThread04(void)
{
    std::cout << __func__ << ": thread executing" << std::endl;
}

// 类函数指针的方式启动线程
void JTestThread::TestStartThread_Method04(void)
{
    std::thread thread_obj(&JTestThread::TestThread04, this) ;
    if (thread_obj.joinable())
    {
        thread_obj.join();
    }
}

2、运行结果

TestThread04: thread executing

五、std::bind方式

1、类成员函数作为stb::bind的参数, 然后stb::bind作为线程参数来启动线程

void JTestThread::TestThread05(void)
{
    std::cout << __func__ << ": thread executing" << std::endl;
}

// std::bind的方式启动线程
void JTestThread::TestStartThread_Method05(void)
{
    std::thread thread_obj(std::bind(&JTestThread::TestThread05, this)) ;
    if (thread_obj.joinable())
    {
        thread_obj.join();
    }
}

2、运行结果

TestThread05: thread executing

六、总结

  • 函数指针方式:定义普通函数
  • 函数对象方式:重载operator()运算符
  • Lambda函数方式:匿名函数
  • 类函数指针的方式:对象函数指针
  • std::bind的方式: 实际上也是对象函数指针

七、参考链接

如何正确创建线程

怎样通过函数对象创建独立线程