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;
}
}
这个代码的目的很简单:
等待新线程执行完成。
关闭线程句柄。
这里的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的好处有:
1.简单易用。
2.跨平台。
因此在C++中,我们还是使用std::thread吧。