利用C++11的function和bind简化类创建线程
问题引出
当在类中需要创建线程时,总是因为线程函数需要定义成静态成员函数,但是又需要访问非静态数据成员这种需求,来做若干重复性的繁琐工作。比如我以前就经常定义一个静态成员函数,然后定一个结构体,结构体形式如下所示,将类指针传入到线程函数中以方便访问费非态成员变量。
struct THREAD_PARAMER { CTestClass* pThis; PVOID pContext; }
解决问题
其实这里不算解决问题吧,应该是用一些其他的方式来减少这种重复性工作。
根据线程函数的要求,除了可以弄成静态成员函数外,其实也可以是全局函数。所以其实不定义静态成员函数也可以在类中创建线程,那重点就是如何把类对象指针、具体执行的函数、需要传递的上下文参数这三个类内部的信息传递到全局的线程函数中呢?
我想到的方法仍然脱离不了封装,因为实际的线程函数只接受一个参数,如果要传递三个过去,必然需要封装出一个新的类型来进行传递。
所以这里要在全局线程中去间接调用类中的成员函数,达到让这个成员函数伪装成线程函数的目的,首先要做两点:
1、封装API函数CreateThread,直接传递类对象指针、成员函数、上下文参数进去就能创建线程,并执行到成员函数中去。
2、对于不同的类,方法要一致,这里就考虑使用模板参数来代替类类型。
有必要在这里先声明一下,下面的内容都是我自己根据当时知识程度一步一步深入的过程,所以如果要找最好的解决方案,可以直接看最后的版本,或者直接去我的github上迁移代码(肯定是我目前最新的)。
第一版:使用模板类作为容器保存参数(VS2010)
有了上面的总结,经过实验写出了如下代码来简化在类中创建线程,首先上测试代码,这部分代码后面不再改变。
#include <iostream> #include "ThreadInClass.h" class CTest { public: void SayHelloInThread(char* nscName) { ThreadInClass::CThreadActuator<CTest>::StartThreadInClass(this, &CTest::ThreadWork, nscName); } DWORD ThreadWork(void* p) { HANDLE lhThread = GetCurrentThread(); if(NULL != lhThread) { DWORD ldwThreadID = GetThreadId(lhThread); std::cout << "子线程ID: " << ldwThreadID << std::endl; CloseHandle(lhThread); } std::cout << "hello, " << (char*)p << std::endl; return 0; } }; void main() { HANDLE lhMainThread = GetCurrentThread(); if(NULL != lhMainThread) { DWORD ldwThreadID = GetThreadId(lhMainThread); std::cout << "主线程ID: " << ldwThreadID << std::endl; CloseHandle(lhMainThread); } CTest loTest; char* lscName = "colin"; loTest.SayHelloInThread(lscName); system("pause"); return; }
下面是封装的类模板,注意我这里简化了StartThreadInClass函数没有列出CreateThread的可用参数,如果需要的话在这里加上即可。
#include <iostream>
#include<functional>
using std::function;
#include<Windows.h>
namespace ThreadInClass{
// 参数容器模板类,用于存放要调用的类对象、函数、及其参数。(返回值不用存放,因为返回值要作为线程结束状态,所以必须为DWORD)
template<typename tClassName>
class CRealThreadParamer
{
private:
typedef std::function<DWORD(tClassName*, PVOID)> RealExcuteFun;
RealExcuteFun mfExcuteFun;
tClassName* mpoInstance;
PVOID mpoContext;
public:
CRealThreadParamer(tClassName* npThis, RealExcuteFun nfWorkFun, PVOID npContext){
mpoInstance= npThis;
mfExcuteFun= nfWorkFun;
mpoContext= npContext;
}
DWORD Run()
{
return mfExcuteFun(mpoInstance, mpoContext);
}
};
// 线程创建执行类,用于提供创建线程和执行线程的接口封装
template<typename tClassName>
class CThreadActuator
{
public:
typedef CRealThreadParamer<tClassName> CThreadParamer;
typedef std::function<DWORD(tClassName*, PVOID)> RealExcuteFun;
static HANDLE StartThreadInClass(tClassName* npThis, RealExcuteFun nfWorkFun, PVOID npContext)
{
CThreadParamer* lpoParamer = new CThreadParamer(npThis, nfWorkFun, npContext);
return CreateThread(nullptr, 0, CThreadActuator::ThreadDispatch, (PVOID)lpoParamer, 0, nullptr);
}
static DWORD WINAPI ThreadDispatch(PVOID npParam)
{
if(nullptr == npParam)
return 0;
else
{
CThreadParamer* lfThreadParamer = (CThreadParamer*)npParam;
DWORD ldwRet= lfThreadParamer->Run();
delete lfThreadParamer;
lfThreadParamer= NULL;
return ldwRet;
}
}
};
}
我这里用到了std::funciton,而这里的用法有点类似于使用typddef的方式去声明一种函数类型。
执行结果如下:
第二版:使用bind将参数绑定到一个function上(VS2010)
当再次查看这部分代码时,我发现CReadThreadParamer的作用就是一个提供调用形如DWORD (tClassName*, PVOID)函数的接口,并且一旦创建了,它的调用形式也固定了(因为参数都是构造的时候就传递进去了)。
这让我想到了bind,平常使用这个不多,但是知道它可以绑定到一个函数上,并减少或者增加这个函数的参数来调用。既然我这里参数都是固定死了,那是不是可以使用bind先把这些参数全部绑定上去,然后在调用的时候只需调用形如DWORD()的函数就可以了呢?
经过尝试,CReadThreadParamer现在可以优化成这个样子了:
template<typename tClassName> class CRealThreadParamer { private: typedef std::function<DWORD(tClassName*, PVOID)> RealExcuteFun; typedef std::function<DWORD()> NewRealExcuteFun; NewRealExcuteFun mfExcuteFun; public: CRealThreadParamer(tClassName* npThis, RealExcuteFun nfWorkFun, PVOID npContext) { mfExcuteFun = std::tr1::bind(nfWorkFun, npThis, npContext); } DWORD Run() { return mfExcuteFun(); } };
第三版:传递function类型指针作为参数给线程函数(VS2010)
再细细看了下现在的CRealThreadParamer,构造函数里直接把所有的参数绑定到了实际执行的函数上,所以类内部只需要保存一个std::function类型了。
等等,既然只有一个std::function类型了,那我之前增加这个类来保存三个类中的参数还有什么意义,直接传递这么一个类型不就行了吗?
也就是说,应该是可以在StartThreadInClass的实现中就把所有参数绑定成一个函数调用,然后保存到std::function传递给线程函数,线程函数再执行这个函数就行了。
根据上述思路,进一步优化后,代码简化了很多很多了,如下:
#include <iostream> #include <functional> using std::function; #include <Windows.h> namespace ThreadInClass{ // 线程创建执行类,用于提供创建线程和执行线程的接口封装 template<typename tClassName> class CThreadActuator { public: typedef std::function<DWORD(tClassName*, PVOID)> RealExcuteFun; typedef std::function<DWORD()> NewRealExcuteFun; static HANDLE StartThreadInClass(tClassName* npThis, RealExcuteFun nfWorkFun, PVOID npContext) { NewRealExcuteFun* lpoParamer = new NewRealExcuteFun(std::tr1::bind(nfWorkFun, npThis, npContext)); return CreateThread(nullptr, 0, CThreadActuator::ThreadDispatch, (PVOID)lpoParamer, 0, nullptr); } static DWORD WINAPI ThreadDispatch(PVOID npParam) { if(nullptr == npParam) return 0; else { NewRealExcuteFun* lfThreadParamer = (NewRealExcuteFun*)npParam; DWORD ldwRet = (*lfThreadParamer)(); delete lfThreadParamer; lfThreadParamer = NULL; return ldwRet; } } }; }
第四版:使用变长模板参数解决参数类型单一的缺陷(VS2013)
到了第三版,我再没有想到还可以简化的方式了,不过到是发现了,如果在使用的时候,我需要传入的上下文内容比较多,还是需要自己构造一个结构体来存放上下文信息。因为类中用来做线程函数(间接的)的形式是固定为DWORD(PVOID)类型的。
那么有没有一种方式可以让这个函数可以有任意多个不同类型的参数呢?其实是有的,那就是使用C++11的类可变参模板。
在更改代码之前,先测试一下直接使用tuple类型作为上下文参数传递,因为它可以存放很多不同类型的数据到 一个变量中,从某种程度上也是可以满足多个上下文参数的。
测试代码如下:
#include <iostream> #include "ThreadInClass.h" #include <tuple> using std::tr1::tuple; class CTest { public: void SayHelloInThread(char* nscName) { ThreadInClass::CThreadActuator<CTest>::StartThreadInClass(this, &CTest::DoSayHelloInThread, nscName); } DWORD DoSayHelloInThread(void* p) { HANDLE lhThread = GetCurrentThread(); if(NULL != lhThread) { DWORD ldwThreadID = GetThreadId(lhThread); std::cout << "DoSayHelloInThread线程ID: " << ldwThreadID << std::endl; CloseHandle(lhThread); } std::cout << "hello, " << (char*)p << std::endl; return 0; } void PrintSumInThread(tuple<int, int>& roAddTupleInfo) { ThreadInClass::CThreadActuator<CTest>::StartThreadInClass(this, &CTest::DoPrintSumInThread, &roAddTupleInfo); } DWORD DoPrintSumInThread(void* p) { HANDLE lhThread = GetCurrentThread(); if(NULL != lhThread) { DWORD ldwThreadID = GetThreadId(lhThread); std::cout << "DoPrintSumInThread线程ID: " << ldwThreadID << std::endl; CloseHandle(lhThread); } std::tr1::tuple<int, int>* lpoT1 = (std::tr1::tuple<int, int>*)p; int i = std::tr1::get<0>(*lpoT1); int j = std::tr1::get<1>((*lpoT1)); std::cout << i << " + " << j << " = " << i + j << std::endl; return 0; } }; void main() { HANDLE lhMainThread = GetCurrentThread(); if(NULL != lhMainThread) { DWORD ldwThreadID = GetThreadId(lhMainThread); std::cout << "主线程ID: " << ldwThreadID << std::endl; CloseHandle(lhMainThread); } CTest loTest; char* lscName = "colin"; loTest.SayHelloInThread(lscName); tuple<int, int> t1(1, 2); loTest.PrintSumInThread(t1); system("pause"); return; }运行结果:
经实验是可以的。不过相对于使用变参模板而言,这种方式需要使用者自己定义出一个tuple,来存放所有要传递的数据,还是不如直接传递来的直观。
接下来更改代码使用变参模板。
注意:截止到上面测试使用tuple,我一直使用的是VS2010版本。但是当我使用变长参数模板时,发现编译不过去,看错误提示似乎是还不支持,所以下面我更换到了VS2013,但是VS2013上要将类成员函数赋值给std::function类型时,必须使用bind才行,所以传递参数时要注意。
#include <iostream> #include <functional> using std::tr1::function; using std::tr1::bind; #include <Windows.h> namespace ThreadInClass{ template<typename tClassName, typename... ArgsType> // 变参模板 class CThreadActuator { public: typedef function<DWORD()> NewRealExcuteFun; /// 使用变参模板 static HANDLE StartThreadInClass(tClassName* npThis, function<DWORD(tClassName*, ArgsType...)> nfWorkFun, ArgsType... npArgs) { NewRealExcuteFun* lpoParamer = new NewRealExcuteFun(bind(nfWorkFun, npThis, npArgs...)); return CreateThread(nullptr, 0, CThreadActuator::ThreadDispatch, (PVOID)lpoParamer, 0, nullptr); } // 真正的线程函数,间接调用类成员函数 static DWORD WINAPI ThreadDispatch(PVOID npParam) { if (nullptr == npParam) return 0; else { NewRealExcuteFun* lfThreadParamer = (NewRealExcuteFun*)npParam; DWORD ldwRet = (*lfThreadParamer)(); delete lfThreadParamer; lfThreadParamer = NULL; return ldwRet; } } }; }
附上测试代码:
#include <iostream> using std::cout; using std::endl; #include "ThreadInClass.h" #include <tuple> using std::tr1::tuple; using std::tr1::get; class CTest { public: void SayHelloInThread(char* nscName) { ThreadInClass::CThreadActuator<CTest, char* >::StartThreadInClass(this, bind(&CTest::DoSayHelloInThread, std::tr1::placeholders::_1, std::tr1::placeholders::_2), nscName); } DWORD DoSayHelloInThread(void* p) { HANDLE lhThread = GetCurrentThread(); if(NULL != lhThread) { DWORD ldwThreadID = GetThreadId(lhThread); cout << "DoSayHelloInThread线程ID: " << ldwThreadID << endl; CloseHandle(lhThread); } cout << "hello, " << (char*)p << endl; return 0; }void PrintSumInThread(tuple<int, int>& roAddTupleInfo) { ThreadInClass::CThreadActuator<CTest, tuple<int, int>& >::StartThreadInClass(this, bind(&CTest::DoPrintSumInThread, std::tr1::placeholders::_1, std::tr1::placeholders::_2), roAddTupleInfo); } DWORD DoPrintSumInThread(tuple<int, int>& roAddTupleInfo) { HANDLE lhThread = GetCurrentThread(); if(NULL != lhThread) { DWORD ldwThreadID = GetThreadId(lhThread); cout << "DoPrintSumInThread线程ID: " << ldwThreadID << endl; CloseHandle(lhThread); } int i = get<0>(roAddTupleInfo); int j = get<1>(roAddTupleInfo); cout << i << " + " << j << " = " << i + j << endl; return 0; } void PrintSumInThread2(int &i, int &j) { ThreadInClass::CThreadActuator<CTest, int, int >::StartThreadInClass(this, bind(&CTest::DoPrintSumInThread2, std::tr1::placeholders::_1, std::tr1::placeholders::_2, std::tr1::placeholders::_3), i, j); } DWORD DoPrintSumInThread2(int i, int j) { HANDLE lhThread = GetCurrentThread(); if (NULL != lhThread) { DWORD ldwThreadID = GetThreadId(lhThread); std::cout << "DoPrintSumInThread2线程ID: " << ldwThreadID << std::endl; CloseHandle(lhThread); } std::cout << i << " + " << j << " = " << i + j << std::endl; return 0; } }; void main() { HANDLE lhMainThread = GetCurrentThread(); if(NULL != lhMainThread) { DWORD ldwThreadID = GetThreadId(lhMainThread); cout << "主线程ID: " << ldwThreadID << endl; CloseHandle(lhMainThread); } CTest loTest; char* lscName = "colin"; loTest.SayHelloInThread(lscName); tuple<int, int> t1(1, 2); loTest.PrintSumInThread(t1); int i = 4; int j = 5; loTest.PrintSumInThread2(i, j); system("pause"); return; }执行结果如下:
第五版:直接传递绑定好所有参数的function(VS2013)
上面有提到,VS2013要将类成员函数赋值给function类型,必须使用bind。
所以实际调用的时候传递参数时,是将成员函数通过bind(&CTest::DoPrintSumInThread2, std::tr1::placeholders::_1, std::tr1::placeholders::_2, std::tr1::placeholders::_3), 这么传递的。其中参数占位符根据成员函数实际的参数个数来定。而参数在这里创建线程的时候也是固定了的,既然如此,我干嘛还用占位符呢?直接传递bind(&CTest::DoPrintSumInThread2, this, i, j)不就行了吗?
经测试上述方案是可行的,仔细想了下,在CreateThread的时候我们需要传递一个参数给线程函数,这个参数类型我们定义成了function类型,而上面这种方式其实也是function啊,并且对于任何已经知道要传递的参数值的成员函数,都可以通过bind,返回function< DWORD() >类型。这就意味着在线程函数里,我根本不需要知道其他信息,只需要执行这个function代表的函数就可以了啊。
茅塞顿开,有了如下代码:
#include <iostream> #include <functional> using std::tr1::function; using std::tr1::bind; #include <Windows.h> namespace ThreadInClass{ class CThreadActuator { public: typedef function<DWORD()> NewRealExcuteFun; // 使用变参模板 static HANDLE StartThreadInClass(function<DWORD()> nfWorkFun) { NewRealExcuteFun* lpoParamer = new NewRealExcuteFun(nfWorkFun); return CreateThread(nullptr, 0, CThreadActuator::ThreadDispatch, (PVOID)lpoParamer, 0, nullptr); } // 真正的线程函数,间接调用类成员函数 static DWORD WINAPI ThreadDispatch(PVOID npParam) { if (nullptr == npParam) return 0; else { NewRealExcuteFun* lpfWorkFun = (NewRealExcuteFun*)npParam; DWORD ldwRet = (*lpfWorkFun)(); delete lpfWorkFun; lpfWorkFun = NULL; return ldwRet; } } }; }#include <iostream> using std::cout; using std::endl; #include "ThreadInClass.h" class CTest { public: void PrintSumInThread(int &i, int &j) { ThreadInClass::CThreadActuator::StartThreadInClass(bind(&CTest::DoPrintSumInThread, this, i, j)); } DWORD DoPrintSumInThread(int i, int j) { HANDLE lhThread = GetCurrentThread(); if (NULL != lhThread) { DWORD ldwThreadID = GetThreadId(lhThread); std::cout << "DoPrintSumInThread2线程ID: " << ldwThreadID << std::endl; CloseHandle(lhThread); } std::cout << i << " + " << j << " = " << i + j << std::endl; return 0; } }; void main() { HANDLE lhMainThread = GetCurrentThread(); if(NULL != lhMainThread) { DWORD ldwThreadID = GetThreadId(lhMainThread); cout << "主线程ID: " << ldwThreadID << endl; CloseHandle(lhMainThread); } CTest loTest; int i = 4; int j = 5; loTest.PrintSumInThread(i, j); system("pause"); return; }
呀,不仅去掉了模板,代码也简洁了好多倍。
第六版
是的,没错还有第6版本。那就是使用c++11的std::thread,使用方式就不多说了,我也是看的别人的介绍。跟我前面介绍的方式差不多,不过额外增加了很多功能就是了。不过也不是说我前面写的都没用,很大程度上thread内部用的方式其实就是差不多的。
然后对比下我的第四版,第四版中是将类类型和变参参数作为类的模板了。而实际上我那个类里面除了StartThreadInClass中使用了这两个模板参数,其他地方都没有使用。所以其实是可以直接定义成这个函数的模板参数的,然后配合第五版的直接绑定所有参数的方式:
#include <iostream> #include <functional> using std::tr1::function; using std::tr1::bind; #include <Windows.h> namespace ThreadInClass{ class CThreadActuator { public: typedef function<DWORD()> NewRealExcuteFun; // 使用变参模板 template <typename _Fn, typename... _Args> static HANDLE StartThreadInClass(_Fn nfWorkFun, _Args... args) { NewRealExcuteFun* lpfWorkFun = new NewRealExcuteFun(bind(nfWorkFun, args...)); return CreateThread(nullptr, 0, CThreadActuator::ThreadDispatch, (PVOID)lpfWorkFun, 0, nullptr); } // 真正的线程函数,间接调用类成员函数 static DWORD WINAPI ThreadDispatch(PVOID npParam) { if (nullptr == npParam) return 0; else { NewRealExcuteFun* lpfWorkFun = (NewRealExcuteFun*)npParam; DWORD ldwRet = (*lpfWorkFun)(); delete lpfWorkFun; lpfWorkFun = NULL; return ldwRet; } } }; }然后发现这种方式似乎连this指针都不需要传入了,也就是说对于除了类成员函数做线程函数的情况,其他普通函数也可以直接使用了。
调用方式为:
ThreadInClass::CThreadActuator::StartThreadInClass(&AddInThread, i, j);与std::thread的区别就是,我这里函数的返回类型必须是DWORD,因为在ThreadDispatch函数里我需要返回这个函数的返回值。欧了,这是最终版本了。
至此再也没有新的版本了- -。。