文章目錄
- C++ 多線程之std::thread淺析
-
- 1. Native 的多線程
- 2. std::thread
-
- 2.1 構造函數
- 2.2 join
- 2.3 detach
- 2.4 析構
- 3. 總結
C++ 多線程之std::thread淺析
現代作業系統能夠呈現給使用者各式各樣的形态,跟多線程是離不開的,例如我們在聽歌的軟體中可以聽歌同時也可以搜尋其他歌曲。
為了支援多線程操作,C++引入了
std::thread
, 本文來探讨一下多線程的使用和基本原理。
1. Native 的多線程
如果在windows環境下面,使用多線程開發,那麼可以使用
CreateThread
底層接口來建立線程,例如如下:
UINT _stdcall ThreadProc(PVOID param)
{
int data = (int)param;
std::cout << "Thread Running. " << data << std::endl;
return 0;
}
void MyThread()
{
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, (LPVOID)100, 0, NULL);
if (hThread!= NULL)
{
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
hThread = NULL;
}
}
如果在windows下面開發過程式的話,肯定是寫過這種代碼的(如果沒有的話,那我猜測估計你寫了個假的Windows代碼)。
但是這個原生态的線程建立過程使用起來比較麻煩:
- 建立和管理起來比較複雜(需要
和WaitForSingleObject
)。CloseHandle
-
使用也比較複雜(參數過多).CreateThread
- 線程的回調函數隻支援一個參數,如果需要使用多個參數,需要封裝一個結構體,然後設定結構體的成員。
- 更加大的問題是,這個代碼隻能在Windows下面運作。
這對于C++來說是無法忍受的,是以C++提供了基礎庫來解決這個問題。
2. std::thread
我們先看一下這個類的簡單使用
void Thread1(int a)
{
std::cout << "Thread1 : " << a << std::endl;
}
void Thread2(int a, int b, int c, int d)
{
std::cout << "Thread1 : " << a << std::endl;
std::cout << "Thread2 : " << b << std::endl;
std::cout << "Thread3 : " << c << std::endl;
std::cout << "Thread4 : " << d << std::endl;
}
void MyThread2()
{
std::thread t1(Thread1, 10);
std::thread t2(Thread2, 100, 200, 300, 400);
t1.join();
t2.join();
std::cout << "MyThread2 end" << std::endl;
}
這個方案的好處是:
- 線程回調函數支援各樣的參數,例如
一個參數和void Thread1(int a)
三個參數。void Thread2(int a, int b, int c, int d)
- 類封裝解決了所有的建立過程。
-
控制線程的運作。join
下面我們深入分析一下這幾個過程.
2.1 構造函數
構造函數的聲明為:
//default (1)
thread() noexcept;
//initialization (2)
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
//copy [deleted] (3)
thread (const thread&) = delete;
//move (4)
thread (thread&& x) noexcept;
從
explicit thread (Fn&& fn, Args&&... args);
這裡,我們知道支援可變參數。這個構造函數的實作如下:
template<class _Fn,
class... _Args,
class = enable_if_t<!is_same_v<remove_cv_t<remove_reference_t<_Fn>>, thread>>>
explicit thread(_Fn&& _Fx, _Args&&... _Ax)
{ // construct with _Fx(_Ax...)
_Launch(&_Thr,
_STD make_unique<tuple<decay_t<_Fn>, decay_t<_Args>...> >(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...));
}
template<class _Target> inline
void _Launch(_Thrd_t *_Thr, _Target&& _Tg)
{ // launch a new thread
_LaunchPad<_Target> _Launcher(_STD forward<_Target>(_Tg));
_Launcher._Launch(_Thr);
}
這裡我們使用
tuple<decay_t<_Fn>, decay_t<_Args>...> >(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...)
将線程函數和相關的參數放入到一個元組中。
_LaunchPad
主要靠
_Pad
來實作,如下:
template<class _Target>
class _LaunchPad final
: public _Pad
{ // stub for launching threads
public:
template<class _Other> inline
_LaunchPad(_Other&& _Tgt)
: _MyTarget(_STD forward<_Other>(_Tgt))
{ // construct from target
}
virtual void _Go()
{ // run the thread function object
_Run(this);
}
private:
template<size_t... _Idxs>
static void _Execute(typename _Target::element_type& _Tup,
index_sequence<_Idxs...>)
{ // invoke function object packed in tuple
_STD invoke(_STD move(_STD get<_Idxs>(_Tup))...);
}
static void _Run(_LaunchPad *_Ln) noexcept // enforces termination
{ // construct local unique_ptr and call function object within
_Target _Local(_STD forward<_Target>(_Ln->_MyTarget));
_Ln->_Release();
_Execute(*_Local,
make_index_sequence<tuple_size_v<typename _Target::element_type>>());
_Cnd_do_broadcast_at_thread_exit();
}
_Target _MyTarget;
};
class __declspec(novtable) _Pad
{ // base class for launching threads
public:
_Pad()
{ // initialize handshake
_Cnd_initX(&_Cond);
_Auto_cnd _Cnd_cleaner(_Cond);
_Mtx_initX(&_Mtx, _Mtx_plain);
_Auto_mtx _Mtx_cleaner(_Mtx);
_Started = false;
_Mtx_lockX(_Mtx);
_Mtx_cleaner._Release();
_Cnd_cleaner._Release();
}
~_Pad() noexcept
{ // clean up handshake; non-virtual due to type-erasure
_Auto_cnd _Cnd_cleaner(_Cond);
_Auto_mtx _Mtx_cleaner(_Mtx);
_Mtx_unlockX(_Mtx);
}
void _Launch(_Thrd_t *_Thr)
{ // launch a thread
_Thrd_startX(_Thr, _Call_func, this);
while (!_Started)
_Cnd_waitX(_Cond, _Mtx);
}
void _Release()
{ // notify caller that it's okay to continue
_Mtx_lockX(_Mtx);
_Started = true;
_Cnd_signalX(_Cond);
_Mtx_unlockX(_Mtx);
}
virtual void _Go() = 0;
private:
static unsigned int __stdcall _Call_func(void *_Data)
{ // entry point for new thread
static_cast<_Pad *>(_Data)->_Go();
return (0);
}
_Cnd_t _Cond;
_Mtx_t _Mtx;
bool _Started;
};
其中主要靠将參數轉發到線程回調函數中。
static void _Run(_LaunchPad *_Ln) noexcept // enforces termination
{ // construct local unique_ptr and call function object within
_Target _Local(_STD forward<_Target>(_Ln->_MyTarget));
_Ln->_Release();
_Execute(*_Local,
make_index_sequence<tuple_size_v<typename _Target::element_type>>());
_Cnd_do_broadcast_at_thread_exit();
}
2.2 join
我們再來看一下windows原生的線程建立過程:
void MyThread()
{
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, (LPVOID)100, 0, NULL);
if (hThread!= NULL)
{
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
hThread = NULL;
}
}
這個代碼的目的很簡單:
- 等待新線程執行完成。
- 關閉線程句柄。
這裡的
WaitForSingleObject(hThread, INFINITE);
和
CloseHandle(hThread);
對應的就是jion。
我們可以看一下這個過程,先看jion執行的堆棧過程:
0:000> kb
# ChildEBP RetAddr Args to Child
00 008ff524 75a4e2c9 000000f0 00000000 00000000 ntdll!NtWaitForSingleObject+0xc
01 008ff598 52f9f054 000000f0 ffffffff 00000000 KERNELBASE!WaitForSingleObjectEx+0x99
02 008ff5b4 002060f2 000000f0 000f550c 00000000 MSVCP140D!_Thrd_join+0x14 [d:\agent\_work\1\s\src\vctools\crt\crtw32\stdcpp\thr\cthread.c @ 50]
03 008ff628 00204004 00201712 00201712 006b6000 xxx!std::thread::join+0xa2 [c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.16.27023\include\thread @ 188]
04 008ff6a0 00208188 00201712 00201712 006b6000 xxx!MyThread2+0x74
此時等待的對象為:
0:000> !handle 000000f0 f
Handle f0
Type Thread
Attributes 0
GrantedAccess 0x1fffff:
Delete,ReadControl,WriteDac,WriteOwner,Synch
Terminate,Suspend,Alert,GetContext,SetContext,SetInfo,QueryInfo,SetToken,Impersonate,DirectImpersonate
HandleCount 3
PointerCount 131043
Name <none>
Object Specific Information
Thread Id f5354.f550c
Priority 10
Base Priority 0
Start Address 52eb6c40 ucrtbased!thread_start<unsigned int (__stdcall*)(void *),1>
主要的操作過程為
MSVCP140D!_Thrd_join
,如下:
MSVCP140D!_Thrd_join:
52f9f040 55 push ebp
52f9f041 8bec mov ebp,esp
52f9f043 83ec08 sub esp,8
52f9f046 6a00 push 0
52f9f048 6aff push 0FFFFFFFFh
52f9f04a 8b4508 mov eax,dword ptr [ebp+8]
52f9f04d 50 push eax
52f9f04e ff1508d00353 call dword ptr [MSVCP140D!_imp__WaitForSingleObjectEx (5303d008)]
52f9f054 83f8ff cmp eax,0FFFFFFFFh
52f9f057 7412 je MSVCP140D!_Thrd_join+0x2b (52f9f06b)
52f9f059 8d4df8 lea ecx,[ebp-8]
52f9f05c 51 push ecx
52f9f05d 8b5508 mov edx,dword ptr [ebp+8]
52f9f060 52 push edx
52f9f061 ff1520d00353 call dword ptr [MSVCP140D!_imp__GetExitCodeThread (5303d020)]
52f9f067 85c0 test eax,eax
52f9f069 7507 jne MSVCP140D!_Thrd_join+0x32 (52f9f072)
52f9f06b b804000000 mov eax,4
52f9f070 eb2f jmp MSVCP140D!_Thrd_join+0x61 (52f9f0a1)
52f9f072 837d1000 cmp dword ptr [ebp+10h],0
52f9f076 7408 je MSVCP140D!_Thrd_join+0x40 (52f9f080)
52f9f078 8b4510 mov eax,dword ptr [ebp+10h]
52f9f07b 8b4df8 mov ecx,dword ptr [ebp-8]
52f9f07e 8908 mov dword ptr [eax],ecx
52f9f080 8b5508 mov edx,dword ptr [ebp+8]
52f9f083 52 push edx
52f9f084 ff159cd00353 call dword ptr [MSVCP140D!_imp__CloseHandle (5303d09c)]
52f9f08a 85c0 test eax,eax
52f9f08c 7509 jne MSVCP140D!_Thrd_join+0x57 (52f9f097)
52f9f08e c745fc04000000 mov dword ptr [ebp-4],4
52f9f095 eb07 jmp MSVCP140D!_Thrd_join+0x5e (52f9f09e)
52f9f097 c745fc00000000 mov dword ptr [ebp-4],0
52f9f09e 8b45fc mov eax,dword ptr [ebp-4]
52f9f0a1 8be5 mov esp,ebp
52f9f0a3 5d pop ebp
主要兩個操作:
-
等等線程退出。WaitForSingleObject(Handle, INFINITE);
-
關閉句柄。CloseHandle(Handle);
C++代碼如下:
inline void thread::join()
{ // join thread
if (!joinable())
_Throw_Cpp_error(_INVALID_ARGUMENT);
const bool _Is_null = _Thr_is_null(_Thr); // Avoid Clang -Wparentheses-equality
if (_Is_null)
_Throw_Cpp_error(_INVALID_ARGUMENT);
if (get_id() == _STD this_thread::get_id())
_Throw_Cpp_error(_RESOURCE_DEADLOCK_WOULD_OCCUR);
if (_Thrd_join(_Thr, nullptr) != _Thrd_success) //等待線程結束,并關閉句柄
_Throw_Cpp_error(_NO_SUCH_PROCESS);
_Thr_set_null(_Thr); //設定線程句柄資訊為null
}
2.3 detach
除了等下線程執行完成之後,還有一個另外的操作,例如
void MyThread()
{
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, (LPVOID)100, 0, NULL);
if (hThread!= NULL)
{
CloseHandle(hThread);
hThread = NULL;
}
}
這個代碼的意思是,我建立一個線程去幹其他事情,我不管那個線程的運作結構和資訊,這個就是
thread
的
detach
的作用,執行個體代碼如下:
void Thread1(int a)
{
std::cout << "Thread1 : " << a << std::endl;
}
void Thread2(int a, int b, int c, int d)
{
Sleep(300);
std::cout << "Thread1 : " << a << std::endl;
std::cout << "Thread2 : " << b << std::endl;
std::cout << "Thread3 : " << c << std::endl;
std::cout << "Thread4 : " << d << std::endl;
}
void MyThread2()
{
std::thread t1(Thread1, 10);
std::thread t2(Thread2, 100, 200, 300, 400);
t1.detach();
t2.detach();
std::cout << "MyThread2 end" << std::endl;
}
此時輸出:
Thread1 : 10MyThread2 end
由此可見,線程還沒有運作完成,程序就結束了,并且
_Thrd_detach
的實作如下:
MSVCP140D!_Thrd_detach:
52f9ef90 55 push ebp
52f9ef91 8bec mov ebp,esp
52f9ef93 51 push ecx
52f9ef94 8b4508 mov eax,dword ptr [ebp+8]
52f9ef97 50 push eax
52f9ef98 ff159cd00353 call dword ptr [MSVCP140D!_imp__CloseHandle (5303d09c)]
52f9ef9e 85c0 test eax,eax
52f9efa0 7509 jne MSVCP140D!_Thrd_detach+0x1b (52f9efab)
52f9efa2 c745fc04000000 mov dword ptr [ebp-4],4
52f9efa9 eb07 jmp MSVCP140D!_Thrd_detach+0x22 (52f9efb2)
52f9efab c745fc00000000 mov dword ptr [ebp-4],0
52f9efb2 8b45fc mov eax,dword ptr [ebp-4]
52f9efb5 8be5 mov esp,ebp
52f9efb7 5d pop ebp
52f9efb8 c3 ret
從這裡我們可以看到,這個程式就是關閉句柄使用的。
C++具體代碼如下:
void detach()
{ // detach thread
if (!joinable())
_Throw_Cpp_error(_INVALID_ARGUMENT);
_Thrd_detachX(_Thr); //關閉句柄
_Thr_set_null(_Thr); //設定句柄資訊為空。
}
2.4 析構
那麼我們是否可以建立線程的時候不調用
join
和
detach
呢,主要是看線程析構函數幹了什麼事情:
~thread() noexcept
{ // clean up
if (joinable())
_STD terminate();
}
這裡可以發現,如果線程對象在析構的時候還是
joinable()
的話,那麼,将會
terminate
整個程序,
joinable()
的判斷流程如下:
_NODISCARD bool joinable() const noexcept
{ // return true if this thread can be joined
return (!_Thr_is_null(_Thr));
}
inline void thread::join()
{ // join thread
if (!joinable())
_Throw_Cpp_error(_INVALID_ARGUMENT);
const bool _Is_null = _Thr_is_null(_Thr); // Avoid Clang -Wparentheses-equality
if (_Is_null)
_Throw_Cpp_error(_INVALID_ARGUMENT);
if (get_id() == _STD this_thread::get_id())
_Throw_Cpp_error(_RESOURCE_DEADLOCK_WOULD_OCCUR);
if (_Thrd_join(_Thr, nullptr) != _Thrd_success)
_Throw_Cpp_error(_NO_SUCH_PROCESS);
_Thr_set_null(_Thr);
}
void detach()
{ // detach thread
if (!joinable())
_Throw_Cpp_error(_INVALID_ARGUMENT);
_Thrd_detachX(_Thr);
_Thr_set_null(_Thr); //設定句柄資訊為空。
}
是以我們線上程對象析構之前,必須調用
join
或者
detach
,基于這個考慮的原因是如果
thread
不調用
join
或者
detach
那麼句柄資訊得不到關閉導緻記憶體洩露。。
3. 總結
線程支援的接口有如下:
從上面分析可以看到,相對系統調用接口來說,
std::thread
的好處有:
- 簡單易用。
- 跨平台。
是以在C++中,我們還是使用
std::thread
吧。