天天看點

C++ 多線程之std::thread淺析C++ 多線程之std::thread淺析

文章目錄

  • 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代碼)。

但是這個原生态的線程建立過程使用起來比較麻煩:

  1. 建立和管理起來比較複雜(需要

    WaitForSingleObject

    CloseHandle

    )。
  2. CreateThread

    使用也比較複雜(參數過多).
  3. 線程的回調函數隻支援一個參數,如果需要使用多個參數,需要封裝一個結構體,然後設定結構體的成員。
  4. 更加大的問題是,這個代碼隻能在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;
}
           

這個方案的好處是:

  1. 線程回調函數支援各樣的參數,例如

    void Thread1(int a)

    一個參數和

    void Thread2(int a, int b, int c, int d)

    三個參數。
  2. 類封裝解決了所有的建立過程。
  3. 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;
	}
}
           

這個代碼的目的很簡單:

  1. 等待新線程執行完成。
  2. 關閉線程句柄。

這裡的

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

           

主要兩個操作:

  1. WaitForSingleObject(Handle, INFINITE);

    等等線程退出。
  2. 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. 總結

線程支援的接口有如下:

C++ 多線程之std::thread淺析C++ 多線程之std::thread淺析

從上面分析可以看到,相對系統調用接口來說,

std::thread

的好處有:

  1. 簡單易用。
  2. 跨平台。

是以在C++中,我們還是使用

std::thread

吧。

繼續閱讀