C++ 多线程之std::thread浅析

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 析构

那么我们是否可以创建线程的时候不调用joindetach呢,主要是看线程析构函数干了什么事情:

~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吧。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值