std::thread使用及实现原理精讲(全)

C++进阶专栏:http://t.csdnimg.cn/HGkeZ 

相关系列文章:

std::thread使用及实现原理精讲(全)

有了std::thread,为什么还需要引入std::jthread?

目录

1.windows创建线程

2.linux创建线程

3._beginthread小融合

4.CreateThread与_beginthread的异同        

5.std::thread大融合

5.1.std::thread的使用

5.1.1.线程的创建

5.1.2.线程的退出

5.1.3.异常情况下等待线程完成

5.1.4.用std::ref向线程传递引用参数

5.2.std::thread实现原理

5.2.1.线程参数退变

5.2.2.与_beginthreadex的关系

5.2.3.std::thread构造

5.2.4.成员变量_Thr

6.总结


1.windows创建线程

   windows一般采用CreateThread创建线程,它的声明如下:

HANDLE CreateThread(
    LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程安全属性
    DWORD dwStackSize, //线程初始栈大小
    LPTHREAD_START_ROUTINE lpStartAddress, //线程函数入口,通常用线程函数名
    LPVOID lpParameter, //给新线程函数传递参数
    DWORD dwCreationFlags, //设置新线程附加标记,为0时,新线程立即运行
    LPDWORD lpThreadld, //用来返回新线程的线程ID,如果不感兴趣,设为NULL
);
//Windows 系统提供的线程创建函数

参数和返回值含义:

1.参数IpThreadAttributes 指定线程安全属性,当该参数位NULL时,线程获取默认安全描述符;

2.参数 dwStackSize 指定线程堆栈的初始大小,以字节为单位。如果该值为0,则新线程使用可执行文件的默认大小;

3.参数 lpStartAddress 指定由线程执行的自定义函数的指针;

4.参数 lpParameter 指定自定义函数需要的参数;

5.参数 dwCreationFlags 指定线程创建后所处的状态;

6.参数 lpThreadID 指定接收线程标识符的变量的指针,若该参数为NULL,则不需返回该标识符;

如果新线程创建成功,则返回值为新线程的句柄,若不成功,则返回NULL。

示例如下:

#include <windows.h>
#include <tchar.h>
#include <strsafe.h>

#define MAX_THREADS 3
#define BUF_SIZE 255

DWORD WINAPI MyThreadFunction( LPVOID lpParam );
void ErrorHandler(LPTSTR lpszFunction);

// Sample custom data structure for threads to use.
// This is passed by void pointer so it can be any data type
// that can be passed using a single void pointer (LPVOID).
typedef struct MyData {
    int val1;
    int val2;
} MYDATA, *PMYDATA;


int _tmain()
{
    PMYDATA pDataArray[MAX_THREADS];
    DWORD   dwThreadIdArray[MAX_THREADS];
    HANDLE  hThreadArray[MAX_THREADS]; 

    // Create MAX_THREADS worker threads.

    for( int i=0; i<MAX_THREADS; i++ )
    {
        // Allocate memory for thread data.

        pDataArray[i] = (PMYDATA) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,
                sizeof(MYDATA));

        if( pDataArray[i] == NULL )
        {
           // If the array allocation fails, the system is out of memory
           // so there is no point in trying to print an error message.
           // Just terminate execution.
            ExitProcess(2);
        }

        // Generate unique data for each thread to work with.

        pDataArray[i]->val1 = i;
        pDataArray[i]->val2 = i+100;

        // Create the thread to begin execution on its own.

        hThreadArray[i] = CreateThread( 
            NULL,                   // default security attributes
            0,                      // use default stack size  
            MyThreadFunction,       // thread function name
            pDataArray[i],          // argument to thread function 
            0,                      // use default creation flags 
            &dwThreadIdArray[i]);   // returns the thread identifier 


        // Check the return value for success.
        // If CreateThread fails, terminate execution. 
        // This will automatically clean up threads and memory. 

        if (hThreadArray[i] == NULL) 
        {
           ErrorHandler(TEXT("CreateThread"));
           ExitProcess(3);
        }
    } // End of main thread creation loop.

    // Wait until all threads have terminated.
    WaitForMultipleObjects(MAX_THREADS, hThreadArray, TRUE, INFINITE);

    // Close all thread handles and free memory allocations.
    for(int i=0; i<MAX_THREADS; i++)
    {
        CloseHandle(hThreadArray[i]);
        if(pDataArray[i] != NULL)
        {
            HeapFree(GetProcessHeap(), 0, pDataArray[i]);
            pDataArray[i] = NULL;    // Ensure address is not reused.
        }
    }

    return 0;
}


DWORD WINAPI MyThreadFunction( LPVOID lpParam ) 
{ 
    HANDLE hStdout;
    PMYDATA pDataArray;

    TCHAR msgBuf[BUF_SIZE];
    size_t cchStringSize;
    DWORD dwChars;

    // Make sure there is a console to receive output results. 

    hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
    if( hStdout == INVALID_HANDLE_VALUE )
        return 1;

    // Cast the parameter to the correct data type.
    // The pointer is known to be valid because 
    // it was checked for NULL before the thread was created.

    pDataArray = (PMYDATA)lpParam;

    // Print the parameter values using thread-safe functions.

    StringCchPrintf(msgBuf, BUF_SIZE, TEXT("Parameters = %d, %d\n"), 
        pDataArray->val1, pDataArray->val2); 
    StringCchLength(msgBuf, BUF_SIZE, &cchStringSize);
    WriteConsole(hStdout, msgBuf, (DWORD)cchStringSize, &dwChars, NULL);

    return 0; 
} 

void ErrorHandler(LPTSTR lpszFunction) 
{ 
    // Retrieve the system error message for the last-error code.

    LPVOID lpMsgBuf;
    LPVOID lpDisplayBuf;
    DWORD dw = GetLastError(); 

    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | 
        FORMAT_MESSAGE_FROM_SYSTEM |
        FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        dw,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR) &lpMsgBuf,
        0, NULL );

    // Display the error message.

    lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT, 
        (lstrlen((LPCTSTR) lpMsgBuf) + lstrlen((LPCTSTR) lpszFunction) + 40) * sizeof(TCHAR)); 
    StringCchPrintf((LPTSTR)lpDisplayBuf, 
        LocalSize(lpDisplayBuf) / sizeof(TCHAR),
        TEXT("%s failed with error %d: %s"), 
        lpszFunction, dw, lpMsgBuf); 
    MessageBox(NULL, (LPCTSTR) lpDisplayBuf, TEXT("Error"), MB_OK); 

    // Free error-handling buffer allocations.

    LocalFree(lpMsgBuf);
    LocalFree(lpDisplayBuf);
}

        先是用CreateThread创建3个线程,3个子线程与主线程并驾齐驱,再用WaitForMultipleObjects无限等待3个子线程的退出,最后释放资源。

        与线程有关的其它函数如下:

 

2.linux创建线程

linux系统一般用函数pthread_create创建线程,函数定义如下:

int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,
void *(*start_rtn)(void*),void *arg);

参数和返回值含义:

1.参数tidp:事先创建好的pthread_t类型的参数。成功时tidp指向的内存单元被设置为新创建线程的线程ID。

2.参数attr:用于定制各种不同的线程属性。APUE的12.3节讨论了线程属性。通常直接设为NULL。

3.参数start_rtn:新创建线程从此函数开始运行。无参数是arg设为NULL即可。

4.参数arg:start_rtn函数的参数。无参数时设为NULL即可。有参数时输入参数的地址。当多于一个参数时应当使用结构体传入。

如果成功返回0,否则返回错误码。

示例代码如下:

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>

#include<pthread.h>

int main()
{
	printf("main start\n");

	pthread_t id;
	int res = pthread_create(&id,NULL,fun,NULL);
	assert(res == 0);

	//之后并发运行
	int i = 0;	
	for(; i < 5; i++)
	{
		printf("main running\n");
		sleep(1);
	}
	
	char *s = NULL;
	pthread_join(id,(void **)&s);
	printf("join : s = %s\n",s);
	
	exit(0);
}

//定义线程函数
void* fun(void *arg)
{
	printf("fun start\n");

	int i = 0;
	for(; i < 10;i++)
	{
		printf("fun running\n");
		sleep(1);
	}

	printf("fun over\n");

	pthread_exit("fun over");//将该字符常量返回给主线程
}

此时,主线程完成五次输出,就会等待子线程结束,阻塞等待,子线程结束后,最后,主线程打印join:s = fun over。

3._beginthread小融合

函数定义如下:

//线程的开始
unsigned long _beginthread(
  void(_cdecl *start_address)(void *), //声明为void (*start_address)(void *)形式
  unsigned stack_size, //是线程堆栈大小,一般默认为0
  void *arglist //向线程传递的参数,一般为结构体
);
 
unsigned long _beginthreadex( //推荐使用
  void *security,	//安全属性,NULL表示默认安全性
  unsigned stack_size, //是线程堆栈大小,一般默认为0
  unsigned(_stdcall  *start_address)(void *),	//声明为unsigned(*start_address)(void *)形式
  void *argilist,	//向线程传递的参数,一般为结构体
  unsigned initflag, //新线程的初始状态,0表示立即执行,CREATE_SUSPEND表示创建后挂起。
  unsigned *thrdaddr //该变量存放线程标识符,它是CreateThread函数中的线程ID。
); //创建成功条件下的将线程句柄转化为unsigned long型返回,创建失败条件下返回0
 
//线程的结束
//释放线程空间、释放线程TLS空间、调用ExiteThread结束线程。
void _endthread(void); 	
 
// retval:设定的线程结束码,与ExiteThread函数的参数功能一样,
//其实这个函数释放线程TLS空间,再调用ExiteThread函数,但没有释放线程空间。
void _endthreadex(unsigned retval);

两组函数都是用来创建和结束线程的。这两对函数的不同点如下:
1.从形式上开,_beginthreadex()更像CreateThread()。_beginthreadex()比_beginthread()多3个参数:intiflag,security和threadaddr。
2.两种创建方式的线程函数不同。_beginthreadex()的线程函数必须调用_stdcall调用方式,而且必须返回一个unsigned int型的退出码。
3._beginthreadex()在创建线程失败时返回0,而_beginthread()在创建线程失败时返回-1。这一点是在检查返回结果是必须注意的。
4.如果是调用_beginthread()创建线程,并相应地调用_endthread()结束线程时,系统自动关闭线程句柄;而调用_beginthreadx()创建线程,并相应地调用_endthreadx()结束线程时,系统不能自动关闭线程句柄。因此调用_beginthreadx()创建线程还需程序员自己关闭线程句柄,以清除线程的地址空间。

示例代码如下:

// crt_begthrdex.cpp
// compile with: /MT
#include <windows.h>
#include <stdio.h>
#include <process.h>

unsigned Counter;
unsigned __stdcall SecondThreadFunc( void* pArguments )
{
    printf( "In second thread...\n" );

    while ( Counter < 1000000 )
        Counter++;

    _endthreadex( 0 );
    return 0;
}

int main()
{
    HANDLE hThread;
    unsigned threadID;

    printf( "Creating second thread...\n" );

    // Create the second thread.
    hThread = (HANDLE)_beginthreadex( NULL, 0, &SecondThreadFunc, NULL, 0, &threadID );

    // Wait until second thread terminates. If you comment out the line
    // below, Counter will not be correct because the thread has not
    // terminated, and Counter most likely has not been incremented to
    // 1000000 yet.
    WaitForSingleObject( hThread, INFINITE );
    printf( "Counter should be 1000000; it is-> %d\n", Counter );
    // Destroy the thread object.
    CloseHandle( hThread );
}

4.CreateThread与_beginthread的异同        

        用使用 CreateThread 是 Windows 的 API 函数,只需要和 Kernel32.lib 库链接。

        用使用 _beginthread 和 _beginthreadex,应用必须和 CRT(C RunTime) 库链接。

      所以一个线程要使用静态 CRT(C RunTime)的库函数,必须使用 _beginthread 和 _beginthreadex 函数。

        不过,在 _beginthread 和 _beginthreadex 函数的内部实现代码中调用的是 CreateThread 函数来实现的(这很显然嘛,CRT 库也是要运行在Windows上)。

        直接在CreateThread API创建的线程中使用sprintf,malloc,strcat等涉及CRT存储堆操作的CRT库函数是很危险的,容易造成线程的意外中止。 在使用_beginthread和_beginthreadex创建的线程中可以安全的使用CRT函数,但是必须在线程结束的时候相应的调用_endthread或_endthreadex。

5.std::thread大融合

        std::thread是C++11推出的标准线程类,利用它就可以非常简单的创建一个线程,而且也不区分哪个操作系统。真正实现了线程创建的大统一。

5.1.std::thread的使用

std::thread提供的接口有:

函数名含义
join阻塞等待到该线程结束。
detach将线程从父进程分离,无法再通过 thread 对象对其进行操作,生命周期也脱离父进程,最终由操作系统进行资源回收。
joinable检查线程是否可被阻塞等待。
get_id获取该线程的唯一标识符。
swap与指定 thread 对象进行互换操作。
native_handle获取该线程的句柄。
hardware_concurrency [static]返回逻辑处理器数量。

5.1.1.线程的创建

线程创建支持的可调用对象有C语言函数、仿函数、类成员函数、lambda函数等。示例代码如下:

#include <chrono>
#include <iostream>
#include <thread>
#include <utility>
 
//C语言函数
void f1(int n)
{
    for (int i = 0; i < 5; ++i)
    {
        std::cout << "正在执行线程1\n";
        ++n;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}
 
void f2(int& n)
{
    for (int i = 0; i < 5; ++i)
    {
        std::cout << "正在执行线程2\n";
        ++n;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}
 
//类成员函数
class foo
{
public:
    void bar()
    {
        for (int i = 0; i < 5; ++i)
        {
            std::cout << "正在执行线程3\n";
            ++n;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
    int n = 0;
};
//仿函数
class baz
{
public:
    void operator()()
    {
        for (int i = 0; i < 5; ++i)
        {
            std::cout << "正在执行线程4\n";
            ++n;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
    int n = 0;
};
 
int main()
{
    int n = 0;
    foo f;
    baz b;
    std::thread t1; // t1 不是线程
    std::thread t2(f1, n + 1); // 按值传递
    std::thread t3(f2, std::ref(n)); // 按引用传递
    std::thread t4(std::move(t3)); // t4 现在运行 f2()。t3 不再是线程
    std::thread t5(&foo::bar, &f); // t5 在对象 f 上运行 foo::bar()
    std::thread t6(b); // t6 在对象 b 的副本上运行 baz::operator()
    std::thread t7([](){ int x = 0;                         
                         std::this_thread::sleep_for(std::chrono::milliseconds(6));});
    t2.join();
    t4.join();
    t5.join();
    t6.join();
    t7.join();
    std::cout << "n 的最终值是 " << n << '\n';
    std::cout << "f.n (foo::n) 的最终值是 " << f.n << '\n';
    std::cout << "b.n (baz::n) 的最终值是 " << b.n << '\n';
}

输出:

正在执行线程1
正在执行线程2
正在执行线程3
正在执行线程4
正在执行线程3
正在执行线程1
正在执行线程2
正在执行线程4
正在执行线程2
正在执行线程3
正在执行线程1
正在执行线程4
正在执行线程3
正在执行线程2
正在执行线程1
正在执行线程4
正在执行线程3
正在执行线程1
正在执行线程2
正在执行线程4
n 的最终值是 5
f.n (foo::n) 的最终值是 5
b.n (baz::n) 的最终值是 0

5.1.2.线程的退出

当线程启动后,一定要在和线程相关联的thread销毁前,确定以何种方式等待线程执行结束

C++11有两种方式来等待线程结束:

  • detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。前面代码所使用的就是这种方式。
    • 调用detach表示thread对象和其表示的线程完全分离;
    • 分离之后的线程是不在受约束和管制,会单独执行,直到执行完毕释放资源,可以看做是一个daemon线程;
    • 分离之后thread对象不再表示任何线程;
    • 分离之后joinable() == false,即使还在执行;

示例代码如下:

#include <iostream>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
 
void foo()
{
    std::this_thread::sleep_for(500ms);
}
 
int main()
{
    std::cout << std::boolalpha;
 
    std::thread t;
    std::cout << "before starting, joinable: " << t.joinable() << '\n';
 
    t = std::thread{foo};
    std::cout << "after starting, joinable: " << t.joinable() << '\n';
 
    t.join();
    std::cout << "after joining, joinable: " << t.joinable() << '\n';
 
    t = std::thread{foo};
    t.detach();
    std::cout << "after detaching, joinable: " << t.joinable() << '\n';
    std::this_thread::sleep_for(1500ms);
}
  • join方式,等待启动的线程完成,才会继续往下执行。假如前面的代码使用这种方式,其输出就会0,1,2,3,因为每次都是前一个线程输出完成了才会进行下一个循环,启动下一个新线程。
    • 只有处于活动状态线程才能调用join,可以通过joinable()函数检查;
    • joinable() == true表示当前线程是活动线程,才可以调用join函数;
    • 默认构造函数创建的对象是joinable() == false;
    • join只能被调用一次,之后joinable就会变为false,表示线程执行完毕;
    • 调用 ternimate()的线程必须是 joinable() == false;
    • 如果线程不调用join()函数,即使执行完毕也是一个活动线程,即joinable() == true,依然可以调用join()函数;

        无论在何种情形,一定要在thread销毁前,调用t.join或者t.detach,来决定线程以何种方式运行。

        当使用join方式时,会阻塞当前代码,等待线程完成退出后,才会继续向下执行;

        而使用detach方式则不会对当前代码造成影响,当前代码继续向下执行,创建的新线程同时并发执行,这时候需要特别注意:创建的新线程对当前作用域的变量的使用,创建新线程的作用域结束后,有可能线程仍然在执行,这时局部变量随着作用域的完成都已销毁,如果线程继续使用局部变量的引用或者指针,会出现意想不到的错误,并且这种错误很难排查。例如:

auto fn = [](const int *a)
{
    for (int i = 0; i < 10; i++)
    {
        cout << *a << endl;
    }
};
 
[fn]
{
    int a = 1010;
    thread t(fn, &a);
    t.detach();
}();

        在lambda表达式中,使用fn启动了一个新的线程,在装个新的线程中使用了局部变量a的指针,并且将该线程的运行方式设置为detach。这样,在lamb表达式执行结束后,变量a被销毁,但是在后台运行的线程仍然在使用已销毁变量a的指针,这样就可能会导致不正确的结果出现。

        所以在以detach的方式执行线程时,要将线程访问的局部数据复制到线程的空间(使用值传递),一定要确保线程没有使用局部变量的引用或者指针,除非你能肯定该线程会在局部作用域结束前执行结束。

        当然,使用join方式的话就不会出现这种问题,它会在作用域结束前完成退出。

5.1.3.异常情况下等待线程完成

        当决定以detach方式让线程在后台运行时,可以在创建thread的实例后立即调用detach,这样线程就会后thread的实例分离,即使出现了异常thread的实例被销毁,仍然能保证线程在后台运行。

        但线程以join方式运行时,需要在主线程的合适位置调用join方法,如果调用join前出现了异常,thread被销毁,线程就会被异常所终结。为了避免异常将线程终结,或者由于某些原因,例如线程访问了局部变量,就要保证线程一定要在函数退出前完成,就要保证要在函数退出前调用join。

void func() {
	thread t([]{
		cout << "hello C++ 11" << endl;
	});
 
	try
	{
		do_something_else();
	}
	catch (...)
	{
		t.join();
		throw;
	}
	t.join();
}

        上面代码能够保证在正常或者异常的情况下,都会调用join方法,这样线程一定会在函数func退出前完成。但是使用这种方法,不但代码冗长,而且会出现一些作用域的问题,并不是一个很好的解决方法。

        一种比较好的方法是资源获取即初始化(RAII,Resource Acquisition Is Initialization),该方法提供一个类,在析构函数中调用join

C++惯用法之RAII思想: 资源管理-CSDN博客

class thread_guard
{
	thread &t;
public :
	explicit thread_guard(thread& _t) :
		t(_t){}
 
	~thread_guard()
	{
		if (t.joinable())
			t.join();
	}
 
	thread_guard(const thread_guard&) = delete;
	thread_guard& operator=(const thread_guard&) = delete;
};
 
void func(){
 
	thread t([]{
		cout << "Hello thread" <<endl ;
	});
 
	thread_guard g(t);
}

无论是何种情况,当函数退出时,局部变量g调用其析构函数销毁,从而能够保证join一定会被调用。

5.1.4.用std::ref向线程传递引用参数

向线程调用的函数传递参数也是很简单的,只需要在构造thread的实例时,依次传入即可。例如:

void func(int *a,int n){}
 
int buffer[10];
thread t(func,buffer,10);
t.join();

需要注意的是,默认的会将传递的参数以拷贝的方式复制到线程空间,即使参数的类型是引用。例如:

void func(int a,const string& str);
thread t(func,3,"hello");

func的第二个参数是string &,而传入的是一个字符串字面量。该字面量以const char*类型传入线程空间后,在**线程的空间内转换为string**。

如果在线程中使用引用来更新对象时,就需要注意了。默认的是将对象拷贝到线程空间,其引用的是拷贝的线程空间的对象,而不是初始希望改变的对象。如下:

class _tagNode
{
public:
	int a;
	int b;
};
 
void func(_tagNode &node)
{
	node.a = 10;
	node.b = 20;
}
 
void f()
{
	_tagNode node;
 
	thread t(func, node);
	t.join();
 
	cout << node.a << endl ;
	cout << node.b << endl ;
}

        在线程内,将对象的字段a和b设置为新的值,但是在线程调用结束后,这两个字段的值并不会改变。这样由于引用的实际上是局部变量node的一个拷贝,而不是node本身。在将对象传入线程的时候,调用std::ref,将node的引用传入线程,而不是一个拷贝。例如:

  thread t(func,std::ref(node));

       也可以使用类的成员函数作为线程函数,示例如下:

class _tagNode{
 
public:
	void do_some_work(int a);
};
_tagNode node;
 
thread t(&_tagNode::do_some_work, &node,20);

上面创建的线程会调用node.do_some_work(20),第三个参数为成员函数的第一个参数,以此类推。

5.2.std::thread实现原理

剖析其源码是了解其机理的最好方法,std::thread的部分源码(VS2019)整理如下:

class thread { // class for observing and managing threads
public:
    class id;

    using native_handle_type = void*;

    thread() noexcept : _Thr{} {}

private:
#if _HAS_CXX20
    friend jthread;
#endif // _HAS_CXX20

    template <class _Tuple, size_t... _Indices>
    static unsigned int __stdcall _Invoke(void* _RawVals) noexcept /* terminates */ {
        // adapt invoke of user's callable object to _beginthreadex's thread procedure
        const unique_ptr<_Tuple> _FnVals(static_cast<_Tuple*>(_RawVals));
        _Tuple& _Tup = *_FnVals;
        _STD invoke(_STD move(_STD get<_Indices>(_Tup))...);
        _Cnd_do_broadcast_at_thread_exit(); // TRANSITION, ABI
        return 0;
    }

    template <class _Tuple, size_t... _Indices>
    _NODISCARD static constexpr auto _Get_invoke(index_sequence<_Indices...>) noexcept {
        return &_Invoke<_Tuple, _Indices...>;
    }

    template <class _Fn, class... _Args>
    void _Start(_Fn&& _Fx, _Args&&... _Ax) {
        using _Tuple                 = tuple<decay_t<_Fn>, decay_t<_Args>...>;
        auto _Decay_copied           = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
        constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{});

#pragma warning(push)
#pragma warning(disable : 5039) // pointer or reference to potentially throwing function passed to
                                // extern C function under -EHc. Undefined behavior may occur
                                // if this function throws an exception. (/Wall)
        _Thr._Hnd =
            reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id));
#pragma warning(pop)

        if (_Thr._Hnd) { // ownership transferred to the thread
            (void) _Decay_copied.release();
        } else { // failed to start thread
            _Thr._Id = 0;
            _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);
        }
    }

public:
    template <class _Fn, class... _Args, enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>, int> = 0>
    _NODISCARD_CTOR explicit thread(_Fn&& _Fx, _Args&&... _Ax) {
        _Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
    }

    ~thread() noexcept {
        if (joinable()) {
            _STD terminate();
        }
    }

    thread(thread&& _Other) noexcept : _Thr(_STD exchange(_Other._Thr, {})) {}

    thread& operator=(thread&& _Other) noexcept {
        if (joinable()) {
            _STD terminate();
        }

        _Thr = _STD exchange(_Other._Thr, {});
        return *this;
    }

    thread(const thread&) = delete;
    thread& operator=(const thread&) = delete;

    void swap(thread& _Other) noexcept {
        _STD swap(_Thr, _Other._Thr);
    }

    _NODISCARD bool joinable() const noexcept {
        return _Thr._Id != 0;
    }

    void join() {
        if (!joinable()) {
            _Throw_Cpp_error(_INVALID_ARGUMENT);
        }

        if (_Thr._Id == _Thrd_id()) {
            _Throw_Cpp_error(_RESOURCE_DEADLOCK_WOULD_OCCUR);
        }

        if (_Thrd_join(_Thr, nullptr) != _Thrd_success) {
            _Throw_Cpp_error(_NO_SUCH_PROCESS);
        }

        _Thr = {};
    }

    void detach() {
        if (!joinable()) {
            _Throw_Cpp_error(_INVALID_ARGUMENT);
        }

        _Check_C_return(_Thrd_detach(_Thr));
        _Thr = {};
    }

    _NODISCARD id get_id() const noexcept;

    _NODISCARD static unsigned int hardware_concurrency() noexcept {
        return _Thrd_hardware_concurrency();
    }

    _NODISCARD native_handle_type native_handle() { // return Win32 HANDLE as void *
        return _Thr._Hnd;
    }

private:
    _Thrd_t _Thr;
};

由以上代码可知:

5.2.1.线程参数退变

C++之std::decay_std::decty-CSDN博客

从源代码中的这行代码:

using _Tuple                 = tuple<decay_t<_Fn>, decay_t<_Args>...>;

可以看出,传入的参数通过decay_t退变,左值引用和右值引用都被擦除类型变为右值了,所以想要通过传入参数返回值的方式要特别注意了。从这里就不难看出在5.1.4的章节中传入引用去线程,值没有返回的原因了。

5.2.2.与_beginthreadex的关系

在源代码的_Start函数中很清晰的看到,std::thread的的实现也是调用_beginthreadex函数创建线程的。

5.2.3.std::thread构造

C++17之std::invoke: 使用和原理探究(全)_c++新特性 invoke-CSDN博客

 1.我们知道_begintrheadex中的线程函数形如:

  unsigned  (_stdcall  *start_address)(void *);

再看一下源码中_Invoke函数:

template <class _Tuple, size_t... _Indices>
static unsigned int __stdcall _Invoke(void* _RawVals) noexcept;

完全一样。

2.利用std::tuple实现参数的传递

C++之std::tuple(一) : 使用精讲(全)

C++之std::tuple(二) : 揭秘底层实现原理

首先通过传入的参数构造出_Tuple, 再利用make_index_sequence产生序列依次获取_Tuple的参数,最后调用std::invoke实现函数的调用。

C++14之std::index_sequence和std::make_index_sequence_std::make_index_sequence<4> 的递归展开过程-CSDN博客

5.2.4.成员变量_Thr

定义如下:

struct _Thrd_t { // thread identifier for Win32
    void* _Hnd; // Win32 HANDLE
    _Thrd_id_t _Id;
};

两个成员变量,一个是线程的ID,一个是线程的句柄。在windows环境下,_Hnd就是CreateThread的返回值,_Id就是CreateThread函数的最后一个参数。

通过上面几个方面的分析,std::thread实现也不过如此;不过要真正理解它的实现,还需要好好理解make_index_sequence、index_sequence、invoke等知识

6.总结

        线程创建和销毁是昂贵的操作,应尽量避免频繁创建和销毁线程。

        线程间共享数据时,要确保数据访问的线程安全性。

        尽量避免在多个线程中访问和修改全局变量或静态变量,除非这些变量是线程安全的。

        使用 std::thread 时,要确保在程序结束前对所有线程调用 join() 或 detach(),以避免资源泄漏。

        总之,std::thread 为 C++ 提供了强大而灵活的多线程支持,使得开发者能够更容易地编写并行程序。然而,多线程编程也带来了额外的复杂性和挑战,需要开发者仔细考虑线程间的数据共享和同步问题。

参考:

创建线程 - Win32 apps | Microsoft Learn
_beginthread、_beginthreadex | Microsoft Learn

 

  • 33
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值