問題引出
當在類中需要創建線程時,總是因為線程函數需要定義成靜態成員函數,但是又需要訪問非靜態數據成員這種需求,來做若干重復性的繁瑣工作。比如我以前就經常定義一個靜態成員函數,然后定一個結構體,結構體形式如下所示,將類指針傳入到線程函數中以方便訪問費非態成員變量。
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函數里我需要返回這個函數的返回值。歐了,這是最終版本了。
至此再也沒有新的版本了- -。。