杨记

碎片化学习令人焦虑,系统化学习使人进步

0%

线程

linux中pthread_join()与pthread_detach()详解_魏波-的博客-CSDN博客_pthread_detach

pthread_join()和pthread_detach()二者的区别_modi000的博客-CSDN博客

NPTL

  1. 察看当前pthread库版本getconf GNU_LIBPTHREAD_VERSION

  2. NPTL实现机制(POSIX),Native POSIX Thread Library

  3. 使用线程库时gcc指定 –lpthread

  4. 查看线程函数列表man -k pthread,不存在则安装线程man page:sudo apt-get install manpages-posix-dev

线程概念

进程的概念体现出两个特点:资源(代码和数据空间、打开的文件等)以及调度/执行

线程是进程内的独立执行代码的实体和调度单元

一个进程内的所有线程共享进程的很多资源(这种共享又带来了同步问题)

什么是线程

LWPlight weight process轻量级的进程,本质仍是进程(在Linux环境下)

进程:独立地址空间,拥有PCB

线程:也有PCB,但没有独立的地址空间(共享)

区别:在于是否共享地址空间。 独居(进程);合租(线程)。

Linux下:

  • 线程:最小的执行单位
  • 进程:最小分配资源单位,可看成是只有一个线程的进程。

img

Linux线程实现原理

类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切。

  1. 轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone

  2. 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的

  3. 进程可以蜕变成线程

  4. 线程可看做寄存器和栈的集合

  5. 在linux下,线程最是小的执行单位;进程是最小的分配资源单位

察看LWP号:ps –Lf pid 查看指定进程里面的所有lwp号(和线程ID不同)

image-20220608181620634

image-20220608181050880

三级映射:进程PCB —> 页目录(可看成数组,首地址位于PCB中) —> 页表 —> 物理页面 —> 内存单元

对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。

但!线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。

实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone

如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。

因此:Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。

线程间共享

线程间共享

  • 进程指令
  • 内存地址空间(.text/.data/.bss/heap/共享库)
  • 文件描述符表
  • 信号处理程序
  • 当前工作目录
  • 用户ID和组ID

线程私有

  • 线程ID
  • 寄存器集合(包括PC和栈指针)
  • 栈(用于存放局部变量)
  • errno变量
  • 信号掩码
  • 调度优先级

线程优缺点

优点: 1. 提高程序并发性 2. 开销小 3. 数据通信、共享数据方便

缺点: 1. 库函数,不稳定 2. 调试、编写困难、gdb不支持 3. 对信号支持不好

优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。

线程与进程对比

image-20220409142558448

  • 线程只拥有少量在运行中必不可少的资源
    • PC指针:标识当前线程执行的位置
    • 寄存器:当前线程执行的上下文环境
    • 栈:用于实现函数调用、局部变量(局部变量是私有的)
  • 进程占用资源多,线程占用资源少,使用灵活
  • 线程不能脱离进程而存在,线程的层次关系,执行顺序并不明显,会增加程序的复杂度
  • 没有通过代码显示创建线程的进程,可以看成是只有一个线程的进程

控制原语对比

控制操作 进程操作API 线程操作API
创建 fork,vfork pthread_create
终止 exit pthread_exit
等待 waitwaitpid pthread_join
读取ID getpid pthread_self
杀死 kill pthread_cancel

线程ID

同进程一样,每个线程也有一个线程ID

进程ID在整个系统中是唯一的,线程ID只在它所属的进程环境中唯一

线程ID的类型是pthread_t,在Linux中的定义:/usr/include/bits/pthreadtypes.h(实际位置可能有变化)

  • typedef unsigned long int pthread_t

线程ID是用来区分线程的,线程号是用来分配资源。

线程启动

线程创建后等待系统调度,被调度后从线程启动例程函数

一次性创建多个线程,调度顺序与创建顺序无关

image-20220412170357636

线程终止

线程的三种终止方式

  • 线程从启动例程函数中返回,函数返回值作为线程的退出码
  • 线程被同一进程中的其他线程取消
  • 线程在任意函数中调用pthread_exit函数终止执行

线程控制原语

获取线程ID:pthread_self()

创建线程:pthread_create()

线程退出:pthread_exit()

等待线程并回收:pthread_join()

终止线程:pthread_cancel()

分离线程:pthread_detach()

