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 255DWORD 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 attributes0,                      // use default stack size  MyThreadFunction,       // thread function namepDataArray[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, //是线程堆栈大小,一般默认为0void *arglist //向线程传递的参数,一般为结构体
);unsigned long _beginthreadex( //推荐使用void *security,	//安全属性,NULL表示默认安全性unsigned stack_size, //是线程堆栈大小,一般默认为0unsigned(_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_CXX20friend jthread;
#endif // _HAS_CXX20template <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 procedureconst 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, ABIreturn 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 Win32void* _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++ 提供了强大而灵活的多线程支持,使得开发者能够更容易地编写并行程序。然而,多线程编程也带来了额外的复杂性和挑战,需要开发者仔细考虑线程间的数据共享和同步问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/764257.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于python+vue网络相册设计与实现flask-django-nodejs-php

网络相册设计与实现的目的是让使用者可以更方便的将人、设备和场景更立体的连接在一起。能让用户以更科幻的方式使用产品&#xff0c;体验高科技时代带给人们的方便&#xff0c;同时也能让用户体会到与以往常规产品不同的体验风格。 与安卓&#xff0c;iOS相比较起来&#xff0…

构造函数(原型和原型链)

原型和原型链 今日目标&#xff1a;原型和原型链是高频面试题 1.原型 2.原型链 要求&#xff1a;清晰的说出来原型和原型链的概念和特性。并能手绘原型和原型链图 3.swiper轮播图插件的使用 00-回顾 # 面向过程&#xff1a; 概念&#xff1a; 根据流程步骤一步步实现特定…

CentOS/RHEL 6.5 上 NFS mount 挂起kernel bug

我本身有四台机器做WAS集群&#xff0c;挂载nfs&#xff0c;其中随机一台客户端计算机端口关闭释放将进入不良状态&#xff0c;对 NFSv4 挂载的任何访问都将挂起&#xff08;例如“ls&#xff0c;cd 或者df均挂起”&#xff09;。这意味着没有人并且所有需要访问共享的用户进程…

久菜盒子|留学|推荐信|专业课老师|结构抗震设计

在众多学生当中&#xff0c;10这名学生给我留下了更深的印象&#xff0c;她对学习的认真态度、一丝不苟的精神&#xff0c;都让我感受到她的与众不同。因此&#xff0c;作为我校土木工程学院的前院长&#xff0c;我对于 10 申请贵校表示支持并毫无保留的推荐这位学生。 在结构抗…

深度学习图像处理02:Tensor数据类型

上一讲深度学习图像处理01&#xff1a;图像的本质&#xff0c;我们了解到图像处理的本质是对矩阵的操作。这一讲&#xff0c;我们讲介绍深度学习图像处理的基本数据类型&#xff1a;Tensor类型。 在深度学习领域&#xff0c;Tensor是一种核心的数据结构&#xff0c;用于表示和…

复旦大学MBA:iLab项目探寻科技创新 助力企业出海

2024年2月底&#xff0c;新一轮复旦MBA iLab商业咨询项目&#xff08;以下简称iLab项目&#xff09;正式拉开序幕。      科创大时代&#xff0c;如何于变局中创新突破、绘就商业“蓝图”&#xff1f;怎样把握ESG投资机遇&#xff0c;创造可持续发展的未来&#xff1f;如何…

图论07-被包围的区域(Java)

7.被包围的区域 题目描述 给你一个 m x n 的矩阵 board &#xff0c;由若干字符 X 和 O &#xff0c;找到所有被 X 围绕的区域&#xff0c;并将这些区域里所有的 O 用 X 填充。 示例 1&#xff1a; 输入&#xff1a;board [["X","X","X",&qu…

2.6、媒体查询(mediaquery)

概述 媒体查询作为响应式设计的核心,在移动设备上应用十分广泛。媒体查询可根据不同设备类型或同设备不同状态修改应用的样式。媒体查询常用于下面两种场景: 针对设备和应用的属性信息(比如显示区域、深浅色、分辨率),设计出相匹配的布局。当屏幕发生动态改变时(比如分屏…

V2X技术与智能传感器的完美融合:提升城市道路安全

在科技不断创新的今天&#xff0c;城市交通领域涌现了大量新技术。有时候我们不仅仅需要独立应用这些新技术来实现交通的变革&#xff0c;更需要将它们巧妙地结合连接起来&#xff0c;以获取更高效更安全的交通环境。本文将探讨V2X技术与智能传感器的结合&#xff0c;如何在城市…

专为智能设备安全打造 | 基于ACM32 MCU的智能断路器方案

随着我国电网建设的快速发展&#xff0c;数字化变电站成为建设和研究的热点&#xff0c;数字化变电站的核心在于一次设备的智能化与二次设备的网络化&#xff0c;对于断路器这种极其重要的电力一次设备而言&#xff0c;其智能化的实现有十分重要的意义&#xff0c;断路器智能化…

平衡隐私与效率,Partisia Blockchain 解锁数字安全新时代

原文&#xff1a;https://cointelegraph.com/news/exploring-multiparty-computations-role-in-the-future-of-blockchain-privacy&#xff1b; https://medium.com/partisia-blockchain/unlocking-tomorrow-outlook-for-mpc-in-2024-and-beyond-cb170e3ec567 编译&#xff1…

skywalking监听apisix

一、原理 Skywalking结合OpenTelemetry Collector Apisix的promethus插件实现对apisix metrics数据的收集。 二、数据流图 1. Apisix Promethus插件从Apisix收集指标数据。 2. OpenTelemetry Collector通过promethus receiver获取来自Apisix Promethus插件的指标数据&#…

python的OA公文发文管理系统flask-django-php-nodejs

采用结构化的分析设计&#xff0c;该方法要求结合一定的图表&#xff0c;在模块化的基础上进行系统的开发工作。在设计中采用“自下而上”的思想&#xff0c;在OA公文发文管理系统实现了用户、公文分类、公文信息、待办提醒等的功能性。系统根据现有的管理模块进行开发和扩展&a…

为什么本地开发环境通常使用 HTTP 而不是 HTTPS

简单快捷&#xff1a;HTTP 相对于 HTTPS 更简单和快速。在开发过程中&#xff0c;可能频繁地修改代码并测试&#xff0c;使用 HTTP 可以减少一些开发中的额外步骤和复杂性。 不涉及敏感信息&#xff1a;在本地开发环境中&#xff0c;通常不涉及真实用户数据或敏感信息的传输&a…

(附源码)基于Spring Boot和Vue的前后端分离考研资料分享平台的设计与实现

前言 &#x1f497;博主介绍&#xff1a;✌专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f447;&#x1f3fb; 精彩专栏 推荐订阅&#x1f447;&#x1f3fb; 2024年Java精品实战案例《100套》 &#x1f345;文末获取源码联系&#x1f345; &#x1f31…

利用Node.js模块在Electron中进行文件系统操作实战(下)

利用Node.js模块在Electron中进行文件系统操作实战&#xff08;下&#xff09; 更详细的文件权限控制文件锁&#xff08;File Locking&#xff09;临时文件符号链接&#xff08;Symbolic Links&#xff09;和硬链接&#xff08;Hard Links&#xff09;文件监视&#xff08;File…

旅游网站|基于JSP技术+ Mysql+Java+ B/S结构的旅游网站设计与实现(可运行源码+数据库+设计文档)

推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含java&#xff0c;ssm&#xff0c;springboot的平台设计与实现项目系统开发资源&#xff08;可…

「媒体宣传」企业活动发布会邀请媒体报道的好处与优势?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 企业活动发布会邀请媒体报道具有多种好处与优势&#xff0c;这些都有助于提升企业的知名度、形象和影响力。以下是一些主要的好处与优势&#xff1a; 提升品牌知名度&#xff1a;媒体报道…

目标控制器数字孪生系统的研究与设计

文章来源&#xff1a;铁路计算机应用,2023,32(10):36-41. 作者&#xff1a;许婧&#xff0c;杨硕&#xff0c;季志均 摘要&#xff1a;随着目标控制器&#xff08;OC&#xff0c;Object Controller&#xff09;系统在轨道交通领域的推广应用&#xff0c;其硬件投入较高、研发…

短视频矩阵系统----源头开发

短视频矩阵源码技术开发要求及实现流程&#xff1a; 短视频矩阵开发要求具备视频录制、编辑、剪辑、分享等基本功能&#xff0c;支持实时滤镜、特效、音乐等个性化编辑&#xff0c;能够实现高效的视频渲染和处理。开发流程主要包括需求分析、技术选型、设计架构、编码实现、测试…