多线程编程与资源同步
在Windows下,主线程退出后,子线程也会被关闭;
在Linux下,主线程退出后,系统不会关闭子线程,这样就产生了僵尸进程
3.2.1创建线程
-
Linux 线程的创建
#include <unistd.h> #include <stdio.h> #include <pthread.h> void* threadfunc(void* arg) { while (1) { sleep(1); printf("I am a new thread!!!\n"); } return NULL; } int main() { pthread_t threadid; pthread_create(&threadid, NULL,threadfunc, NULL); while (1) { } return 0; }
-
Windows CRT[1]提供的线程创建函数
#include <process.h> #include <stdio.h> unsigned int __stdcall threadfun(void* args) { while (true) { printf("I am new thread!"); } return 0; } int main(int argc,char* argv[]) { unsigned int threadid; _beginthreadex(0, 0, threadfun, 0, 0, &threadid); while (true)//不让主线程退出 { } return 0; }
-
C++ 提供的
std::thread
类#include <iostream> #include <thread> void threadproc1() { while (true) { printf("I am aNew Thread!!"); } } void threadproc2(int a,int b) { while (true) { printf("I an Thread2"); } } int main() { std::thread t1(threadproc1); std::thread t2(threadproc2, 1, 2); while (true) { } }
这种方法容易出错,原因如下:
#include <iostream> #include <thread> void threadproc1() { while (true) { printf("I am aNew Thread!!"); } } void func() { std::thread t(threadproc1); } int main() { func(); while (true) { } }
这段代码实际上试运行不了的,在vs2019 release模式下结果:
在func函数调用完成后,func中的局部变量t被销毁,而此时线程函数仍然在运行.所以使用
std::thread
创建线程必须保证线程函数运行期间,线程对象始终有效!!当然,我们也可以使用
detach
方法解决这个问题,使线程函数和线程对象脱离... void func() { std::thread t(threadproc1); t.detach(); } ...
这样就可以运行了,但是不推进这样做,因为我们需要线程对象对线程进行管理
3.2.2线程ID
下面介绍一下Linux系统线程ID本质
-
Linux系统线程ID本质
在Linux系统中有三种方法可以获取一个线程的ID
- 调用
pthread_create
函数时,可以通过第一个参数获取线程ID - 在需要获取ID的线程中调用
pthread_self
函数获取 - 通过系统调用获取线程ID
其中,方法一和方法二获取线程ID的结果都是一样的,都是
pthread_t
类型 - 调用
不同的进程可能有同样的地址内存块(共享内存),所以通过方法一和方法二获取到的线程ID可能不是全系统唯一的,而方法三获取的线程ID是全系统唯一的,就是LWP(轻量级进程)[2]
-
C++ 获取线程ID的方法
#include <thread> #include <iostream> #include <sstream> void worker_thread_func() { while (true) { } } int main() { //获取线程t的id std::thread t(worker_thread_func); std::thread::id worker_thread_id=t.get_id(); std::cout<<worker_thread_id<<std::endl; //获取主线程的id std::thread::id main_thread_id = std::this_thread::get_id(); std::cout<<main_thread_id<<std::endl; while (true) { } }
运行结果(ubuntu20):
3.2.3 等待线程结束
-
在Linux下等待线程结束
Linux 线程库提供了
pthread_join
函数,用来等待某线程的退出并接收他的返回值这种操作被称为汇接(join)
Declared in: pthread.h static int pthread_join(pthread_t __th, void * *__thread_return)
参数
__th
是需要等待的线程ID;参数__thread__return是输出参数,用于接受被等待线程的退出码,可以再调用
pthread_exit()
时指定退出码Declared in: pthread.h static void pthread_exit(void *__retval)
其中
__retval
可以通过__thread_return参数获得下面来展示一个实例,在程序启动时开启一个工作线程,工作线程将当前系统时间写入一个文件后,主线程等待工作线程退出后,从文件中读取时间,并将其输出
#include <cstdio>#include <string>#include <pthread.h>#include <cstring>#define TIME_FILENAME "time.txt"void* fileThreadFunc(void* arg){ time_t now = time(nullptr); tm* t = localtime(&now); char timeStr[32]={0}; snprintf(timeStr,32,"%04d/%02d %02d:%02d:%02d", t->tm_year+1900, t->tm_mon+1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); FILE* fp = fopen(TIME_FILENAME,"w"); if(fp == nullptr) { printf("打开文件(time.txt)失败\n"); return nullptr; } size_t sizeToWrite=strlen(timeStr) +1; size_t ret = fwrite(timeStr,1,sizeToWrite,fp); if(ret != sizeToWrite) { printf("写入错误!\n"); } fclose(fp); return nullptr;}int main(){ pthread_t fileThreadID; int ret = pthread_create(&fileThreadID, nullptr,fileThreadFunc, nullptr); if(ret != 0) { printf("创建线程失败\n"); return -1; } int* retval; pthread_join(fileThreadID,(void**)&retval); FILE* fp = fopen(TIME_FILENAME,"r"); if(fp == nullptr) { printf("打开文件失败!\n"); return -2; } char buf[32] = {0}; int sizeRead = fread(buf,1,32,fp); if(sizeRead == 0) { printf("读取文件失败"); fclose(fp); return -3; } printf("Current time is: %s.\n",buf); fclose(fp); return 0;}
执行结果如下:
-
Windows 等待线程结束
在Windows下我们可以使用
WaitForSingleObject function (synchapi.h)
和WaitForMultipleObjects function (synchapi.h)
-
C++11提供的等待线程结束的函数
#include <cstdio>#include <cstring>#include <thread>#define TIME_FILENAME "time.txt"void fileThreadFunc(){ time_t now = time(nullptr); tm* t = localtime(&now); char timeStr[32]={0}; snprintf(timeStr,32,"%04d/%02d %02d:%02d:%02d", t->tm_year+1900, t->tm_mon+1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); FILE* fp = fopen(TIME_FILENAME,"w"); if(fp == nullptr) { printf("打开文件(time.txt)失败\n"); return ; } size_t sizeToWrite=strlen(timeStr) +1; size_t ret = fwrite(timeStr,1,sizeToWrite,fp); if(ret != sizeToWrite) { printf("写入错误!\n"); } fclose(fp); return;}int main(){ std::thread t(fileThreadFunc); if(t.joinable()) t.join(); FILE* fp = fopen(TIME_FILENAME,"r"); if(fp == nullptr) { printf("打开文件失败!\n"); return -2; } char buf[32] = {0}; int sizeRead = fread(buf,1,32,fp); if(sizeRead == 0) { printf("读取文件失败"); fclose(fp); return -3; } printf("Current time is: %s.\n",buf); fclose(fp); return 0;}
3.4.3 C++11 对整型变量原子操作的支持
C++11 提供了对整形变量原子操作的支持
即std::atomic
这是一个模板类型
Defined in header <atomic>template< class T >struct atomic;
例子
#include <atomic>#include <iostream>int main(){ std::atomic<int> value{1}; value++;//自增,原子操作 std::cout<<value<<std::endl;}
C++ 线程同步对象
C++ 新标准中新增用于线程同步的std::mutex
和std::condition_variable
3.7.1 std::mutex
系列
这个系列类型的对象均提供了加锁lock
,尝试加锁trylock
和解锁unlock
的方法
#include <iostream>#include <chrono>#include <thread>#include <mutex>int g_num =0 ;std::mutex g_num_mutex;void slow_increment(int id){ for (int i = 0; i < 10; ++i) { g_num_mutex.lock(); ++g_num; std::cout<<id<<"->"<<g_num<<std::endl; g_num_mutex.unlock(); } std::this_thread::sleep_for(std::chrono::seconds(1));}int main(){ std::thread t1(slow_increment,0); std::thread t2(slow_increment,1); t1.join(); t2.join(); return 0;}
输出:
3.7.2 std::shared_mutex
std::shared_mutex
的底层实现是操作系统提供的读写锁,也就是说,在有多个线程对共享资源读且少许线程对共享资源写的情况下,std::shared_mutex
比std::mutex
效率更高
shared_mutex
通常用于多个读线程能同时访问同一资源而不导致数据竞争,但只有一个写线程能访问的情形。
3.7.3 std::condition_variable
Member functions
(constructor) | constructs the object (public member function) |
---|---|
(destructor) | destructs the object (public member function) |
operator=[deleted] | not copy-assignable (public member function) |
Notification | |
notify_one | notifies one waiting thread (public member function) |
notify_all | notifies all waiting threads (public member function) |
Waiting | |
wait | blocks the current thread until the condition variable is woken up (public member function) |
wait_for | blocks the current thread until the condition variable is woken up or after the specified timeout duration (public member function) |
wait_until | blocks the current thread until the condition variable is woken up or until specified time point has been reached (public member function) |
Native handle | |
native_handle | returns the native handle (public member function) |
`
-
CRT原先是指Microsoft开发的C Runtime Library(C语言运行时库),用于操作系统的开发及运行。后来在此基础上开发了C++ Runtime Library,所以现在CRT是指Microsoft开发的C/C++ Runtime Library。在VC的CRT/SRC目录下,可以看到CRT的源码,不仅有C的,也有C++的。CRT(Microsoft’s C/C++ Runtime Library)的一个真子集(主要是C++ Runtime Library)是一个符合(或至少是企图符合)C++标准的C++库。而Windows API(以及Windows的其他许多部分)都是在CRT的基础上开发的。 ↩︎
-
既然称作轻量级进程,可见其本质仍然是进程,与普通进程相比,LWP与其它进程共享所有(或大部分)逻辑地址空间和系统资源,一个进程可以创建多个LWP,这样它们共享大部分资源;LWP有它自己的进程标识符,并和其他进程有着父子关系;这是和类Unix操作系统的系统调用vfork()生成的进程一样的。LWP由内核管理并像普通进程一样被调度。Linux内核是支持LWP的典型例子。Linux内核在 2.0.x版本就已经实现了轻量进程,应用程序可以通过一个统一的clone()系统调用接口,用不同的参数指定创建轻量进程还是普通进程,通过参数决定子进程和父进程共享的资源种类和数量,这样就有了轻重之分*。在内核中, clone()调用经过参数传递和解释后会调用do_fork(),这个核内函数同时也是fork()、vfork()系统调用的最终实现。在大多数系统中,LWP与普通进程的区别也在于它只有一个最小的执行上下文和调度程序所需的统计信息,而这也是它之所以被称为轻量级的原因。* 因为LWP之间共享它们的大部分资源,所以它在某些应用程序就不适用了;这个时候就要使用多个普通的进程了。例如,为了避免内存泄漏(a process can be replaced by another one)和实现特权分隔(processes can run under other credentials and have other permissions)。↩︎