判断两个线程ID是否相等:int pthread_equal(pthread_t t1, pthread_t t2); (目前没啥用,可以直接t1 == t2

pthread_self函数

man 3 pthread_self

pthread_self函数可以让调用线程获取自己的线程ID

1
2
#include <pthread.h>
pthread_t pthread_self(void);

返回值:调用线程的线程ID

线程ID:`pthread_t类型,本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现

线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)

注意:不应使用全局变量 pthread_t tid,在子线程中通过pthread_create传出参数来获取线程ID,而应使用pthread_self

pthread_create函数

man 3 pthread_create

pthread_create函数用于创建一个线程

1
2
3
4
5
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_rtn)(void *),
void *arg);

调用pthread_create函数的线程是所创建线程的父线程

参数:

  • thread:指向线程ID的指针,当函数成功返回时将存储所创建的子线程ID
  • attr:用于指定线程属性(一般直接传入空指针NULL,采用默认线程属性)
  • start_rtn:线程的启动例程函数(类似进程的main函数入口)指针,创建的线程首先执行该函数代码(可以调用其他函数)
  • arg:向线程的启动例程函数传递信息的参数

返回值:成功返回0,出错时返回各种错误码(Linux环境下,所有线程特点,失败均直接返回错误号。)

在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。start_routine函数接收一个参数,是通过pthread_createarg参数传递给它的,该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。start_routine的返回值类型也是void *,这个指针的含义同样由调用者自己定义。start_routine返回时,这个线程就退出了,其它线程可以调用pthread_join得到start_routine的返回值,类似于父进程调用wait(2)得到子进程的退出状态。

pthread_create成功返回后,新创建的线程的id被填写到thread参数所指向的内存单元。我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯一的,调用getpid(2)可以获得当前进程的id,是一个正整数值。线程id的类型是thread_t,它只在当前进程中保证是唯一的,在不同的系统中thread_t这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址,所以不能简单地当成整数用printf打印,调用pthread_self(3)可以获得当前线程的id。

attr参数表示线程属性,一般传NULL给attr参数,表示线程属性取缺省值。深入了解参考APUE。

代码示例

创建一个子线程

用法和进程创建子进程类似。

不同:进程结束(main函数return或exit),子线程不会继续执行,所以要等子线程执行完。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

void *thrd_func(void *arg)
{
printf("In thread: thread id = %u, pid = %u\n", pthread_self(), getpid());
return NULL;
}

int main()
{
pthread_t tid;
int ret;

printf("In main: thread id = %u, pid = %u\n", pthread_self(), getpid());
ret = pthread_create(&tid, NULL, thrd_func, NULL);
if(ret != 0)
{
fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
exit(-1);
}
sleep(1); // 去掉之后子线程可能没有输出

printf("In main: thread id = %u, pid = %u\n", pthread_self(), getpid());

return 0;
}

创建N个子线程

创建5个线程,每个线程打印自己的序号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <pthread.h>

void *thrd_func(void *arg)
{
//int i = *((int*)arg); // 传地址
int i = (int)arg; // 传值
sleep(i); // 使子线程按顺序打印输出
printf("%dth thread: thread id = %u, pid = %u\n", i+1, pthread_self(), getpid());
return NULL;
}

int main()
{
pthread_t tid[5];
int ret;

printf("In main: thread id = %u, pid = %u\n", pthread_self(), getpid());
int i;
for(i = 0; i < 5; ++i)
{
//ret = pthread_create(&tid[i], NULL, thrd_func, (void*)&i); // 传地址
ret = pthread_create(&tid[i], NULL, thrd_func, (void*)i); // 传值
if(ret != 0)
{
fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
exit(-1);
}
}
sleep(5); // 休眠5秒,等待子线程结束

printf("In main: thread id = %u, pid = %u\n", pthread_self(), getpid());

return 0;
}

注意代码中两种给子线程传参的方式:

1)传值:子线程按顺序输出

image-20220608225635395

会有警告warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]

因为将int强制转换为void*,再将void*强制转换为int

32位系统下指针大小为4字节,64位是8字节。int大小为4字节。int强制转换为void*,高位补0。void*再转回int,取低位。

2)传地址:出问题

image-20220608230509650

现象:第一次5个子线程都没有输出。第二次注释子进程中的睡眠,5个子线程输出6th,不合预期

原因:主线程循环5次创建5个子线程,i变为5,执行sleep,让出cpu。传地址后,子线程访问的i就不是预期的0、1、2、3、4,而是5,各自加1输出了6th。子线程不去掉sleep时,睡眠时间比主线程长,主线程结束后,整个进程结束,子线程没来得及输出。

线程共享全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

int var = 100;

void *tfn(void *arg)
{
var = 200;
printf("thread\n");
return NULL;
}

int main(void)
{
printf("At first var = %d\n", var);

pthread_t tid;
pthread_create(&tid, NULL, tfn, NULL);
sleep(1);
printf("after pthread_create, var = %d\n", var);

return 0;
}

image-20220608231551583

pthread_exit函数

man 3 pthread_exit

作用:退出当前线程

1
2
#include <pthread.h>
void pthread_exit(void *retval);
  • retval 该指针将参数传递给pthread_join函数(与exit函数参数用法类似)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <pthread.h>

void *func1(void *arg)
{
printf("In thread child: thread id = %u, pid = %u\n", pthread_self(), getpid());
return NULL;
}

void *thrd_func(void *arg)
{
printf("In thread: thread id = %u, pid = %u\n", pthread_self(), getpid());

pthread_t tid;
int ret;

ret = pthread_create(&tid, NULL, func1, NULL);

return NULL;
}

int main()
{
pthread_t tid;
int ret;

printf("In main: thread id = %u, pid = %u\n", pthread_self(), getpid());
ret = pthread_create(&tid, NULL, thrd_func, NULL);
if(ret != 0)
{
fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
exit(-1);
}

printf("In main: thread id = %u, pid = %u\n", pthread_self(), getpid());

pthread_exit((void*)1); //退出主控线程
// return 0; // 退出进程
}

image-20220609101307247

注意

多线程环境中,应尽量少用,或者不使用exit函数,取而代之使用pthread_exit函数,将单个线程退出。任何线程里exit导致进程退出,其他线程未工作结束,主控线程退出时不能returnexit

另注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

  • return:返回到调用者那里去
  • pthread_exit():将调用该函数的线程退出
  • exit: 将进程退出

phread_join函数

man 3 pthread_join

阻塞等待线程退出,获取线程退出状态。(对应进程中 waitpid() 函数)

1
2
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

调用该函数的父线程将一直被阻塞,直到指定的子线程终止

返回值:成功返回0,否则返回错误编号

参数:

  • thread:需要等待的子线程ID
  • retval:线程返回值
    • 若线程从启动例程返回returnretval将包含返回码
    • 若线程被取消pthread_cancelretval指向的内存单元值置为PTHREAD_CANCELED
    • 若线程通过调用pthread_exit函数终止,retval就是调用pthread_exit时传入的参数
    • 若不关心线程返回值,可直接将该参数设置为空指针NULL

结构体示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

typedef struct {
char ch;
int var;
char str[64];
} exit_t;

void *thrd_func(void *arg)
{
exit_t *retvar = (exit_t*)arg;

retvar->ch = 'm';
retvar->var = 200;
strcpy(retvar->str, "my thread\n");

pthread_exit((void*)retvar);
}

int main()
{
pthread_t tid;
int ret;
exit_t *retval = (exit_t*)malloc(sizeof(exit_t));
printf("In main : thread id=%lu, pid=%u\n", pthread_self(), getpid());
pthread_create(&tid, NULL, thrd_func, (void*)retval);
pthread_join(tid, (void**)&retval);
printf("ch = %c, var = %d, str = %s\n", retval->ch, retval->var, retval->str);

free(retval);
pthread_exit(NULL);
}

image-20220609142823616

回收N个线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

int var = 100;

void *tfn(void *arg)
{
int i;
i = (int)arg;

sleep(i);
if(i == 1) {
var = 333;
printf("var = %d\n", var);
return (void*)var;
}
else if(i == 3)
{
var = 777;
printf("I'm %dth pthread, pthread_id = %lu\n var = %d\n", i+1, pthread_self(), var);
pthread_exit((void*)var);
}
else
{
printf("I'm %dth pthread, pthread_id = %lu\n var = %d\n", i+1, pthread_self(), var);
pthread_exit((void*)var);
}

return NULL;
}

int main()
{
pthread_t tid[5];
int i;
int *ret[5];
for(i = 0; i < 5; ++i)
pthread_create(&tid[i], NULL, tfn, (void*)i);

for(i = 0; i < 5; ++i)
{
pthread_join(tid[i], (void**)&ret[i]);
printf("----------%d is ret = %d\n", i, (int)ret[i]);
}
printf("I'm main pthread tid = %lu\t var = %d\n", pthread_self(), var);
sleep(i);
return 0;
}

image-20220609153152276

pthread_cancel函数

man 3 pthread_cancel

线程调用该函数可以取消同一进程中的其他线程(即让该线程终止)

1
2
#include <pthread.h>
int pthread_cancel(pthread_t thread);

参数:tid 需要取消的线程ID

返回值:成功返回0,出错返回错误编号

注意事项

  • 在默认情况下,pthread_cancel函数与被取消线程(ID等于tid的线程)自身调用pthread_exit函数(参数为PTHREAD_CANCELED)效果等同
  • 线程可以选择忽略取消方式或者控制取消方式
  • pthread_cancel并不等待线程终止,它仅仅是提出请求

【注意】:线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。

取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write等,执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。也可参阅 APUE.12.7 取消选项小节。

如线程中没有取消点,可以通过调用pthread_testcancel函数自行设置一个取消点。

被取消的线程,退出值定义在Linux的pthread库中。常数PTHREAD_CANCELED的值是-1。可在头文件pthread.h中找到它的定义:#define PTHREAD_CANCELED ((void *) -1)。因此当我们对一个已经被取消的线程使用pthread_join回收时,得到的返回值为-1。

取消点示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

void *tfn1(void *arg)
{
printf("thread 1 returning\n");
return (void*)111;
}

void *tfn2(void *arg)
{
while(1) {
printf("thread 2 : I'm going to die in 3 seconds ... \n");
sleep(1);

// pthread_testcancel(); // 添加取消点
}
return (void*)666;
}

int main()
{
pthread_t tid;
void *tret = NULL;

pthread_create(&tid, NULL, tfn1, NULL);
pthread_join(tid, &tret);
printf("thread 1 exit code = %d\n\n", (int)tret);

pthread_create(&tid, NULL, tfn2, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &tret);
printf("thread 2 exit code = %d\n", (int)tret);

return 0;
}

运行结果:

image-20220609164540216

tfn2中的while中的语句注释掉,即第2个子线程一直运行但不涉及系统调用。第2 个子线程不会终止,主线程会一直阻塞等待

image-20220609165000944

pthread_detach函数

man 3 pthread_detach

作用:实现线程分离

1
2
#include <pthread.h>
int pthread_detach(pthread_t thread);

返回值:成功:0;失败:错误号

线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。

进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。

也可使用 pthread_create函数参2(线程属性)来设置线程分离。

特点:

  • 使用pthread_detach函数后,使线程处于分离态;
  • 使用pthread_detach函数后,线程在退出后,会自己清理资源
  • 相较pthread_join,使用pthread_detach函数不会阻塞主线程,但是无法获取线程的返回值。
  • pthread_detach使用时,依然需要配合sleep函数或者while(1);,否则无法保证子线程先于主线程执行完,且不能再pthread_join回收该线程

示例:线程分离后,不能使用pthread_join回收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

void *tfn(void *arg)
{
int n = 3;

while (n--) {
printf("thread count %d\n", n);
sleep(1);
}
pthread_exit((void *)1);
}

int main(void)
{
pthread_t tid;
void *tret;
int err;

#if 1

pthread_attr_t attr; /*通过线程属性来设置游离态*/
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, tfn, NULL);

#else

pthread_create(&tid, NULL, tfn, NULL);
pthread_detach(tid); //让线程分离 ----自动退出,无系统残留资源

#endif

while (1) {
err = pthread_join(tid, &tret);
printf("-------------err= %d\n", err);
if (err != 0)
fprintf(stderr, "thread_join error: %s\n", strerror(err));
else
fprintf(stderr, "thread exit code %d\n", (int)tret);

sleep(1);
}

return 0;
}

image-20220609155347698

线程属性

linux下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。

pthead_attr_t结构体

1
2
3
4
5
6
7
8
9
10
11
typedef struct {
int etachstate; //线程的分离状态
int schedpolicy; //线程调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
} pthread_attr_t;

主要结构体成员:

  1. 线程分离状态

  2. 线程栈大小(默认平均分配)

  3. 线程栈警戒缓冲区大小(位于栈末尾) 参 APUE.12.3 线程属性

属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。

线程属性主要包括如下属性:

  • 作用域(scope)
  • 栈尺寸(stack size)
  • 栈地址(stack address)
  • 优先级(priority)
  • 分离的状态(detached state)
  • 调度策略和参数(scheduling policy and parameters)

默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。

相关操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 线程属性初始化
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

// 线程的分离状态
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);

// 线程的栈地址
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize);

// 线程的栈大小
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);

线程属性初始化

注意:应先初始化线程属性,再pthread_create创建线程

初始化线程属性:

int pthread_attr_init(pthread_attr_t *attr); 成功:0;失败:错误号

销毁线程属性所占用的资源:

int pthread_attr_destroy(pthread_attr_t *attr); 成功:0;失败:错误号

线程的分离状态

线程的分离状态决定一个线程以什么样的方式来终止自己。

非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。

分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。

线程分离状态的函数:

设置线程属性,分离or非分离:

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

获取程属性,分离or非分离:

int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);

成功:0;失败:错误号

参数:

  • attr:已初始化的线程属性
  • detachstate
    • PTHREAD_CREATE_DETACHED(分离线程)
    • PTHREAD_CREATE_JOINABLE(非分离线程)

注意:如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程的启动历程函数里调用pthread_cond_timedwait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

线程的栈地址

POSIX.1定义了两个常量 _POSIX_THREAD_ATTR_STACKADDR_POSIX_THREAD_ATTR_STACKSIZE检测系统是否支持栈属性。也可以给sysconf函数传递_SC_THREAD_ATTR_STACKADDR_SC_THREAD_ATTR_STACKSIZE来进行检测。

当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstackpthread_attr_getstack两个函数分别设置和获取线程的栈地址。

int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize); 成功:0;失败:错误号

int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize); 成功:0;失败:错误号

参数:

  • attr:指向一个线程属性的指针
  • stackaddr:返回获取的栈地址
  • stacksize:返回获取的栈大小

线程的栈大小

当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用,当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。

函数pthread_attr_getstacksizepthread_attr_setstacksize提供设置。

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); 成功:0;失败:错误号

int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize); 成功:0;失败:错误号

参数:

  • attr:指向一个线程属性的指针
  • stacksize:返回线程的堆栈大小

代码示例

分离线程示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

void *tfn(void *arg)
{
int n = 3;
while(n--)
{
printf("thread count %d\n", n);
sleep(1);
}
pthread_exit((void*)1);
}

int main()
{
pthread_t tid;
void *tret;
int err;

pthread_attr_t attr; // 通过线程属性来设置游离态
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, tfn, NULL);

while(1)
{
err = pthread_join(tid, &tret);
printf("-----err = %d\n", err);
if(err != 0)
fprintf(stderr, "thread_join error: %s\n", strerror(err));
else
fprintf(stderr, "thread exit code %d\n", (int)tret);
sleep(1);
}
return 0;
}

image-20220609191236028

线程数量示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// max_pthread.c
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>


void *tfn(void *arg)
{
while (1)
sleep(1);
}

int main(void)
{
pthread_t tid;
int ret, count = 1;

for (;;) {
ret = pthread_create(&tid, NULL, tfn, NULL);
if (ret != 0) {
printf("%s\n", strerror(ret));
break;
}
printf("---------%d\n", ++count);
}

return 0;
}

image-20220609192440859

线程栈示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#define SIZE 0x10000000

void *th_fun(void *arg)
{
while (1)
sleep(1);
}

int main(void)
{
pthread_t tid;
int err, detachstate, i = 1;
pthread_attr_t attr;
size_t stacksize; //typedef size_t unsigned int
void *stackaddr;

pthread_attr_init(&attr);
pthread_attr_getstack(&attr, &stackaddr, &stacksize);
pthread_attr_getdetachstate(&attr, &detachstate);

if (detachstate == PTHREAD_CREATE_DETACHED) //默认是分离态
printf("thread detached\n");
else if (detachstate == PTHREAD_CREATE_JOINABLE) //默认时非分离
printf("thread join\n");
else
printf("thread un known\n");

/* 设置线程分离属性 */
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

while (1) {
/* 在堆上申请内存,指定线程栈的起始地址和大小 */
stackaddr = malloc(SIZE);
if (stackaddr == NULL) {
perror("malloc");
exit(1);
}
stacksize = SIZE;
pthread_attr_setstack(&attr, stackaddr, stacksize); //借助线程的属性,修改线程栈空间大小

err = pthread_create(&tid, &attr, th_fun, NULL);
if (err != 0) {
printf("%s\n", strerror(err));
exit(1);
}
printf("%d\n", i++);
}

pthread_attr_destroy(&attr);

return 0;
}

线程使用注意事项

  1. 主线程退出其他线程不退出,主线程应调用pthread_exit

  2. 避免僵尸线程

    1. pthread_join — 回收子线程
    2. pthread_detach — 分离线程
    3. pthread_create — 创建时设置分离属性

被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;

  1. mallocmmap申请的内存可以被其他线程释放

  2. 应避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit

  3. 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制

同步

所谓同步,即同时起步,协调一致。不同的对象,对“同步”的理解方式略有不同。如,设备同步,是指在两个设备之间规定一个共同的时间参考;数据库同步,是指让两个或多个数据库内容保持一致,或者按需要部分保持一致;文件同步,是指让两个或多个文件夹里的文件保持一致。等等

而,编程中、通信中所说的同步与生活中大家印象中的同步概念略有差异。“同”字应是指协同、协助、互相配合。主旨在协同步调,按预定的先后次序运行

线程同步

同步即协同步调,按预定的先后次序运行。

线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。

举例1:银行存款 5000。柜台,折:取3000;提款机,卡:取 3000。剩余:2000

举例2:内存中100字节,线程T1欲填入全1, 线程T2欲填入全0。但如果T1执行了50个字节失去cpu,T2执行,会将T1写过的内容覆盖。当T1再次获得cpu继续 从失去cpu的位置向后写入1,当执行结束,内存中的100字节,既不是全1,也不是全0。

产生的现象叫做与时间有关的错误(time related)。为了避免这种数据混乱,线程需要同步。

“同步”的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。

因此,所有“多个控制流,共同操作一个共享资源”的情况,都需要同步。

数据混乱原因

  1. 资源共享(独享资源则不会)

  2. 调度随机(意味着数据访问会出现竞争)

  3. 线程间缺乏必要的同步机制。

以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。

所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。

任务关系

Linux系统中多任务(进程/线程)之间的关系

  • 独立:仅竞争CPU资源
  • 互斥:竞争除CPU外的其他资源
  • 同步:协调彼此运行的步调,保证协同运行的各个任务具有正确的执行次序
  • 通信:数据共享,彼此间传递数据或信息,以协同完成某项工作

线程数据共享

image-20220409153932168

任务互斥问题

任务互斥—资源共享关系(间接相互制约关系)

  • 任务本身之间不存在直接联系。一个任务正在使用某个系统资源,另外一个想用该资源的任务就必须等待,而不能同时使用

全局变量存储在进程数据段中,被线程所共享。线程对全局变量的访问,要经历三个步骤

  1. 将内存单元中的数据读入寄存器

  2. 对寄存器中的值进行运算

  3. 将寄存器中的值写回内存单元

image-20220409154622468

解决方法:互斥量

任务同步问题

任务同步—相互合作关系(直接相互制约关系)

  • 两个或多个任务为了合作完成同一个工作,在执行速度或某个确定的时序点上必须相互协调,即一个任务的执行必须依赖另一个任务的执行情况

image-20220409160816138

程序设计中存在这样的情况:多个线程都要访问临界资源又要相互合作(线程间同时存在互斥关系和同步关系

线程A先执行某操作(例如对全局变量x的修改)后,线程B才能(根据变量x的值判断)执行另一操作
(可能是对全局变量x的修改),该如何实现?

  • Linux提供了条件变量机制:条件变量与互斥量一起使用时,允许线程以互斥的方式阻塞等待特定条件的发生(同步)

临界资源:在一段时间内只允许一个任务(线程或进程)访问的资源。诸任务间应采取互斥方式,实现对资源的共享

  • 共享变量,打印机等属于临界资源
  • 访问临界资源的那段代码被称为临界区

线程同步

互斥量mutex

互斥量确保同一时间里只有一个线程访问临界资源或进入临界区

互斥量(mutex)本质上是一把锁

  • 在访问临界资源前,对互斥量进行加锁
  • 在访问完成后对互斥量解锁
  • 对互斥量加锁后,任何其他试图对互斥量加锁的线程将会被阻塞,直到互斥量被解锁为止

image-20220609230838624

当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。

所以,互斥锁实质上是操作系统提供的一把建议锁(又称协同锁),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定

因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱

互斥量的操作

头文件#include <pthread.h>

  1. 定义互斥量变量: pthread_mutex_t mutex;
  2. 初始化互斥量变量:pthread_mutex_init函数
  3. 对互斥量加锁:
    • pthread_mutex_lock函数(阻塞)
    • pthread_mutex_trylock函数(非阻塞)
    • pthread_mutex_timedlock函数(限时加锁)
  4. 对互斥量解锁:pthread_mutex_unlock函数
  5. 销毁互斥量变量:pthread_mutex_destroy函数

以上5个函数的返回值都是:成功返回0,出错返回错误码

pthread_mutex_t类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。只有两种取值1、0。

函数原型

1)pthread_mutex_init

作用:初始化互斥量变量

静态初始化

  • pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER;
  • 如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化

动态初始化

  • int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
    • mutex:指向互斥量的指针
    • attr:设置互斥量的属性,通常可采用默认属性(线程共享),传入空指针(NULL)。

restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改

2)pthread_mutex_destroy

作用:销毁互斥量

互斥量在使用完毕后,必须要对互斥量进行销毁,以释放资源

int pthread_mutex_destroy(pthread_mutex_t *mutex);

  • mutex:即互斥量

3)pthread_mutex_lock

在对临界资源访问之前,需要对互斥量进行加锁操作

int pthread_mutex_lock(pthread_mutex_t *mutex);

当调用pthread_mutex_lock时,若互斥量已被加锁,则调用线程将被阻塞直到可以完成加锁操作为止。

4)pthread_mutex_trylock

尝试加锁,非阻塞

int pthread_mutex_trylock(pthread_mutex_t *mutex);

  • 调用该函数时,若互斥量未加锁,则对该互斥量加锁,返回0;
  • 若互斥量已加锁,则函数直接返回错误码EBUSY(不会阻塞调用线程)

5)pthread_mutex_unlock

互斥量的解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。

6)pthread_mutex_timedlock

限时加锁

1
2
3
4
5
6
7
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);

struct timespec { //头文件 time.h
time_t tv_sec; /* seconds */
long tv_nsec; /* nanosecondes*/ 纳秒
}

man sem_timedwait可以查看struct timespec结构体

形参abstime:绝对时间。

如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。

错误用法:

1
2
3
struct timespec t = {1, 0};
// 只能定时到 1970年1月1日 00:00:01秒(早已经过去)
pthread_cond_timedwait (&cond, &mutex, &t);

正确用法:

1
2
3
4
time_t cur = time(NULL);  // 获取当前时间
struct timespec t; // 定义timespec 结构体变量t
t.tv_sec = cur+1; // 定时1秒
pthread_cond_timedwait (&cond, &mutex, &t);

代码示例

两个线程共享stdout资源,分别打印hello worldHELLO WORLD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

pthread_mutex_t mutex;

void *tfn(void *arg)
{
srand(time(NULL));

while(1) {
pthread_mutex_lock(&mutex);
printf("hello ");
sleep(rand() % 3); // 模拟长时间操作共享资源,导致CPU易主产生与时间有关函数
printf("world\n");
pthread_mutex_unlock(&mutex); // 共享stdout资源 使用完立即解锁 不要睡眠后再解锁
sleep(rand() % 3);
}

return NULL;
}

int main()
{
pthread_t tid;
srand(time(NULL));

pthread_mutex_init(&mutex, NULL);

pthread_create(&tid, NULL, tfn, NULL);
int flag = 5;
while(flag--) {
pthread_mutex_lock(&mutex);
printf("HELLO ");
sleep(rand() % 3);
printf("WORLD\n");
pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}
pthread_cancel(tid);
pthread_join(tid, NULL);
int ret = pthread_mutex_destroy(&mutex); //销毁锁
if(ret != 0) {
printf("pthread_mutex_destroy: %s\n", strerror(ret));
}

pthread_exit(NULL);
}

image-20220610112336201

小结在访问共享资源前加锁,访问结束后立即解锁。锁的“粒度”应越小越好。

死锁

死锁场景

1. 线程试图对同一个互斥量A加锁两次。
1. 线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁
1. 震荡(哲学家进餐问题,同时拿起筷子,同时释放筷子,同时再拿起筷子...)

避免方法:

  • 保证资源的获取顺序,要求每个线程获取资源的顺序一致
  • 当得不到所有所需资源时,放弃已经获得的资源(不同时),等待

读写锁rwlock

与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享。

读写锁引入

问题描述:

  • 在对临界资源的访问中,更多的是读操作,而写操作较少,只有互斥量机制可能会影响访问效率
  • 期望对临界资源的访问控制粒度更精细,任一时刻允许多个线程对临界资源进行读操作,但只允许一个线程对临界资源进行写操作

互斥关系:

  • 读操作-写操作互斥
  • 写操作-写操作互斥
  • 读操作-读操作不互斥

同步关系

  • 缓冲区不满,才允许写操作
  • 缓冲区不空,才允许读操作

读写锁通信机制

在保证互斥的基础上,Linux提供了对临界资源访问控制粒度更细的读写锁机制

  1. 读写锁是写模式加锁(写锁)时, 解锁前,所有对该锁加锁的线程都会被阻塞。

  2. 读写锁是读模式加锁(读锁)时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。

  3. 读写锁是读模式加锁时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高

读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享

读写锁非常适合于对数据结构读的次数远大于写的情况。

读写锁的操作

读写锁的操作与互斥量的操作非常类似

头文件:#include <pthread.h>

  1. 定义读写锁变量:pthread_rwlock_t rwlock;
  2. 初始化读写锁变量:pthread_rwlock_init函数
  3. 访问临界资源(读操作或写操作)前对读写锁加锁
    • 阻塞申请读锁:pthread_rwlock_rdlock函数
    • 阻塞申请写锁:pthread_rwlock_wrlock函数
    • 非阻塞申请读锁:pthread_rwlock_tryrdlock函数
    • 非阻塞申请写锁:pthread_rwlock_trywrlock函数
    • 限时申请读锁:pthread_rwlock_timerdlock函数
    • 限时申请写锁:pthread_rwlock_timewrlock函数
  4. 解锁(包括读锁和写锁):pthread_rwlock_unlock函数
  5. 销毁读写锁变量:pthread_rwlock_destroy函数

以上函数成功返回0,失败返回错误码

函数原型

1)读写锁初始化

静态初始化:pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER

动态初始化:

1
2
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
  • attr表读写锁属性,通常使用默认属性,传NULL即可。

2)销毁读写锁变量

1
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

3)请求读锁

1
2
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 阻塞
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); // 非阻塞

4)请求写锁

1
2
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // 阻塞
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); // 非阻塞

5)限时加锁

1
2
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abstime); // 限时加读锁
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abstime); // 限时加写锁
  • abstime的使用参考前面互斥量的pthread_mutex_timedlock函数

6)读写锁解锁

1
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
  • 就近原则。加锁(读锁/写锁)与解锁配对出现,为代码中距离最近的加锁操作解锁

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/* 3个线程不定时 "写" 全局资源,5个线程不定时 "读" 同一全局资源 */
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

int counter; // 全局资源
pthread_rwlock_t rwlock;

void *th_write(void *arg)
{
int t;
int i = (int)arg;

while(1) {
usleep(1000);
pthread_rwlock_wrlock(&rwlock);
t = counter;
printf("=========== write %d : %lu : counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);
pthread_rwlock_unlock(&rwlock);
usleep(5000);
}
return NULL;
}

void *th_read(void* arg)
{
int i = (int)arg;

while(1) {
pthread_rwlock_rdlock(&rwlock);
printf("-------------read %d : %lu : %d\n", i, pthread_self(), counter);
pthread_rwlock_unlock(&rwlock);
usleep(900);
}
return NULL;
}

int main()
{
int i;
pthread_t tid[8];
pthread_rwlock_init(&rwlock, NULL);

for(i = 0; i < 3; ++i)
pthread_create(&tid[i], NULL, th_write, (void*)i);

for(i = 0; i < 5; ++i)
pthread_create(&tid[i+3], NULL, th_read, (void*)i);

for(i = 0; i < 8; ++i)
pthread_join(tid[i], NULL);

pthread_rwlock_destroy(&rwlock); // 释放读写锁

return 0;
}

image-20220610143129783

条件变量cond

条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。

条件变量的操作

头文件:#include <pthread.h>

  1. 定义互斥量变量,定义条件变量pthread_cond_t cond;
  2. 初始化互斥量,初始化条件变量pthread_cond_init函数
  3. 等待条件线程x(假设先执行)
    1. 互斥量加锁 —> XX操作 —>
    2. 等待条件变量:pthread_cond_wait函数、pthread_cond_timedwait函数
    3. —> XX操作 —> 互斥量解锁
  4. 触发条件线程y(假设后执行)
    1. 触发条件变量:pthread_cond_signal函数、pthread_cond_broadcast函数
  5. 销毁互斥量变量,销毁条件变量pthread_cond_destroy

函数的返回值都是:成功返回0, 失败返回错误码。

为什么条件变量需要和互斥量配合使用

  • 条件变量的使用场景伴随共享资源的使用,例如全局变量
  • 在调用pthread_cond_wait前,需要使互斥量处于加锁状态,这样可以通过原子操作的方式,将调用线程放到该条件变量等待线程队列(临界资源)中

调用pthread_cond_wait函数后内核自动执行的操作:

  • 在线程阻塞等待条件变量之前,调用pthread_mutex_unlock
  • 若条件变量被其他线程触发,在该线程被唤醒后,调用pthread_mutex_lock

函数说明

1)条件变量初始化

静态初始化:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

动态初始化:

1
2
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
  • cond:条件变量
  • attr:条件变量属性,若为NULL,则使用默认属性

2)条件变量销毁

int pthread_cond_destroy(pthread_cond_t * cond);

3)等待条件变量

1
2
3
// 将使调用线程进入阻塞状态,直到条件被触发
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);

pthread_cond_wait函数作用:( 1.2.两步为一个原子操作

  1. 阻塞等待条件变量cond(参1)满足

  2. 释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);

  3. 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);

1
2
3
4
// 限时等待一个条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
  • abstime的使用参考前面互斥量的pthread_mutex_timedlock函数

4)触发条件变量

1
2
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

pthread_cond_signal唤醒该条件变量等待线程队列中的某一个线程

pthread_cond_broadcast唤醒该条件变量等待线程队列中的所有线程,这些线程会进行竞争

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <unistd.h>
#include <stdio.h>
#include <pthread.h>

pthread_mutex_t count_lock;
pthread_cond_t count_ready;
int count;

void *decrement_count(void *arg);

void *increment_count(void *arg);

int main()
{
pthread_t tid1, tid2;
count = 0;
pthread_mutex_init(&count_lock, NULL);
pthread_cond_init(&count_ready, NULL);

pthread_create(&tid1, NULL, decrement_count, NULL);
sleep(2);
pthread_create(&tid2, NULL, increment_count, NULL);

pthread_join(tid2, NULL);
printf("decrement quit\n");

pthread_join(tid1, NULL);
return 0;
}

void *decrement_count(void *arg)
{
pthread_mutex_lock(&count_lock); // 上锁
printf("decrement:waitting\n");
pthread_cond_wait(&count_ready, &count_lock);
/*等待条件变量,期间互斥量仍然可用*/
count -= 1;
printf("decrement:count = %d\n", count);
pthread_mutex_unlock(&count_lock); // 解锁
printf("decrement quit\n");
pthread_exit(NULL);
}

void *increment_count(void *arg)
{
pthread_mutex_lock(&count_lock); // 上锁
printf("increment:running\n");
count += 1;
pthread_cond_signal(&count_ready);
/*触发条件变量*/
printf("increment:count = %d\n", count); // 解锁
pthread_mutex_unlock(&count_lock);
pthread_exit(NULL);
}

image-20220409165937723

条件变量优点

相较于mutex而言,条件变量可以减少竞争。

如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。

生成者消费者模型

一个生成者线程,一个消费者线程

生成者生成自定义的struct msg

生产的产品以链表的结构组合,头部插入,所以后生产的先消费

代码中生成者和消费者执行完各自动作后的睡眠不能去掉。不然某个线程解锁完立马加锁,一直占据CPU

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 互斥量
pthread_cond_t product = PTHREAD_COND_INITIALIZER; // 条件变量

struct msg {
int num;
struct msg *next;
};

struct msg *head = NULL;

void* producer(void *arg)
{
struct msg *mp = NULL;
while(1) {
mp = (struct msg*)malloc(sizeof(struct msg));
mp->num = rand() % 400 + 1;
printf("----producer : %d\n", mp->num);
pthread_mutex_lock(&mutex); // 加锁
mp->next = head;
head = mp;
pthread_mutex_unlock(&mutex); // 解锁

pthread_cond_signal(&product);
sleep(rand() % 5);
}
}

void *consumer(void *arg)
{
struct msg *tmp;
while(1) {
pthread_mutex_lock(&mutex); // 上锁
if(head == NULL) {
pthread_cond_wait(&product, &mutex);
}
tmp = head;
head = head->next;
pthread_mutex_unlock(&mutex); // 解锁
printf("====consumer : %d\n", tmp->num);
free(tmp);
tmp = NULL;
sleep(rand() % 5);
}
}

int main()
{
pthread_t ptid, ctid;

srand(time(NULL)); // 随机数种子

pthread_create(&ptid, NULL, producer, NULL);
pthread_create(&ctid, NULL, consumer, NULL);

pthread_join(ptid, NULL);
pthread_join(ctid, NULL);

pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&product);

pthread_exit(NULL);
}

image-20220610170438272

进程同步

信号量sem

进化版的互斥锁(1 —> N)

由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。

信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。

信号量的操作

头文件#include <semaphore.h>

  1. 定义信号量变量:sem_t sem
  2. 初始化信号量变量:sem_init函数
  3. 对信号量加锁(P操作):
    • sem_wait函数(阻塞)
    • sem_trywait函数(非阻塞)
    • sem_timedlock函数(限时加锁)
  4. 对信号量解锁(V操作):sem_post函数
  5. 销毁信号量变量:sem_destroy函数

以上5个函数的返回值都是:成功返回0;失败返回-1,并设置errno

sem_t类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。 初始化信号量sem不能 < 0。

信号量的初值,决定了可占用信号量的线程的个数。

函数说明

1)初始化信号量

1
int sem_init(sem_t *sem, int pshared, unsigned int value);
  • sem信号量
  • pshared0用于线程间;取非0(一般为1)用于进程间
  • value指定信号量初值 >=0

2)销毁信号量

1
int sem_destroy(sem_t *sem);

3)给信号量加锁 (P操作--

1
2
3
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
  • abs_timeout绝对时间,参考前面pthread_mutex_timedlock函数介绍

4)对信号量解锁 (V操作++

1
int sem_post(sem_t *sem);

生产者消费者模型

线程实现。一个生产者,一个消费者。所以只考虑了同步。如果是多个生产者,多个消费者,需要考虑上互斥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <semaphore.h>
#include <unistd.h>
#include <time.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM 5

int queue[NUM]; // 缓冲区 环形队列
sem_t blank_number; // 空闲
sem_t product_number; // 已生产

void *producer(void *arg)
{
int i = 0;
while(1) {
sem_wait(&blank_number);
queue[i] = rand() % 1000 + 1; // 生产一个产品
printf("----produce : %d\n", queue[i]);
sem_post(&product_number);
i = (i + 1) % NUM;
sleep(rand() % 3);
}
}

void *consumer(void *arg)
{
int i = 0;
while(1) {
sem_wait(&product_number);
printf("====consume : %d\n", queue[i]);
queue[i] = 0; // 消费一个产品
sem_post(&blank_number);
i = (i + 1) % NUM;
sleep(rand() % 3);
}
}

int main()
{
srand(time(NULL));
pthread_t pid, cid;

sem_init(&blank_number, 0, NUM); //初始化空闲缓冲区为5
sem_init(&product_number, 0, 0); // 初始化已占用缓冲区为0

pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);

pthread_join(pid, NULL);
pthread_join(cid, NULL);

sem_destroy(&blank_number);
sem_destroy(&product_number);

pthread_exit(NULL);
}

image-20220610205703709

互斥量mutex

进程间也可以使用互斥锁,来达到同步的目的。但应在pthread_mutex_init初始化之前,修改其属性为进程间共享。mutex的属性修改函数主要有以下几个。

主要操作

头文件:#include <pthread.h>

  1. 用于定义mutex锁的属性pthread_mutexattr_t mattr;

  2. 初始化一个mutex属性对象:pthread_mutexattr_init函数

  3. 设置属性修改mutexpthread_mutexattr_setpshared函数
  4. 销毁mutex属性对象(而非销毁锁):pthread_mutexattr_destroy函数

返回值:成功返回0,失败返回错误码

函数原型

1
2
3
4
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);

pshared取值:

  • 线程锁:PTHREAD_PROCESS_PRIVATE(mutex的默认属性即为线程锁,进程间私有)
  • 进程锁:PTHREAD_PROCESS_SHARED

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <sys/mman.h>
#include <sys/wait.h>

struct mt {
int num;
pthread_mutex_t mutex;
pthread_mutexattr_t mutexattr;
};

int main(void)
{
int i;
struct mt *mm;
pid_t pid;

/* int fd = open("mt_test", O_CREAT | O_RDWR, 0777);
ftruncate(fd, sizeof(*mm));
mm = mmap(NULL, sizeof(*mm), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
unlink("mt_test"); */
mm = mmap(NULL, sizeof(*mm), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0); // 匿名映射
memset(mm, 0, sizeof(*mm));

pthread_mutexattr_init(&mm->mutexattr); //初始化mutex属性对象
pthread_mutexattr_setpshared(&mm->mutexattr, PTHREAD_PROCESS_SHARED); //修改属性为进程间共享
pthread_mutex_init(&mm->mutex, &mm->mutexattr); //初始化一把mutex琐

pid = fork();
if (pid == 0) {
for (i = 0; i < 10; i++) {
pthread_mutex_lock(&mm->mutex);
(mm->num)++;
printf("-child----num++ %d\n", mm->num);
pthread_mutex_unlock(&mm->mutex);
sleep(1);
}
} else if (pid > 0) {
for ( i = 0; i < 10; i++) {
sleep(1);
pthread_mutex_lock(&mm->mutex);
mm->num += 2;
printf("-parent---num+=2 %d\n", mm->num);
pthread_mutex_unlock(&mm->mutex);
}
wait(NULL);
}

pthread_mutexattr_destroy(&mm->mutexattr); //销毁mutex属性对象
pthread_mutex_destroy(&mm->mutex); //销毁mutex
munmap(mm,sizeof(*mm)); //释放映射区
return 0;
}

image-20220610212537547

条件变量cond

条件变量一般和互斥量配套使用。既然互斥量可以用于进程间,条件变量自然也可以。

头文件:#include <pthread.h>

  1. 用于定义条件变量cond的属性pthread_condattr_t mattr;

  2. 初始化一个cond属性对象:pthread_condattr_init函数

  3. 设置属性修改cond:pthread_condattr_setpshared函数
  4. 销毁cond属性对象(而非销毁条件变量):pthread_condattr_destroy函数

返回值:成功返回0,失败返回错误码

函数原型

1
2
3
4
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
int pthread_condattr_getpshared(const pthread_condattr_t *attr, int *pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);

pshared取值:

  • 线程:PTHREAD_PROCESS_PRIVATE(默认属性)
  • 进程:PTHREAD_PROCESS_SHARED

读写锁rwlock

读写锁同样可以用于进程间互斥,需要设置属性

头文件:#include <pthread.h>

  1. 用于定义读写锁rwlock的属性pthread_rwlockattr_t mattr;

  2. 初始化一个rwlock属性对象:pthread_rwlockattr_init函数

  3. 设置属性修改rwlock:pthread_rwlockattr_setpshared函数
  4. 销毁rwlock属性对象(而非销毁读写锁):pthread_rwlockattr_destroy函数

返回值:成功返回0,失败返回错误码

函数原型

1
2
3
4
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);

pshared取值:

  • 线程:PTHREAD_PROCESS_PRIVATE(默认属性)
  • 进程:PTHREAD_PROCESS_SHARED

文件锁fcntl

借助 fcntl 函数来实现锁机制。操作文件的进程没有获得锁时,可以打开,但无法执行readwrite操作。

1
2
3
4
5
6
7
8
9
10
11
int fcntl(int fd, int cmd, struct flock*);

struct flock {
...
short l_type; // 锁的类型:F_RDLCK 、F_WRLCK 、F_UNLCK
short l_whence; // 偏移位置:SEEK_SET、SEEK_CUR、SEEK_END
off_t l_start; // 起始偏移,开始上锁位置
off_t l_len; // 长度,对多长的数据上锁:0表示整个文件加锁
pid_t l_pid; // 持有该锁的进程ID:(F_GETLK only)
...
};
  • cmd
    • F_SETLK 设置文件锁(trylock)
    • F_SETLKW 设置文件锁(lock),解锁(unlock)
    • F_GETLK 获取文件锁

注意:

1)依然遵循读共享、写独占特性。但!如若进程不加锁直接操作文件,依然可访问成功,但数据势必会出现混乱。

2)多线程间共享文件描述符,而给文件加锁,是通过修改文件描述符所指向的文件结构体中的成员变量来实现的。因此,多线程中无法使用文件锁。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

void sys_err(char *str)
{
perror(str);
exit(-1);
}

int main(int argc, char* argv[])
{
int fd;
struct flock f_lock;

if(argc < 2) {
printf("./a.out filename\n");
exit(-1);
}

if((fd = open(argv[1], O_RDWR)) < 0)
sys_err("open");

f_lock.l_type = F_WRLCK; // 选用写锁
//f_lock.l_type = F_RDLCK; //选用读锁

f_lock.l_whence = SEEK_SET;
f_lock.l_start = 0;
f_lock.l_len = 0; // 0 表示整个文件加锁

fcntl(fd, F_SETLKW, &f_lock);
printf("get flock\n");
sleep(10);

f_lock.l_type = F_UNLCK;
fcntl(fd, F_SETLKW, &f_lock);
printf("un flock\n");

close(fd);
return 0;
}

总结

进程和线程共有:

互斥量mutex、读写锁rwlock、条件变量cond、信号量sem、

进程独有:文件锁

欢迎关注我的其它发布渠道