总言
多线程:进程线程基本概念、线程控制、互斥与同步。
文章目录
- 总言
- 1、基本概念
- 1.1、补充知识
- 1.1.1、堆区细粒度划分
- 1.1.2、虚拟地址到物理空间的转化
- 1.2、如何理解线程、进程
- 1.2.1、如何理解线程?
- 1.2.2、如何理解进程?
- 1.3、实践操作
- 1.3.1、基本演示(线程创建)
- 1.3.2、线程如何看待进程内部的资源?
- 1.3.3、进程VS线程(调度层面上)
- 2、线程控制
- 2.1、线程创建
- 2.1.1、函数介绍
- 2.1.2、演示
- 2.2、线程等待
- 2.2.1、函数介绍
- 2.2.2、演示一:验证退出有序
- 2.2.3、演示二:线程返回值
- 2.2.4、演示三:线程返回值2.0
- 2.3、线程终止
- 2.3.1、exit终止进程
- 2.3.2、pthread_exit
- 2.3.3、pthread_cancel
- 2.4、线程ID探索:pthread_self()
- 2.5、其它验证
- 2.5.1、验证全局区的数据能被多进程共享(__thread介绍)
- 2.5.2、如果在线程中使用了execl系列进程替换函数,会发生什么?
- 2.6、线程分离
- 3、线程互斥与同步
- 3.1、线程互斥
- 3.1.1、问题引入与概念介绍
- 3.1.2、互斥锁
- 3.1.2.1、相关涉及函数
- 3.1.2.2、使用一:静态、全局方式
- 3.1.2.3、使用二:动态、局部方式
- 3.1.2.4、问题说明(由实践到理论理解)
- 3.1.3、锁的原理
- 3.1.4、死锁
- 3.1.5、可重入VS线程安全
- 3.2、线程同步
- 3.2.1、问题引入与概念介绍
- 3.2.2、方案一:条件变量
- 3.2.2.1、方案说明与函数介绍
- 3.2.2.2、方案演示1.0
1、基本概念
1.1、补充知识
1.1.1、堆区细粒度划分
问题:堆区里有很多申请到的小空间,那么如何知道哪块区域是一个整体?以及如何找到对应堆区申请的空间?
回答:struct_vm_area_sturct
结构体。每次在堆区申请空间,会生成这样一个结构体,vm_start
、vm_end
能记录所申请空间的首尾位置。将这些结构体以双链表的形式链接起来,就可通过vm_next
、vm_prev
找到每个空间位置。
struct vm_area_struct {
unsigned long vm_start; // Our start address within vm_mm.
unsigned long vm_end; // The first byte after our end address within vm_mm.
// linked list of VM areas per task, sorted by address
struct vm_area_struct *vm_next, *vm_prev;
//…………
//其它内容
}
说明:OS是可以做到让进程进行资源的细粒度划分。
1.1.2、虚拟地址到物理空间的转化
①我们的可执行程序在编译阶段,就已经以4KB为单位按照虚拟地址的区域被划分。(页帧)
②物理内存也是以4KB为单位划分为一个个小块,并以struct page{ }
结构体来管理。(页框)
Linux内核将整个物理内存按照页对齐方式划分成千上万个页进行管理。由于一个物理页用一个struct page表示,那么系统会有成千上万个struct page结构体,这些结构体也会占用实际的物理内存,因此,内核选择用union联合体来减少内存的使用。
③IO的基本单位是4KB。相当于把页帧装进页框里。
1.2、如何理解线程、进程
1.2.1、如何理解线程?
在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
说明:
1、通过进程虚拟地址空间可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
2、因此,线程在进程内部运行的(线程在进程地址空间内运行),是OS调度的基本单位(CPU进行调度时,不关心执行流是进程还是线程,只关心PCB)。
3、一切进程至少都有一个执行线程。
4、不同操作系统下(Linux、windows等),线程的实现方案不同(只要满足设定的条件规则即可)。上述图示的是Linux的线程方案,实际上Linux没有真正意义上的线程结构,是用进程PCB模拟的。也因此,Linux并不能直接给我们提供线程相关的接口,只能提供轻量级进程的接口(Linux下,PCB<=其它OS的PCB,故将Linux进程称之为轻量级进程)。
5、pthread线程库(Linux系统自带的原生线程库):在用户层实现了一套用户层多线程方案,以库的方式提供给用户进行使用。(相当于省去一定的学习线程库实现的成本,只需要会调用该线程库即可)。
1.2.2、如何理解进程?
用户视角:进程 = 内核数据结构(可存在多个PCB)+ 该进程对应的代码和数据。
内核视角:进程是承担分配系统资源的基本实体。(进程向操作系统申请系统资源,此后线程的资源分配就由进程来执行,即OS角度,这些PCB、虚拟地址、页表等是以进程为单位申请的。)
如何理解曾经我们所写的代码?
以前我们所写的可执行程序,属于内部只有一个执行流的进程。引入线程后,可以有内部有多个执行流的进程。
1.3、实践操作
1.3.1、基本演示(线程创建)
1)、相关函数介绍和使用说明
man pthread_create
:创建一个新的线程。
NAME
pthread_create - create a new thread
SYNOPSIS
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.
参数:
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数(start_routine)的参数
返回值:成功返回0;失败返回错误码
RETURN VALUE
On success, pthread_create() returns 0; on error, it returns an error number, and the contents
of *thread are undefined.
其它:由于调用的是操作系统库,因此 使用gcc/g++
时需要加上选项表示对应库的名称:-lpthread
。 PS:关于动静态库如何使用的相关细节说明见博文:Linux || 基础IO(二)。
2)、演示一:基本使用演示
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
相关代码如下:
#include<iostream>
#include<pthread.h>
#include<stdio.h>
#include<unistd.h>
#include<string>
using namespace std;
void* threadRun(void * args)
{
string name = (char*)args;//字符串首元素地址、赋值运算符
while(true)
{
cout << name << " , pid:" << getpid() << "\n" << endl;
sleep(1);
}
}
int main()
{
pthread_t tid[5];//创建5个线程
char threadname[64];//用于arg参数传递线程名称,以便区分
for(int i = 0; i < 5; ++ i)
{
//snprintf每次都会向threadname数组中写入字符串。(thread-1、thread-2、thread-3、……)
snprintf(threadname, sizeof(threadname) ,"%s-%d","thread",i);
pthread_create(tid+i, nullptr, threadRun, (void*)threadname);
sleep(1);//此处是缓解传参BUG
}
while(true)
{
cout << " main thread , pid:" << getpid() << endl;
sleep(3);
}
return 0;
}
以下为演示结果:
相关说明:
1、ps -aL
:可查看线程。
2、主线程和新线程运行顺序是不确定的,取决于调度器(和父子进程顺序不定一样)
3、由上图可知,CPU调度时看的是LWP,而非PID。因为当有多个线程时,LWP唯一,但PID可以对应多个线程。(PS:对于单线程的进程,其LWP和PID一样,故CPU看的仍旧是LWP。)
4、kill -9 PID
:用于杀掉一个进程,需要注意,内部所有线程都被杀掉。
main thread , pid:28571
thread-1 , pid:28571
thread-0 , pid:28571
thread-3 , pid:28571
thread-2 , pid:28571
thread-4 , pid:28571
Killed #kill -9 28571 杀掉进程,对于进程内所有线程都被杀掉
[wj@VM-4-3-centos T0927]$
1.3.2、线程如何看待进程内部的资源?
进程是资源分配的基本单位,线程是调度的基本单位。
1、线程共享进程数据,但也拥有自己的一部分数据,如:
①线程ID;
②一组寄存器;
③栈(一般认为独自占用);
④errno
错误码;
⑤信号屏蔽字;
⑥调度优先级。
2、除了上述全局变量在各线程中都可以访问到,各线程还共享以下进程资源和环境:
①文件描述符表;
②每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数);
③当前工作目录;
④用户id和组id;
⑤代码区、全局数据区(已初始化/未初始化)、堆区、共享区。
1.3.3、进程VS线程(调度层面上)
1)、为什么说线程切换的成本更低?
1、地址空间、页表不需要被切换。(假如调度的是另外的进程PCB,则上下文、页表、地址空间等都需要切换,故而比线程切换成本更高)。
2、对于线程,CPU内部有L1~L3 cache(缓存),对内存的代码和数据根据局部性原理预读到CPU内部。(若是进程切换,cache会失效,新进程只能重新缓存)。
2、线程控制
1)、总览
2)、POSIX线程库
1、与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_
”打头。
2、要使用这些函数库,要通过引入头文<pthread.h>
。
3、链接这些线程函数库时要使用编译器命令的“-lpthread
”选项。
2.1、线程创建
2.1.1、函数介绍
1)、函数介绍
pthread_create
函数在上述小节中已经演示过,此处只做补充说明。
NAME
pthread_create - create a new thread
SYNOPSIS
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.
参数:
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数(start_routine)的参数
返回值:成功返回0;失败返回错误码
RETURN VALUE
On success, pthread_create() returns 0; on error, it returns an error number, and the contents
of *thread are undefined.
2.1.2、演示
2)、演示线程异常
演示代码如下:
PS:当创建线程成功,新线程执行对应的threadRoutine
(参数start_routine)内部内容,主线程继续执行ptread_create
后面的代码。
void* threadRoutine(void * args)
{
while(true)
{
sleep(2);
cout << "新线程: " << (char*)args << " , is runing. " << endl;
int a = 10;
a /= 0 ;//error
}
}
int main()
{
fflush(stdout);
pthread_t tid; // 创建一个线程
pthread_create(&tid, nullptr, threadRoutine, (void *)"new thread");
while (true)
{
cout << " main thread , pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
演示结果如下:
线程异常说明:
1、单个线程如果出现除零、野指针等问题导致线程崩溃,进程也会随着崩溃;
2、线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
2.2、线程等待
2.2.1、函数介绍
1)、 为什么需要线程等待?
回答:对于已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。若主线程不等待资源回收,那么新创建的线程不会复用刚才退出线程的地址空间。如此,就会造成类似于僵尸进程的问题。
2)、函数介绍
man pthread_join
:等待线程结束。调用该函数的线程将挂起等待,直到id为thread的线程终止。
NAME
pthread_join - join with a terminated thread
SYNOPSIS
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
Compile and link with -pthread.
DESCRIPTION
The pthread_join() function waits for the thread specified by thread to terminate. If that
thread has already terminated, then pthread_join() returns immediately. The thread specified
by thread must be joinable.
参数
thread:线程ID
retval:它指向一个指针,后者通常是线程thread运行结束后的返回值
返回值:成功返回0;失败返回错误码
RETURN VALUE
On success, pthread_join() returns 0; on error, it returns an error number.
说明:thread
线程以不同的方法终止,通过pthread_join
得到的终止状态是不同的,总结如下:
1. 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,retval所指向的单元里存放的是常数PTHREAD_ CANCELED。(宏:-1)
3. 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是pthread_exit传递的参数。
4. 如果对thread线程的终止状态不感兴趣,可以将retval设置为NULL。
2.2.2、演示一:验证退出有序
演示代码如下:
void* pthreadRoutine(void * args)
{
cout << (char*)args << ": runing." << endl;
int count = 5;
do{
cout << "new thread: " << count << endl;
sleep(1);
}while(count--);
cout << "new thread: quit." << endl;
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");
cout << "main thread: create succeed." << endl;
pthread_join(tid, nullptr);
cout << "main thread: wait succeed, main quit." << endl;
return 0;
}
演示结果如下:
可用脚本来观察:
while :; do ps -aL | head -1 && ps -aL | grep thread; sleep 1; done
2.2.3、演示二:线程返回值
问题: 在上述演示代码中,void* pthreadRoutine(void * args)
, 执行函数会返回一个(void*)
,该返回值是给谁?
回答: 谁来等待,就给谁。一般是给主线程,主线程可通过线程等待pthread_join
来知道结果,即该函数的第二参数: void **value_ptr
。
PS:注意其参数类型是void**
,这里属于输出型参数,获取pthread_create的返回值(void*),改变了实参value_ptr,那么需要二级指针变量。
演示代码如下:
void* pthreadRoutine(void * args)
{
cout << (char*)args << ": runing." << endl;
int count = 5;
do{
cout << "new thread: " << count << endl;
sleep(1);
}while(count--);
cout << "new thread: quit." << endl;
return (void*)22;//注意点1:从整形转变为void*类型,相当于将地址数据为22处返回。
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");
cout << "main thread: create succeed." << endl;
void* ret = nullptr;//注意点2:用于接收pthreadRoutine,新线程返回值
pthread_join(tid, &ret);//注意点2:要想实参被修改,则需要传址,void*的地址类型为void**
cout << "ret: " << ret << " , ret:" << (long long)ret <<endl;//注意点3:Linux下指针为8字节,故此处强转int类型(4字节)不适用(会出现截断问题)
cout << "main thread: wait succeed, main quit." << endl;
return 0;
}
演示结果如下:
2.2.4、演示三:线程返回值2.0
除了上述返回一个值外,线程的返回值具有可玩性,运用恰当可以做一些有意义的操作。以下为一个代码举例,我们可以让新线程做一些运算,并将结果存储在堆中,或以其它方式返回给主线程。
演示代码如下:虽然是新线程申请的动态空间,但堆区在线程间能够共享,所以主线程也能看到。
void* pthreadRoutine(void * args)
{
cout << (char*)args << ": runing." << endl;
int* data = new int[5];
for(int count = 0 ; count < 5; ++count)
{
cout << "new thread: " << count << endl;
data[count] = count;
sleep(1);
}
cout << "new thread: quit." << endl;
return (void*)data;//返回了堆上申请的空间(新线程)
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");
cout << "main thread: create succeed." << endl;
int* ret = nullptr;//用于接收pthreadRoutine,新线程返回值
pthread_join(tid, (void**)&ret);
cout << "main thread: wait succeed." << endl;
for(int i = 0; i < 5; ++i)
{
cout << ret[i] << ' ';
}
cout << endl;
return 0;
}
演示结果如下:
2.3、线程终止
2.3.1、exit终止进程
说明:exit
是用于终止进程的,调用它不仅仅当前线程会被终止,整个进程都会终止。
演示代码如下:
void* pthreadRoutine(void * args)
{
cout << (char*)args << ": runing." << endl;
int* data = new int[5];
for(int count = 0 ; count < 5; ++count)
{
cout << "new thread: " << count << endl;
data[count] = count;
sleep(1);
}
cout<< "now, exit the new thread." << endl;
exit(22);//使用exit终止新线程
cout << "new thread: quit." << endl;
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");
cout << "main thread: create succeed." << endl;
pthread_join(tid, nullptr);//阻塞式等待
cout << "main thread: wait succeed." << endl;
while(true)
{
cout << " main still runing." << endl;
sleep(1);
}
return 0;
}
演示结果如下:
2.3.2、pthread_exit
1)、函数介绍
man pthread_exit
:线程可以调用该函数终止自己。
NAME
pthread_exit - terminate calling thread
SYNOPSIS
#include <pthread.h>
void pthread_exit(void *retval);
Compile and link with -pthread.
DESCRIPTION
The pthread_exit() function terminates the calling thread and returns a value via
retval that (if the thread is joinable) is available to another thread in the same
process that calls pthread_join(3).
参数:
retval:返回指针,用于存储退出线程的返回数据,注意不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。
RETURN VALUE
This function does not return to the caller.
2)、使用演示
演示代码如下:根据之前所学,如果thread线程是调用pthread_exit
终止的,retval
参数所指向的单元将会传递给pthread_join
。
void* pthreadRoutine(void * args)
{
cout << (char*)args << ": runing." << endl;
int* data = new int[5];
for(int count = 0 ; count < 5; ++count)
{
cout << "new thread: " << count << endl;
data[count] = count;
sleep(1);
}
cout<< "now, exit the new thread." << endl;
pthread_exit((void*)11);//使用exit终止新线程
cout << "new thread: quit." << endl;
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");
cout << "main thread: create succeed." << endl;
int* ret = nullptr;
pthread_join(tid, (void**)&ret);//阻塞式等待,接收来自pthread_exit的参数值
cout << "main thread: wait succeed, the return value:" << (long long)ret << endl;
while(true)
{
cout << " main still runing." << endl;
sleep(1);
}
return 0;
}
演示结果如下:
2.3.3、pthread_cancel
1)、函数介绍
man pthread_cancel
:取消一个执行中的线程。
NAME
pthread_cancel - send a cancellation request to a thread
SYNOPSIS
#include <pthread.h>
int pthread_cancel(pthread_t thread);
Compile and link with -pthread.
DESCRIPTION
The pthread_cancel() function sends a cancellation request to the thread thread.
Whether and when the target thread reacts to the cancellation request depends on
two attributes that are under the control of that thread: its cancelability state
and type.
参数
thread:线程ID
返回值:成功返回0,失败返回错误码
RETURN VALUE
On success, pthread_cancel() returns 0; on error, it returns a nonzero error num‐
ber.
2)、使用演示
演示代码如下:
void* pthreadRoutine(void * args)
{
cout << (char*)args << ": runing." << endl;
size_t count = 0;
while(true)//让新线程一直循环运行
{
cout << "new thread: " << count++ << endl;
sleep(1);
}
cout << "new thread: quit." << endl;
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");
cout << "main thread: create succeed." << endl;
sleep(6);//让新线程运行6s后,取消新线程。
pthread_cancel(tid);
int* ret = nullptr;
pthread_join(tid, (void**)&ret);//阻塞式等待,接收来自pthread_cancel的返回值
cout << "main thread: wait succeed, the return value:" << (long long)ret << endl;
int count = 5;//让主线程在新线程退出后再运行一段时间
while(count--)
{
cout << " main thread: runing." << endl;
sleep(1);
}
cout << "main quit." << endl;
return 0;
}
演示结果如下:
PS:不要在随意位置使用终止函数,按照场景需求正常使用即可。一般是主线程中使用,取消新线程;虽然没说不可以在新线程中取消主线程,但有可能会引起一些奇怪问题。
2.4、线程ID探索:pthread_self()
1)、问题引入
此处使用2.3.3中演示代码,对main函数稍加修改,打印tid
值:
pthread_cancel(tid);
int* ret = nullptr;
pthread_join(tid, (void**)&ret);//阻塞式等待,接收来自pthread_cancel的返回值
cout << "main thread: wait succeed, the return value:" << (long long)ret <<" ,tid:" << tid << endl;
如下,tid:140604466824960
,是一个很大的数字。这似乎与我们之前学习到的进程ID,文件描述符fd等都不同,且也并非我们用ps -aL 指令查看到的LWP值。
[wj@VM-4-3-centos T0927]$ ./thread.out
main thread: create succeed.
new thread: runing.
new thread: 0
new thread: 1
new thread: 2
new thread: 3
new thread: 4
new thread: 5
main thread: wait succeed, the return value:-1 ,tid:140604466824960
main thread: runing.
main thread: runing.
main thread: runing.
main thread: runing.
main thread: runing.
main quit.
[wj@VM-4-3-centos T0927]$
那么,这里的线程ID究竟什么?
2)、解释说明
结论:对于Linux目前实现的实现而言,pthread_t
类型的线程ID,本质就是一个进程地址空间上的一个地址。
主线程用的是虚拟地址的栈结构,新线程用的是库里提供的私有栈结构。
pthread_self()
可以获得线程自身的ID,在哪个线程中使用,获取的就是哪个线程的ID。
演示代码如下:
void* pthreadRoutine(void * args)
{
size_t count = 3;
while(count--)
{
cout << (char*)args << ": runing." << " tid:" << pthread_self() << endl;
sleep(1);
}
cout << "new thread: quit." << endl;
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");//创建新线程
pthread_join(tid, nullptr);//阻塞式等待新线程
int count = 3;//让主线程在新线程退出后再运行一段时间
while(count--)
{
cout << "main thread: runing." " tid:" << pthread_self() << endl;
sleep(1);
}
cout << "main quit." << endl;
return 0;
}
演示结果如下:
[wj@VM-4-3-centos T0927]$ make
g++ -o thread.out mythread.cc -std=c++11 -lpthread
[wj@VM-4-3-centos T0927]$ ls
makefile mythread.cc thread.out
[wj@VM-4-3-centos T0927]$ ./thread.out
new thread: runing. tid:140510573528832
new thread: runing. tid:140510573528832
new thread: runing. tid:140510573528832
new thread: quit.
main thread: runing. tid:140510591952704
main thread: runing. tid:140510591952704
main thread: runing. tid:140510591952704
main quit.
[wj@VM-4-3-centos T0927]$
2.5、其它验证
2.5.1、验证全局区的数据能被多进程共享(__thread介绍)
1)、相关演示
演示代码:
int val = 0;
void* Routine(void* args)
{
while(true)
{
//cout << "new thread," << ", val:" << val << ", &val:" << &val << endl;
printf("new thread, val:%d, &val:%p\n",val,&val);
val++;
sleep(1);
}
}
int main()
{
pthread_t tid = 0;
pthread_create(&tid, nullptr, Routine, (void*)"new thread");
while(true)
{
//cout << "main thread" << ", val:" << val << ", &val:" << &val << endl;
printf("main thread, val:%d, &val:%p\n",val,&val);
sleep(1);
}
pthread_join(tid ,nullptr);
return 0;
}
演示结果:
2)、若想让全局变量不被共享,如何操作?
__thread
:修饰全局变量,可以让该全局变量被每一个线程独自占有(线程的局部存储)。
注意这里的的__
是两个_
。使用如下:__thread int val = 0;
2.5.2、如果在线程中使用了execl系列进程替换函数,会发生什么?
演示代码:
void* Routine(void* args)
{
while(true)
{
sleep(3);
execl("/bin/ls","ls",nullptr);//进程替换
printf("new thread,tid:%u\n",pthread_self());
sleep(1);
}
}
int main()
{
pthread_t tid = 0;
pthread_create(&tid, nullptr, Routine, (void*)"new thread");
while(true)
{
printf("main thread,tid:%u\n",pthread_self());
sleep(1);
}
pthread_join(tid ,nullptr);
return 0;
}
演示结果如下:
需要注意,语言级别的线程(如C++中也提供了线程),无论再怎么支持,其底层还是使用的是原生系统的线程库。
2.6、线程分离
1)、函数介绍
man pthread_detach
:默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join
操作,否则无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时自动释放线程资源。
NAME
pthread_detach - detach a thread
SYNOPSIS
#include <pthread.h>
int pthread_detach(pthread_t thread);
Compile and link with -pthread.
DESCRIPTION
The pthread_detach() function marks the thread identified by thread as detached. When a detached
thread terminates, its resources are automatically released back to the system without the need for
another thread to join with the terminated thread.
Attempting to detach an already detached thread results in unspecified behavior.
RETURN VALUE
On success, pthread_detach() returns 0; on error, it returns an error number.
1、可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。
2、线程分离和线程等待是冲突的,一个线程不能既是joinable又是分离的。
2)、演示一
演示代码如下:
void* Routine(void* args)
{
//可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
pthread_detach(pthread_self());
int count = 5;
while(count--)
{
printf("new thread,tid:%u, count:%d\n",pthread_self(),count);
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid = 0;
pthread_create(&tid, nullptr, Routine, (void*)"new thread");
int count = 5;
while(count--)
{
printf("main thread, tid:%u, count:%d\n",pthread_self(),count);
sleep(1);
}
cout <<"thr result:" << strerror(pthread_join(tid ,nullptr)) << endl;
return 0;
}
演示结果如下:
3)、演示二:线程分离,若该线程异常,是否会影响主线程?
回答:会,同一个进程中,资源还是共享的。
3、线程互斥与同步
3.1、线程互斥
3.1.1、问题引入与概念介绍
1)、问题引入:不加保护的情况下,多线程抢票逻辑
问题说明:如果多线程访问同一个全局变量,并对它进行数据计算,多线程会相互影响吗?
演示代码:
int tickets = 1000;
void* getTickets(void* args)
{
(void)args;
while(true)
{
if(tickets > 0 )
{
usleep(1000);//休眠
printf("%p: %d\n", pthread_self(), tickets--);
}
else break;
}
return nullptr;
}
int main()
{
pthread_t tid1, tid2, tid3;//一次创建多个线程
pthread_create(&tid1, nullptr, getTickets, nullptr);
pthread_create(&tid2, nullptr, getTickets, nullptr);
pthread_create(&tid3, nullptr, getTickets, nullptr);
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_join(tid3, nullptr);
return 0;
}
演示结果:根据上述代码,printf
只会打印出tickets > 0
的数,可运行程序我们发现最后总会有ticket== -1
。为什么会出现此现象?
2)、原因解释1.0
分析上述代码,tickets>0
是逻辑运算,会在CPU中进行,同理,tickets--
也是在CPU中进行的。这就需要将物理地址中存储的tickets变量载入进程(当前执行流)的上下文,在线程被调度时CPU执行计算操作。
但需要注意的是,tickets--
实则为三步操作:①读取数据到CPU寄存器;②CPU内部进行数据计算;③将结果写回内存。若在此期间,线程因为CPU调度被切换,那么对于tickets的相关操作,无论执行到哪一步骤,都会随PCB上下文被切换走,直到下次再被调度。
由此,在不加保护的情况下,多个线程对同一数据不具有实时同步性,会导致并发访问时数据不一致。
3)、一些概念
临界资源: 多线程执行流共享的资源就叫做临界资源。
临界区: 每个线程内部,访问临界资源的代码,就叫做临界区(实际还有很大一部分代码段属于普通代码)。
互斥: 任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。
3.1.2、互斥锁
针对上述问题,一个避免方法是加锁保护。
3.1.2.1、相关涉及函数
1、对锁初始化(初始化互斥量):
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
mutex:要初始化的互斥量
attr:NULL
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
2、加锁、解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
3、销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
①使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
②不要销毁一个已经加锁的互斥量
③已经销毁的互斥量,要确保后面不会有线程再尝试加锁
3.1.2.2、使用一:静态、全局方式
1)、代码演示1.0
如下:
int tickets = 1000;
//1、定义一个锁并对其初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* getTickets(void* args)
{
(void)args;
while(true)
{
//2、加锁:
pthread_mutex_lock(&mutex);
if(tickets > 0 )
{
usleep(1000);//休眠
printf("%p: %d\n", pthread_self(), tickets--);
pthread_mutex_unlock(&mutex);//3、解锁
}
else
{
pthread_mutex_unlock(&mutex);//3、解锁
break;
}
}
return nullptr;
}
int main()
{
pthread_t tid1, tid2, tid3;//一次创建多个线程
pthread_create(&tid1, nullptr, getTickets, nullptr);
pthread_create(&tid2, nullptr, getTickets, nullptr);
pthread_create(&tid3, nullptr, getTickets, nullptr);
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_join(tid3, nullptr);
return 0;
}
演示结果:
解释说明:
1、if…else
语句中,break退出前需要解锁:
2、能明显看出与不加锁时相比,运行速度变慢。(可以获取一个时间来验证,time、gettimeofday)。
3、可加入随机数,让持锁进程更加随机。(实则我们演示时多个线程都有参与)
//int main()
srand((unsigned int)time(nullptr)^getpid());//种子:用于让持锁线程更加随机
//void* getTickets(void* args)
usleep(rand() % 1500);//休眠:让休眠随机一点
4、加锁时容易影响效率,为了保证加锁粒度,加锁区域越小越好。
3.1.2.3、使用二:动态、局部方式
int tickets = 10000;
#define THREAD_NUM 5 //待创建线程数目
class ThreadData // 用于创建线程时,args传参:线程名、锁
{
public:
ThreadData(const string &name, pthread_mutex_t *pmutex)
: _name(name), _pmutex(pmutex)
{}
string _name;
pthread_mutex_t* _pmutex;
};
void *getTickets(void *args)
{
ThreadData* td = (ThreadData*)args;
while(true)
{
//3、加锁:
pthread_mutex_lock(td->_pmutex);
if(tickets > 0 )
{
usleep(rand() % 1500);//休眠:让休眠随机一点
printf("%s: %d\n", td->_name.c_str(), tickets--);
pthread_mutex_unlock(td->_pmutex);//3、解锁
}
else
{
pthread_mutex_unlock(td->_pmutex);//3、解锁
break;
}
}
delete td;//销毁new出来的空间
return nullptr;
}
int main()
{
srand((unsigned int)time(nullptr) ^ getpid()); // 种子:用于让持锁线程更加随机
clock_t t1 = clock(); // 测试时间
pthread_mutex_t mutex; // 1、定义一个锁
pthread_mutex_init(&mutex, nullptr); // 2、对锁初始化
// 创建线程
pthread_t tid[THREAD_NUM]; // 线程ID
for (int i = 0; i < THREAD_NUM; ++i)
{
string name = "thread"; // 线程名
name += std::to_string(i + 1); // 线程名
ThreadData *td = new ThreadData(name, &mutex);
pthread_create(tid + i, nullptr, getTickets, (void *)td);
}
// 等待线程
for (int i = 0; i < THREAD_NUM; ++i)
{
pthread_join(tid[i], nullptr);
}
//4、销毁锁
pthread_mutex_destroy(&mutex);
clock_t t2 = clock();
cout << "time: " << (t2 - t1) << endl;
return 0;
}
3.1.2.4、问题说明(由实践到理论理解)
1)、加锁之后,线程在临界区中是否切换,是否会有问题?
回答:会切换,但不会有问题。
第一次理解:当前线程虽然被切换了,但其是持有锁被切换的。而其他抢票线程要执行临界区代码,也必须先申请锁,此时锁无法申请成功的,所以,也不会让其他线程进入临界区,由此保证了临界区中数据一致性!
2)、原子性体现?
回答:设线程1持有锁,在未持有锁的线程看来,对其最有意义的情况只有两种:1. 线程1没有持有锁(什么都没做) 2. 线程1释放锁(做完),此时我可以申请锁。
3)、加锁就是串行执行了吗?
回答:是的,执行临界区代码一定是串行的。
3.1.3、锁的原理
1)、问题引入
要访问临界资源,每一个线程都必须先申请锁,每一个线程都必须先看到同把一锁并能够访问它。这就味着锁本身就是一种共享资源。所以,为了保证锁的安全,申请和释放锁,必须是原子的。
那么,谁来保证?如何保证?锁是如何实现的?
2)、原理解释
为了实现互斥锁操作,大多数体系结构都提供了swap
或exchange
指令,该指令的作用是把寄存器和内存单元的数据相交换。在汇编角度,若只有一条汇编语句,则认为该汇编指令是原子性。
即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
PS:实际底层还有过多概念原理,这里只是一层理解。
1、谁来保证锁的安全?
回答:锁自身。加锁、解锁这步动作都只涉及一行汇编。
3.1.4、死锁
1)、概念
死锁是指在一组进程中的各个进程均占有不会释放的资源,因互相申请被其他进程所站用的不会释放的资源,使得彼此处于一种永久等待的状态。
以下为一种线程自己把自己弄成死锁的场景举例:
void *getTickets(void *args)
{
ThreadData* td = (ThreadData*)args;
while(true)
{
//此处已经加锁
pthread_mutex_lock(td->_pmutex);
if(tickets > 0 )
{
usleep(rand() % 1500);
printf("%s: %d\n", td->_name.c_str(), tickets--);
pthread_mutex_lock(td->_pmutex);//在加锁后,尚未解锁前,再次加锁,那么即使是线程本身,也是申请失败的。
pthread_mutex_unlock(td->_pmutex);//解锁
}
else
{
pthread_mutex_unlock(td->_pmutex);//解锁
break;
}
}
delete td;//销毁new出来的空间
return nullptr;
}
2)、死锁的必要条件
互斥条件:一个资源每次只能被一个执行流使用。(产生死锁,正是因为牵扯到互斥)
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。(类似于吃着碗里的还看着锅里的)
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。
PS:只要产生死锁,必然是这四个条件都被满足。反之,若破坏其中某个条件,就无法达成死锁。
3)、如何避免死锁
破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配
3.1.5、可重入VS线程安全
1)、概念
线程安全:多个线程并发同一段代码时,不会出现不同的结果,则说明线程是安全的。常见对全局变量或者静态变量进行操作时,在没有锁保护的情况下会出现并发问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,称为不可重入函数。
2)、常见的线程不安全和线程安全的情况
线程不安全:
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
线程安全:
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
3)、常见的可重入和不可重入的情况
不可重入:
调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
可重入:
不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
4)、联系与区别
联系:
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
区别:
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
3.2、线程同步
3.2.1、问题引入与概念介绍
1)、引入:上述互斥锁是否存在什么问题?
回答:存在以下两种不合理的行为(虽然没有错误,但不合适)
1、在拥有资源时,某个线程频繁的申请到资源,导致其它线程处于"饥饿"状态(长时间得不到资源)。
2、在资源短缺时,某个线程频繁申请失败,浪费彼此时间。
为了解决上述访问临界资源合理性的问题,我们引入同步的概念。
12)、什么叫做同步?
说明:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步(线程同步)。
那么,如何实现同步?
3.2.2、方案一:条件变量
3.2.2.1、方案说明与函数介绍
1)、在使用条件变量前的一些理解说明
申请临界资源前,要先对临界资源是否存在做出检测,而检测本身也是在访问临界资源,也需要对其进行加锁解锁。常规方式下, 线程检测条件是否就绪,就需要频繁申请和释放锁。(此时若临界资源不就绪,线程申请锁失败,相当于其在频繁地加锁解锁做无意义的耗时行为)
void *getTickets(void *args)
{
ThreadData* td = (ThreadData*)args;
while(true)
{
pthread_mutex_lock(td->_pmutex);//加锁
if(tickets > 0 )//如此处,在申请临界资源前,先对临界资源是否存在做了检测。而该检测是在加锁之后进行的。
{
usleep(rand() % 1500);
printf("%s: %d\n", td->_name.c_str(), tickets--);
pthread_mutex_lock(td->_pmutex);
pthread_mutex_unlock(td->_pmutex);//解锁
}
else
{
pthread_mutex_unlock(td->_pmutex);//解锁
break;
}
}
delete td;
return nullptr;
}
考虑到此,我们设置出方案,让线程在(首次)检测到资源不就绪时,①不再频繁地重复进行资源检测,而是处于等待状态;②当资源就绪时,能接收到相应通知,随后再进行资源申请和访问。
2)、条件变量涉及函数
3.2.2.2、方案演示1.0
演示代码:
#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>
using namespace std;
#define TNUM 4
volatile bool quit = false;//用于让线程结束循环,退出
typedef void(*func_t)(const string& name, pthread_mutex_t* pmx, pthread_cond_t* pcd);//函数指针
class ThreadData//用于create线程是,args传参
{
public:
ThreadData(const string& name, func_t func, pthread_mutex_t* pmx, pthread_cond_t* pcd)
:_name(name),_func(func),_pmx(pmx),_pcd(pcd)
{}
string _name;//线程名
func_t _func;//函数指针
pthread_mutex_t* _pmx;//锁
pthread_cond_t* _pcd;//条件变量
};
void func1(const string &name, pthread_mutex_t *pmx, pthread_cond_t *pcd)
{
while (!quit)
{
pthread_mutex_lock(pmx); // 加锁
// if(满足条件)(访问临界资源)
// else(不满足条件,等待临界资源就绪)
pthread_cond_wait(pcd, pmx);
cout << name.c_str() << " is running, action : F1--帮助键" << endl;
cout << endl;
pthread_mutex_unlock(pmx); // 解锁
}
}
void func2(const string &name, pthread_mutex_t *pmx, pthread_cond_t *pcd)
{
while (!quit)
{
pthread_mutex_lock(pmx); // 加锁
// if(满足条件)(访问临界资源)
// else(不满足条件,等待临界资源就绪)
pthread_cond_wait(pcd, pmx);
cout << name.c_str() << " is running, action : F2--重命名" << endl;
cout << endl;
pthread_mutex_unlock(pmx); // 解锁
}
}
void func3(const string &name, pthread_mutex_t *pmx, pthread_cond_t *pcd)
{
while (!quit)
{
pthread_mutex_lock(pmx); // 加锁
// if(满足条件)(访问临界资源)
// else(不满足条件,等待临界资源就绪)
pthread_cond_wait(pcd, pmx);
cout << name.c_str() << " is running, action : F3--搜索按钮" << endl;
cout << endl;
pthread_mutex_unlock(pmx); // 解锁
}
}
void func4(const string &name, pthread_mutex_t *pmx, pthread_cond_t *pcd)
{
while (!quit)
{
pthread_mutex_lock(pmx); // 加锁
// if(满足条件)(访问临界资源)
// else(不满足条件,等待临界资源就绪)
pthread_cond_wait(pcd, pmx);
cout << name.c_str() << " is running, action : F4--浏览器网址列表" << endl;
cout << endl;
pthread_mutex_unlock(pmx); // 解锁
}
}
void* Entry(void* args)
{
//所有新线程都会执行Entry函数,在Entry函数中每个线程又会执行其对应的func
ThreadData* td = (ThreadData*) args;
td->_func(td->_name, td->_pmx, td->_pcd);
sleep(1);
cout << td->_name.c_str() <<": " <<pthread_self() << endl;
delete td;//新线程有自己独立的栈结构,每一个td变量都在各自私有栈中保存,最后新线程运行结束时,记得释放掉申请出来的堆
return nullptr;
}
int main()
{
//创建并初始化锁、条件变量
pthread_cond_t cond;
pthread_mutex_t mutex;
pthread_cond_init(&cond, nullptr);
pthread_mutex_init(&mutex, nullptr);
//创建新线程
pthread_t tid[TNUM];
func_t funcs[TNUM] = {func1, func2, func3, func4};
for(int i = 0; i < TNUM; ++i)
{
string name = "thread";
name += to_string(i);
ThreadData* td = new ThreadData(name, funcs[i], &mutex, &cond);
pthread_create(tid+i, nullptr, Entry, (void*)td);
}
//主线程:唤醒新线程
int count = 10;//执行10s退出
while(count--)
{
cout << "awake thread: " << endl;
pthread_cond_signal(&cond);//任意唤醒一个线程:并不关心具体是哪一个
sleep(1);
}
quit = true;//此时func函数不满足条件,线程回到Entry
cout << endl << "quit -> true." << endl;
pthread_cond_broadcast(&cond);//虽然quit退出函数,但有线程处于等待条件状态,此处再统一唤醒。
//只是为了演示两种唤醒函数。
sleep(3);
cout << endl;
//等待新线程
for(int i = 0; i < TNUM; ++i)
{
pthread_join(tid[i],nullptr);
cout << "join thread: " << tid[i] << endl;
}
//销毁锁、条件变量
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return 0;
}
演示结果: