多线程与线程同步

视频教程:爱编程的大丙

视频文档地址:爱编程的大丙

1.线程与进程区别

  • 进程由独立的地址空间,多个线程共用同一个地址空间

    • 在一个地址空间中多个线程独享:每个线程都有自己的栈区。寄存器
    • 在一个地址空间中多个线程共享:代码段、堆区、全局数据区,打开的文件都是线程共享的
  • 线程是程序执行的最小单位,进程是操作系统中最小的资源分配单位

    • 每个进程对应一个虚拟地址空间,一个进程只能抢一个CPU时间片
    • 一个地址空间中可以划分出多个线程,在有效的资源基础上,可以抢更多的CPU时间片
  • CPU的调度与切换:线程的上下文切换比进程快

    • 上下文切换: 进程\线程分时复用CPU时间片,在切换之前会将上一个任务的状态进行保存(状态存储在寄存器),下次切换回这个任务的时候(此时再次获得CPU资源-时间片),加载这个状态继续运行,任务从保存到再次加载这个过程就是一次上下文切换
    • 线程更为廉价,启动/退出速度更快,对系统资源冲击小

2.创建线程

2.1.线程函数

每一个线程都有一个唯一的线程IDID类型为pthread_t,这个ID是一个无符号长整型数,通过以下函数获取当前线程ID

1
pthread_t pthread_self(void);//返回当前线程ID

在一个进程中调用线程创建函数,就可以得到一个子线程,和进程不同,需要给每一个线程指定一个处理函数否则线程无法工作.创建线程(实际上就是确定调用该线程函数的入口点)

1
2
3
4
#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,线程库名字为pthread
//LINUX中编译时,需要链接动态库pthread C文件名 - lpthread -o 可执行文件名
  • 参数:

    pthread :传出参数,无符号长整型,线程创建成功,会将线程ID写入到这个指针指向的内存中

    attr : 线程的属性,一般情况写入默认值即可,NULL

    start_routine :函数指针,用于回调,指向的函数,就是在线程中需要执行的任务。即子线程运行函数。创建出子线程处理的动作

    ​ 该函数指针指向函数的参数为void *,因此可以将任意类型的参数传递给子线程执行的函数

    ​ 回调函数的类型应该为void *类型

    arg :子线程运行函数的参数。作为实参传递到start_routine指针指向的函数内部

    返回值:0表示成功 -1表示失败

2.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
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>

/*
程序执行逻辑:
1.main函数
2.主线程中创建子线程
3.执行主线程
4.执行子线程
*/

//子线程执行任务的回调函数
void* callback(void *arg)
{
for (int i = 0; i < 5; i++)
{
printf("子线程:i = %d\n",i);

}
printf("子线程:%ld\n",pthread_self());
//pthread_self()返回当前线程ID---长整型
return NULL;
}

int main(void)
{
pthread_t tid; //子线程的线程ID
pthread_create(&tid,NULL,callback,NULL);
for (int i = 0;i<5;i++)
{
printf("主线程:i = %d\n",i);
}
printf("主线程:%ld\n",pthread_self());
return 0;
}

Linux中编译,需要连接pthread动态库:

1
2
3
gcc pthrad_create.c -lpthread -o app
执行:
./app

运行结果:

image-20231214220441534

结果分析:

1
2
3
可以发现此时并没有执行子线程:
子线程被创建之后,运行需要抢CPU时间片,在子线程没有抢到CPU时间片之前,主线程以及执行完毕----将地址空间释放
此时子线程就无法在地址空间执行

解决方案:

  • #include <usistd.h>
    //main函数中return 0前添加
    sleep(3)函数;    //让主线程等一会--主线程放弃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

    * ```C
    //修改代码:
    #include<stdio.h>
    #include<stdlib.h>
    #include<pthread.h>
    #include <unistd.h>

    /*
    程序执行逻辑:
    1.main函数
    2.主线程中创建子线程
    3.执行主线程
    4.执行子线程
    */

    //子线程执行任务的回调函数
    void* callback(void *arg)
    {
    for (int i = 0; i < 5; i++)
    {
    printf("子线程:i = %d\n",i);
    }
    printf("子线程:%ld\n",pthread_self());
    //pthread_self()返回当前线程ID---长整型
    return NULL;
    }

    int main(void)
    {
    pthread_t tid; //子线程的线程ID
    pthread_create(&tid,NULL,callback,NULL);
    for (int i = 0;i<5;i++)
    {
    printf("主线程:i = %d\n",i);
    }
    printf("主线程:%ld\n",pthread_self());
    sleep(3); //主线程休眠3s
    // 退出进程
    exit(0);
    }
  • 方案一执行结果:

image-20231214220644673

3.线程退出

线程退出函数,主要使用在主线程里。若想要线程退出,但是不会导致虚拟地址空间的释放(针对主线程),就可以调用线程库中的线程退出函数
调用该函数,当前线程就会立马退出,不会影响其他线程的执行,在子线程或者主线程都可以使用

1
2
#include <pthread.h>
void pthread_exit(void *retval);
  • 参数retval

    线程退出时携带的数据,当前子线程的主线程会得到该数据,若不需要,指定为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
    #include<stdio.h>
    #include<stdlib.h>
    #include<pthread.h>
    #include<unistd.h>

    //子线程执行任务的回调函数
    void* callback(void *arg)
    {
    for (int i = 0; i < 5; i++)
    {
    printf("子线程:i = %d\n",i);
    }
    printf("子线程:%ld\n",pthread_self());
    //pthread_self()返回当前线程ID---长整型
    return NULL;
    }

    int main(void)
    {
    pthread_t tid; //子线程的线程ID
    pthread_create(&tid,NULL,callback,NULL);
    printf("子线程创建成功,线程ID:%ld\n",tid);
    printf("主线程ID:%ld\n",ptherad_self());
    for (int i = 0;i<5;i++)
    {
    printf("主线程:i = %d\n",i);
    }

    pthread_exit(NULL); //线程退出函数,退出主线程
    return 0;
    }

运行结果,主线程退出后,子线程仍然可以继续运行,每次执行的顺序会不一样这取决于CPU系统的调度

image-20231214221309944

4.线程回收

4.1线程函数

线程与进程相似,子线程退出时其内核资源主要由主线程回收,线程库中提供的线程回收函数:phread_join(),该函数是一个阻塞函数

(若子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源回收,函数被调用一次,只能回收一个子线程,如果多个子线程则需要进行循环回收)

  • 通过线程回收函数还可以获得子线程退出时传递出来的数据

  • #include <pthread.h>
    int pthread_join(pthread_t thread,void **retval)
        //这是一个阻塞函数,子线程在运行该函数就会阻塞
        //子线程退出,函数解除阻塞,回收对应子线程资源
    
    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

    * **参数:**

    * thread : 要被回收的子线程的线程ID
    * retval : 二级指针,指向一级指针的地址,是一个传出参数,这个地址中存储了`pthread_exit()`传递出的数据,若不需要该参数可以指定为NULL

    * **返回值:**

    * 线程回收成功返回0,回收失败返回错误号



    #### 4.2回收子线程数据

    ##### 4.2.1定义全局/静态变量

    ```c
    #include<stdio.h>
    #include<stdlib.h>
    #include<pthread.h>
    #include <unistd.h>

    struct Test
    {
    int num;
    int age;
    };

    //子线程执行任务的回调函数
    void* callback(void *arg)
    {
    for (int i = 0; i < 5; i++)
    {
    printf("子线程:i = %d\n",i);
    }
    printf("子线程:%ld\n",pthread_self());

    struct Test t;
    t.num = 100;
    t.age = 6;
    //线程退出
    pthread_exit(&t);//将结构体地址传给pthread_exit中的指针参数
    //将该数据通过线程退出函数传递出去
    }

    int main(void)
    {
    pthread_t tid; //子线程的线程ID
    pthread_create(&tid,NULL,callback,NULL);
    printf("主线程:%ld\n",pthread_self());

    //定义一个一级指针,用于指向线程tid传递出的数据
    void *ptr;
    //pthread_join()参数为一个二级指针,用于指向,上面的一级指针,二级指针变量存储的是一级指针的地址
    pthread_join(tid,&ptr);
    //此时一级指针ptr指向子线程退出时,传回数据的地址
    struct Test *pt = (struct Test *) ptr;
    printf("子线程age:%d\n",pt->age);
    printf("子线程num:%d\n", pt->num);
    exit(0);
    }

Linux中运行结果:

1
2
ggc pt.c -lpthread -o app
./app

image-20231214222320116

结果分析:

1
2
此时的结果,在主线程中打印,子线程退出时,传回的数据,并没有我们预期的结果
ptr指针,指向的是,子线程传出的数据地址,该地址是子线程的一块(局部变量)栈内存,该块栈内存,被多个线程平均分成若干份占用,但是一个子线程退出,就会将该块内存退还,那么此时内存里的资源也会被释放。

代码修改:

1
2
3
4
//将回调函数中:
struct Test t;
//修改为:
static struct Test t;//静态变量存放在数据段,其生命周期为程序的执行周期 或者使得struct Test t为全局变量

运行结果:

image-20231214222423727

4.2.2使用子线程栈

通过函数 pthread_exit(); 可以得知,子线程退出的时候,需要将数据记录到一块内存中,通过参数传出的是存储数据的内存的地址,而不是具体数据,由因为参数是 void* 类型,所有这个万能指针可以指向任意类型的内存地址。先来看第一种方式,将子线程退出数据保存在子线程自己的栈区:

实例代码:

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
// pthread_join.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 定义结构
struct Persion
{
int id;
char name[36];
int age;
};

// 子线程的处理代码
void* working(void* arg)
{
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf("child == i: = %d\n", i);
if(i == 6)
{
struct Persion p;
p.age =12;
strcpy(p.name, "tom");
p.id = 100;
// 该函数的参数将这个地址传递给了主线程的pthread_join()
pthread_exit(&p);
}
}
return NULL; // 代码执行不到这个位置就退出了
}

int main()
{
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);

printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("i = %d\n", i);
}

// 阻塞等待子线程退出
void* ptr = NULL;
// ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
// 这个内存地址就是pthread_exit() 参数指向的内存
pthread_join(tid, &ptr);
// 打印信息
struct Persion* pp = (struct Persion*)ptr;
printf("子线程返回数据: name: %s, age: %d, id: %d\n", pp->name, pp->age, pp->id);
printf("子线程资源被成功回收...\n");

return 0;
}

编译并且执行测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 编译代码
$ gcc pthread_join.c -lpthread
# 执行程序
$ ./a.out
子线程创建成功, 线程ID: 140652794640128
我是主线程, 线程ID: 140652803008256
i = 0
i = 1
i = 2
我是子线程, 线程ID: 140652794640128
child == i: = 0
child == i: = 1
child == i: = 2
child == i: = 3
child == i: = 4
child == i: = 5
child == i: = 6
子线程返回数据: name: , age: 0, id: 0
子线程资源被成功回收...

结果分析:

1
2
3
4
5
通过打印的日志可以发现,在主线程中没有没有得到子线程返回的数据信息,具体原因是这样的:

如果多个线程共用同一个虚拟地址空间,每个线程在栈区都有一块属于自己的内存,相当于栈区被这几个线程平分了,当线程退出,线程在栈区的内存也就被回收了,因此随着子线程的退出,写入到栈区的数据也就被释放了。

因此在主线程中无法成功的将子线程中栈区的数据成功打印出来
4.2.3使用主线程栈

虽然每个线程都有属于自己的栈区空间,但是位于同一个地址空间的多个线程是可以相互访问对方的栈空间上的数据的。由于很多情况下还需要在主线程中回收子线程资源,所以主线程一般都是最后退出,基于这个原因在下面的程序中将子线程返回的数据保存到了主线程的栈区内存中:

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 定义结构
struct Persion
{
int id;
char name[36];
int age;
};


// 子线程的处理代码---指针函数---回调函数
void* working(void* arg)
{
struct Persion* p = (struct Persion*)arg;
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf("child == i: = %d\n", i);
if(i == 6)
{
// 使用主线程的栈内存
p->age =12;
strcpy(p->name, "tom");
p->id = 100;
// 该函数的参数将这个地址传递给了主线程的pthread_join()
pthread_exit(p);
}
}
return NULL;
}

int main()
{
// 1. 创建一个子线程
pthread_t tid;

struct Persion p;
// 主线程的栈内存传递给子线程
pthread_create(&tid, NULL, working, &p);

printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("i = %d\n", i);
}

// 阻塞等待子线程退出
void* ptr = NULL;
// ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
// 这个内存地址就是pthread_exit() 参数指向的内存
pthread_join(tid, &ptr);
// 打印信息
printf("name: %s, age: %d, id: %d\n", p.name, p.age, p.id);
printf("子线程资源被成功回收...\n");

return 0;
}

运行结果,发现子线程中对主线程栈区更新的数据成功打印出来了。

因为即使子线程退出了,但是并不影响主线程栈区的数据

image-20231214223148660

5.线程分离

在某些情况下,程序中的主线程有属于自己的业务处理流程,若让主线程负责子线程的资源回收,调用pthread_join() 只要子线程不退出主线程就会一直阻塞,主线程的任务就不能被执行。

线程分离函数:pthread_detach()函数,调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并且回收(为主线程减负)。线程分离之后在主线程中使用pthread_join()就无法回收到子线程的资源

线程分离函数:

1
2
3
#include <pthread.h>
// 参数就子线程的线程ID, 主线程就可以和这个子线程分离了
int pthread_detach(pthread_t thread);

实例代码:

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
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include <unistd.h>

struct Test
{
int num;
int age;
};

//子线程执行任务的回调函数
void* callback(void *arg)
{
for (int i = 0; i < 5; i++)
{
printf("子线程:i = %d\n",i);
}
printf("子线程:%ld\n",pthread_self());


return NULL;
}

int main(void)
{
pthread_t tid; //子线程的线程ID
pthread_create(&tid,NULL,callback,NULL);
printf("主线程:%ld\n",pthread_self());

//子线程分离---在回调函数中执行子线程退出后,会有其他系统线程来负责回收子线程资源(不用主线程负责)
pthread_detach(tid);
//主线程退出
pthread_exit(NULL);

return 0;
}

编译运行结果

虽然子线程进行分离之后,不要在主线程进行子线程的资源回收,但是当子线程还在运行的时候,主线程提前exit退出了,内存地址空间都不存在了,子线程也不会存在。

image-20231215215651154

6.其他线程函数

6.1线程取消

其含义是:在某些特定情况下在一个线程中杀死另外一个线程,使用这个函数杀死一个线程分两步:

  • 在线程 A 中调用线程取消函数 pthread_cancel,指定杀死线程 B,这时候线程 B 是死不了的
  • 线程 B 中进行一次系统调用(从用户区切换到内核区),否则线程 B 可以一直运行。
1
2
3
#include <pthread.h>
// 参数是子线程的线程ID
int pthread_cancel(pthread_t thread);
  • 参数:要杀死的线程的线程 ID

  • 返回值:函数调用成功返回 0,调用失败返回非 0 错误号。

实例代码:

示例代码中,主线程调用线程取消函数,只要在子线程中进行了系统调用,当子线程执行到这个位置就挂掉了。

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
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

//子线程处理函数--回调函数--指针函数(因为pthread_create()中回调函数的参数,是void *类型)
void *callback(void *arg)
{
int j = 0;
for (int i = 0;i<9;i++)
{
j++;
}
//printf()函数会调用系统函数,因此该函数会间接的系统调用(满足线程取消的情况)
printf("子线程ID:%ld\n",pthread_self());
for (int i = 0; i < 9; i++)
{
printf("child i:%ld\n",i);
}
return NULL;
}

int main()
{
//1.创建子线程
pthread_t tid;//定义子线程ID--长整形
pthread_create(&tid, NULL, callback, NULL);

printf("子线程创建成功,线程ID:%ld\n",tid);
//2.主线程执行下边代码
printf("我是主线程,线程ID:%ld",pthread_self());
for (int i = 0; i < 3; i++)
{
printf("i = %d\n", i);
}

//杀死子线程---若子线程中出现系统调用,子线程就结束
pthread_cancel(tid);

//主线程退出
pthread_exit(NULL);

return 0;
}

运行结果:

1
可以发现,主线程任务执行后,调用pthread_cancel()函数,杀死子进程,在子进程的第一个for循环,此时还不存在系统调用,但是当子线程中执行到printf("子线程ID:%ld\n",pthread_self());后此时printf调用了系统函数,在子线程间接进行了系统调用,成功杀死子进程,子进程第二个for打印printf("child i:%ld\n",i)就不再执行。

image-20231215220154221

系统调用的方式:

1
2
1.直接调用 Linux 系统函数
2.调用标准 C 库函数,为了实现某些功能,在 Linux 平台下标准 C 库函数会调用相关的系统函数

6.2线程ID比较

在Linux系统中线程ID本身就是一个无符号长整形,因此可以直接使用比较操作符比较两个线程ID,但是线程库是可以跨平台使用的,在某些平台上pthread_t可能不是一个单纯的整型,这种情况比较两个线程ID必须使用比较函数;

比较函数原型:

1
2
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
  • 参数:t1 和 t2 是要比较的线程的线程 ID
  • 返回值:如果两个线程 ID 相等返回非 0 值,如果不相等返回 0

7.线程同步

假设有 4 个线程 A、B、C、D,当前一个线程 A 对内存中的共享资源进行访问的时候,其他线程 B, C, D 都不可以对这块内存进行操作,直到线程 A 对这块内存访问完毕为止,B,C,D 中的一个才能访问这块内存,剩余的两个需要继续阻塞等待,以此类推,直至所有的线程都对这块内存操作完毕。 线程对内存的这种访问方式就称之为线程同步,通过对概念的介绍,我们可以了解到所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。

7.1为什么要线程同步

在研究线程同步之前,先来看一个两个线程交替数数(每个线程数 50 个数,交替数到 100)的例子:

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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <pthread.h>

#define MAX 50
//全局变量
int number;

//线程A处理函数
void *funA_num(void *arg)
{
for (int i = 0; i < MAX; i++)
{
int cur = number;
cur++;
number = cur;
printf("Thread A,id = %ld,number = %d\n",pthread_self(),number);
}
return NULL;
}

//线程B处理函数
void *funB_num(void *arg)
{
for (int i = 0; i < MAX; i++)
{
int cur = number;
cur++;
number = cur;
printf("Thread B,id = %ld,number = %d\n", pthread_self(), number);
//休眠5s
sleep(5);
}
return NULL;
}

int main(int argc,char *argv)
{
pthread_t p1, p2;

//创建两个子线程
pthread_create(&p1,NULL,funA_num,NULL);
pthread_create(&p2, NULL, funB_num, NULL);

//阻塞,资源回收
pthread_join(p1,NULL);
pthread_join(p2,NULL);

return 0;
}

代码结果:

image-20231219193211879

结果分析:

1
2
3
4
5
6
通过对上面例子的测试,可以看出虽然每个线程内部循环了 50 次每次数一个数,但是最终没有数到 100(最后还是数到了100),通过输出的结果可以看到,有些数字被重复数了多次,其原因就是没有对线程进行同步处理,造成了数据的混乱。

在测试程序中两个线程共用全局变量 number 当线程变成运行态之后开始数数,从物理内存加载数据,让后将数据放到 CPU 进行运算,最后将结果更新到物理内存中。如果数数的两个线程都可以顺利完成这个流程,那么得到的结果肯定是正确的。

如果线程 A 执行这个过程期间就失去了 CPU 时间片,线程 A 被挂起了最新的数据没能更新到物理内存。线程 B 变成运行态之后从物理内存读数据,很显然它没有拿到最新数据,只能基于旧的数据往后数,然后失去 CPU 时间片挂起。线程 A 得到 CPU 时间片变成运行态,第一件事儿就是将上次没更新到内存的数据更新到内存,但是这样会导致线程 B 已经更新到内存的数据被覆盖,活儿白干了,最终导致有些数据会被重复数很多次。

  • 两个线程在数数的时候需要分时复用CPU时间片,并且测试程序中嗲用sleep()导致线程的CPU时间片没用完就被挂起,这让CPU的上下文切换(保存当前状态,下一次运行时,需要加载保存的状态)更为频繁。更容易造成数据混乱的现象

    image-20231219193307167

    CPU 对应寄存器、一级缓存、二级缓存、三级缓存是独占的,用于存储处理的数据和线程的状态信息,数据被 CPU 处理完成需要再次被写入到物理内存中,物理内存数据也可以通过文件 IO 操作写入到磁盘中。

7.2同步方式

对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步。常用的线程同步方式有四种:互斥锁、读写锁、条件变量、信号量。所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也被称之为临界资源

  • 临界资源:找全局变量

image-20231219193326014

找到临界资源之后,再找和临界资源相关的上下文代码,这样就得到了一个代码块,这个代码块可以称之为临界区。确定好临界区(临界区越小越好)之后,就可以进行线程同步了,线程同步的大致处理思路是这样的:

  • 在临界区代码的上边,添加加锁函数,对临界区加锁。
    • 哪个线程调用这句代码,就会把这把锁锁上,其他线程就只能阻塞在锁上了。
  • 在临界区代码的下边,添加解锁函数,对临界区解锁。
    • 出临界区的线程会将锁定的那把锁打开,其他抢到锁的线程就可以进入到临界区了。
  • 通过锁机制能保证临界区代码最多只能同时有一个线程访问,这样并行访问就变为串行访问了

8.互斥锁

8.1互斥锁函数

互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块,被锁定的这个代码块,所有的线程只能顺序执行 (不能并行处理),这样多线程访问共享资源数据混乱的问题就可以被解决了,需要付出的代价就是执行效率的降低,因为默认临界区多个线程是可以并行处理的,现在只能串行处理


在Linux中互斥锁的类型为pthread_mutex_t,创建一个这种类型的变量就得到了一把互斥锁:

1
pthread_mutex_t mutex;
  • 在创建的锁对象中保存了当前这把锁的状态信息:锁定还是打开,如果是锁定状态还记录了给这把锁加锁的线程信息(线程 ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程才能被解除阻塞。一般情况下,每一个共享资源对应一个把互斥锁,锁的个数和线程的个数无关。

互斥锁函数操作原型:

1
2
3
4
5
//初始化互斥锁
//restrict:是一个关键字,用于修饰指针,只有这个关键词修饰的指针可以访问指向的内存地址,其他指针,无法访问
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
//释放互斥锁资源
int pthread_mutex_destory(pthread_mutex_t *mutex);
  • 参数:
    • mutex : 互斥锁变量的地址
    • attr : 互斥锁属性,一般默认即可,这个参数指定为NULL
  • 返回值:
    • 如果函数调用成功会返回 0,调用失败会返回相应的错误号

1
2
// 修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中
int pthread_mutex_lock(pthread_mutex_t *mutex);

这个函数被调用,首先会判断参数 mutex 互斥锁中的状态是不是锁定状态:

  • 没有被锁定,是打开的,这个线程可以加锁成功,这个这个锁中会记录是哪个线程加锁成功了
  • 如果被锁定了,其他线程加锁就失败了,这些线程都会阻塞在这把锁上
  • 当这把锁被解开之后,这些阻塞在锁上的线程就解除阻塞了,并且这些线程是通过竞争的方式对这把锁加锁,没抢到锁的线程继续阻塞

1
2
// 尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);

调用这个函数对互斥锁变量加锁还是有两种情况:

  • 如果这把锁没有被锁定是打开的,线程加锁成功
  • 如果锁变量被锁住了,调用这个函数加锁的线程,不会被阻塞,加锁失败直接返回错误号

1
2
// 对互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 不是所有的线程都可以对互斥锁解锁,哪个线程加的锁,哪个线程才能解锁成功。

8.2互斥锁的使用

将7.1代码进行修改,两个线程操作同一个全局变量number,因此需要添加一把互斥锁,来控制两个线程

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
68
69
70
71
72
73
74
75
76
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <pthread.h>

#define MAX 50
//全局变量
int number;

//创建一把互斥锁
//全局变量,多个线程共享
pthread_mutex_t mutex;

//线程A处理函数
void *funA_num(void *arg)
{
//若线程A加锁成功,线程A不阻塞
//若线程B加锁成功,线程A阻塞
for (int i = 0; i < MAX; i++)
{
pthread_mutex_lock(&mutex);
int cur = number;
cur++;
usleep(5);
number = cur;
//解锁
pthread_mutex_unlock(&mutex);
printf("Thread A,id = %ld,number = %d\n",pthread_self(),number);
}
pthread_exit(NULL);
// return NULL;
}

//线程B处理函数
void *funB_num(void *arg)
{
for (int i = 0; i < MAX; i++)
{
//a加锁成功, b线程访问这把锁的时候是锁定的
//线程B先阻塞, a线程解锁之后阻塞解除
//线程B加锁成功了
pthread_mutex_lock(&mutex);
int cur = number;
cur++;
usleep(10);//10us--可以不加---使得线程休眠
number = cur;
printf("Thread B,id = %ld,number = %d\n", pthread_self(), number);
pthread_mutex_unlock(&mutex);
}
pthread_exit(NULL);
// return NULL;
}

int main(int argc,char *argv)
{
pthread_t p1, p2;

//初始化互斥锁
pthread_mutex_init(&mutex,NULL);

//创建两个子线程
pthread_create(&p1,NULL,funA_num,NULL);
pthread_create(&p2, NULL, funB_num, NULL);

//阻塞,资源回收
pthread_join(p1,NULL);
pthread_join(p2,NULL);

//消费互斥锁
//线程销毁后,释放互斥锁
pthread_mutex_destroy(&mutex);

return 0;
}

运行结果:

线程A\B一起数数到了100,但是并非完全交替数数,因为多个线程抢占CPU是随机的是一个概率问题

image-20231219193939447

image-20231219194009400

8.3死锁

当多个线程访问共享资源,需要加锁,如果锁使用不当,就会造成死锁这种现象。如果线程死锁造成的后果是:所有的线程都被阻塞,并且线程的阻塞是无法解开的(因为可以解锁的线程也被阻塞了)。


造成死锁的场景有如下几种:

  • 加锁之后忘记解锁

    • // 场景1
      void func()
      {
          for(int i=0; i<6; ++i)
          {
              // 当前线程A加锁成功, 当前循环完毕没有解锁, 在下一轮循环的时候自己被阻塞了
              // 其余的线程也被阻塞
              pthread_mutex_lock(&mutex);
              ....
              .....
              // 忘记解锁
          }
      }
      
      // 场景2
      void func()
      {
          for(int i=0; i<6; ++i)
          {
              // 当前线程A加锁成功
              // 其余的线程被阻塞
              pthread_mutex_lock(&mutex);
              ....
              .....
              if(xxx)
              {
                  // 函数退出, 没有解锁(解锁函数无法被执行了)
                  return ;
              }
              
              pthread_mutex_lock(&mutex);
          }
      }
      
      
      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

      * 重复加锁,造成死锁

      * ```c
      void func()
      {
      for(int i=0; i<6; ++i)
      {
      // 当前线程A加锁成功
      // 其余的线程阻塞
      pthread_mutex_lock(&mutex);
      // 锁被锁住了, A线程阻塞
      pthread_mutex_lock(&mutex);
      ....
      .....
      pthread_mutex_unlock(&mutex);
      }
      }

      // 隐藏的比较深的情况
      void funcA()
      {
      for(int i=0; i<6; ++i)
      {
      // 当前线程A加锁成功
      // 其余的线程阻塞
      pthread_mutex_lock(&mutex);
      ....
      .....
      pthread_mutex_unlock(&mutex);
      }
      }

      void funcB()
      {
      for(int i=0; i<6; ++i)
      {
      // 当前线程A加锁成功
      // 其余的线程阻塞
      pthread_mutex_lock(&mutex);
      funcA(); //在函数B中调用函数A 重复加锁
      ....
      .....
      pthread_mutex_unlock(&mutex);
      }
      }

  • 在程序中有多个共享资源,因此有很多把锁,随意加锁,导致相互被阻塞

    场景描述:

    1.有两个共享资源:X, Y,X对应锁A, Y对应锁B

    ​ 线程A访问资源X, 加锁A

    ​ 线程B访问资源Y, 加锁B

    2.线程A要访问资源Y, 线程B要访问资源X,因为资源X和Y已经被对应的锁锁住了,因此这个两个线程被阻塞

    ​ 线程A被锁B阻塞了, 无法打开A锁

    ​ 线程B被锁A阻塞了, 无法打开B锁

    image-20231219194515636

  • 避免死锁

    避免多次锁定,多检查

    对共享资源访问完毕之后,一定要解锁,或者在加锁的使用 trylock

    如果程序中有多把锁,可以控制对锁的访问顺序 (顺序访问共享资源,但在有些情况下是做不到的),另外也可以在对其他互斥锁做加锁操作之前,先释放当前线程拥有的互斥锁。

    项目程序中可以引入一些专门用于死锁检测的模块

9.读写锁

读写锁是互斥锁的升级版,在做读操作的时候可以提高程序的执行效率,如果所有的线程都是做读操作, 那么读是并行的,但是使用互斥锁,读操作也是串行的。

使用读写锁 读:并行 写:串行

读写锁是一把锁,锁的类型为 pthread_rwlock_t

1
2
pthread_rwlock_t rwlock;
//创建一把读写锁

读写锁可以做两件事情,既可以锁定读操作,可以锁定写操作


以下是读写锁中记录的信息

  • 锁的状态:锁定 / 打开
  • 锁定的是什么操作:读操作 / 写操作,使用读写锁锁定了读操作,需要先解锁才能去锁定写操作,反之亦然。
  • 哪个线程将这把锁锁上了

读写锁的使用方式也互斥锁的使用方式是完全相同的:找共享资源,确定临界区,在临界区的开始位置加锁(读锁 / 写锁),临界区的结束位置解锁

读写锁的特点:

  • 使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的。
  • 使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的(与互斥锁一样)。
  • 使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问者两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高。


读写锁函数操作原型:

1
2
3
4
5
6
7
#include <pthread.h>
pthread_rwlock_t rwlock;
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
// 释放读写锁占用的系统资源
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  • 参数

    rwlock: 读写锁的地址,传出参数

    attr: 读写锁属性,一般使用默认属性,指定为 NULL

  • 返回值:

    如果函数调用成功返回 0,失败返回对应的错误号


1
2
// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数的线程会被阻塞


1
2
3
// 这个函数可以有效的避免死锁
// 如果加读锁失败, 不会阻塞当前线程, 直接返回错误号
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数加锁失败,对应的线程不会被阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作。


1
2
// 在程序中对读写锁加写锁, 锁定的是写操作
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数的线程会被阻塞


1
2
3
// 这个函数可以有效的避免死锁
// 如果加写锁失败, 不会阻塞当前线程, 直接返回错误号
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数加锁失败,但是线程不会阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作。


解锁:

1
2
// 解锁, 不管锁定了读还是写都可用解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

9.1读写锁的使用

题目要求:8 个线程操作同一个全局变量,3 个线程不定时写同一全局资源,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
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 全局变量
int number = 0;

// 定义读写锁
pthread_rwlock_t rwlock;

// 写的线程的处理函数
void* writeNum(void* arg)
{
while(1)
{
//添加写锁
pthread_rwlock_wrlock(&rwlock);
int cur = number;
cur ++;
number = cur;
printf("++写操作完毕, number : %d, tid = %ld\n", number, pthread_self());
pthread_rwlock_unlock(&rwlock);
// 添加sleep目的是要看到多个线程交替工作
sleep(1);
}

pthread_exit(NULL);
// return NULL;
}

// 读线程的处理函数
// 多个线程可以如果处理动作相同, 可以使用相同的处理函数
// 每个线程中的栈资源是独享
void* readNum(void* arg)
{
while(1)
{
//添加读锁
pthread_rwlock_rdlock(&rwlock);
printf("--全局变量number = %d, tid = %ld\n", number, pthread_self());
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
pthread_exit(NULL);
// return NULL;
}

int main()
{
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);

// 3个写线程, 5个读的线程
pthread_t wtid[3];
pthread_t rtid[5];
for(int i=0; i<3; ++i)
{
pthread_create(&wtid[i], NULL, writeNum, NULL);
}

for(int i=0; i<5; ++i)
{
pthread_create(&rtid[i], NULL, readNum, NULL);
}

// 释放资源---线程回收
for(int i=0; i<3; ++i)
{
pthread_join(wtid[i], NULL);
}

for(int i=0; i<5; ++i)
{
pthread_join(rtid[i], NULL);
}

// 销毁读写锁
pthread_rwlock_destroy(&rwlock);

return 0;
}

运行结果

读操作可以并行。但是写操作只能串行,每次写操作造成number均会变化,实现线程同步

image-20231219200451747

10.条件变量

10.1条件变量函数

严格意义上来说,条件变量的主要作用不是处理线程同步,而是进行线程的阻塞。如果在多线程程序中只使用条件变量无法实现线程的同步,必须要配合互斥锁来使用。虽然条件变量和互斥锁都能阻塞线程,但是二者的效果是不一样的,二者的区别如下:

  • 假设有 A-Z 26 个线程,这 26 个线程共同访问同一把互斥锁,如果线程 A 加锁成功,那么其余 B-Z 线程访问互斥锁都阻塞,所有的线程只能顺序访问临界区
  • 条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,这种情况下还是会出现共享资源中数据的混乱。

一般情况下条件变量用于处理生产者和消费者模型,并且和互斥锁配合使用。条件变量类型对应的类型为pthread_cond_t,这样就可以定义一个条件变量类型的变量了:

1
pthread_cond_t cond;//定义一个条件变量类型的变量

被条件变量阻塞的线程的线程信息会被记录到这个变量中,以便在解除阻塞的时候使用


条件变量操作原型如下:

1
2
3
4
5
6
7
#include <pthread.h>
pthread_cond_t cond;
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
// 销毁释放资源
int pthread_cond_destroy(pthread_cond_t *cond);

参数

  • cond: 条件变量的地址
  • attr: 条件变量属性,一般使用默认属性,指定为 NULL

线程阻塞函数:

1
2
// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

通过函数原型可以看出,该函数在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁主要功能是进行线程同步,让线程顺序进入临界区,避免出现数共享资源的数据混乱。该函数会对这个互斥锁做以下几件事情:

  • 在阻塞线程时候,如果线程已经对互斥锁 mutex 上锁,那么会将这把锁打开,这样做是为了避免死锁
  • 当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个mutex互斥锁锁上,继续向下访问临界区

1
2
3
4
5
6
7
8
// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};
// 将线程阻塞一定的时间长度, 时间到达之后(超时设置), 线程就解除阻塞了
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

这个函数的前两个参数和 pthread_cond_wait 函数是一样的,第三个参数表示线程阻塞的时长,但是需要额外注意一点:struct timespec 这个结构体中记录的时间是从1971.1.1到某个时间点的时间,总长度使用秒/纳秒表示。因此赋值方式相对要麻烦一点:

1
2
3
4
time_t mytim = time(NULL);	// 1970.1.1 0:0:0 到当前的总秒数
struct timespec tmsp;
tmsp.tv_nsec = 0;
tmsp.tv_sec = time(NULL) + 100; // 线程阻塞100s

1
2
3
4
// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);

调用上面两个函数中的任意一个,都可以换线被 pthread_cond_wait 或者pthread_cond_timedwait阻塞的线程,区别就在于 pthread_cond_signal 是唤醒至少一个被阻塞的线程(总个数不定),pthread_cond_broadcast 是唤醒所有被阻塞的线程。

10.2生产者和消费者

生产者和消费者模型的组成:

  • 生产者线程->若干个

    生产商品或者任务放入到任务队列中

    任务队列满了就阻塞,不满的时候就工作

    通过一个生产者的条件变量控制生产者线程阻塞和非阻塞

  • 消费者线程->若干个

    读任务队列,将任务或者数据取出

    任务队列中有数据就消费,没有数据就阻塞

    通过一个消费者的条件变量控制消费者线程阻塞和非阻塞

  • 队列 -> 存储任务 / 数据,对应一块内存,为了读写访问可以通过一个数据结构维护这块内存

    可以是数组、链表,也可以使用 stl 容器:queue /stack/list/vector

    image-20231219205133201

1
场景描述:使用条件变量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 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
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 链表的节点
struct Node
{
int number;
struct Node *next; // 后驱指针
};

// 定义条件变量
pthread_cond_t cond;
// 定义互斥锁变量
pthread_mutex_t mutex;
// 创建链表的头指针
struct Node *head = NULL;

// 生产者的回调函数
void *producer(void *arg)
{
// 一直生产
while(1)
{
// 加锁
pthread_mutex_lock(&mutex);
// 创建新的链表节点
struct Node *pnew = (struct Node*) malloc(sizeof (struct Node));
// 节点初始化
pnew->number = rand() % 100;
// 节点的连接, 添加到链表的头部, 新节点就新的头结点
pnew->next = head;
// 头节点指针指向新添加的节点
head = pnew;
printf("+++producer, number = %d, tid = %ld\n", pnew->number, (pthread_self() % 100));
// 解锁
pthread_mutex_unlock(&mutex);
// 生产慢一点
sleep(rand() % 3);

// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
// 生产了任务, 通知消费者消费
pthread_cond_broadcast(&cond);
}

// 线程退出
pthread_exit(NULL);
}


// 消费者回调函数
void *consumer(void *arg)
{
if(1)
{
// 加锁
pthread_mutex_lock(&mutex);
// 任务队列中已经没有节点可以进行消费了
if(head == NULL)
{
// 1.调用该函数使得消费者线程在此处进行阻塞
// 2.消费者线程在59行添加互斥锁成功,但是线程阻塞在这行代码上面,锁还没有解开
// 3.消费者所有线程在访问这把锁的时均会阻塞,生产者也会阻塞 ==> 如果不使用pthread_cond_wait函数则会造成死锁
// 4.pthread_cond_wait函数会自动将线程拥有的锁解开, ==> 避免死锁
// 5.消费者解开锁之后,生产者就可以继续加锁进行生产
// 6.当任务队列又有节点可以进行消费时,消费者通过函数pthread_cond_broadcast解除消费者阻塞
pthread_cond_wait(&cond, &mutex);
// 7.当消费者线程解除阻塞之后, 会自动将这把锁锁上
// 8.这时候当前这个线程又重新拥有了这把互斥锁
}
// 取出任务队列中的头节点进行消费
struct Node * pnode = head;
printf("--consumer: number: %d, tid = %ld\n", pnode->number, (pthread_self() % 100));
head = pnode->next;
free(pnode);
pthread_mutex_unlock(&mutex);

sleep(rand() % 3);
}
pthread_exit(NULL);
}


int main(int argc,char **argv)
{
// 初始化条件变量
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);

// 创建5个生产者, 5个消费者
pthread_t ptid[5];
pthread_t ctid[5];
for(int i=0; i<5; ++i)
{
pthread_create(&ptid[i], NULL, producer, NULL);
}

for(int i=0; i<5; ++i)
{
pthread_create(&ctid[i], NULL, consumer, NULL);
}

// 释放资源--线程回收
for(int i=0; i<5; ++i)
{
// 阻塞等待子线程退出
pthread_join(ptid[i], NULL);
}

for(int i=0; i<5; ++i)
{
pthread_join(ctid[i], NULL);
}

// 销毁条件变量
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);

return 0;
}

代码分析:

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
void* consumer(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
// 一直消费, 删除链表中的一个节点
if(head == NULL) // 这样写有bug
{
pthread_cond_wait(&cond, &mutex);
}
// 取出链表的头结点, 将其删除
struct Node* pnode = head;
printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
head = pnode->next;
free(pnode);
pthread_mutex_unlock(&mutex);

sleep(rand() % 3);
}
return NULL;
}

/*
为什么在第7行使用if 有bug:
当任务队列为空, 所有的消费者线程都会被这个函数阻塞 pthread_cond_wait(&cond, &mutex);
也就是阻塞在代码的第9行

当生产者生产了1个节点, 调用 pthread_cond_broadcast(&cond); 唤醒了所有阻塞的线程
- 有一个消费者线程通过 pthread_cond_wait()加锁成功, 其余没有加锁成功的线程继续阻塞
- 加锁成功的线程向下运行, 并成功删除一个节点, 然后解锁
- 没有加锁成功的线程解除阻塞继续抢这把锁, 若又一个子线程加锁成功
- 但是这个任务链表中的节点已经为上一个消费者给删除了此时链表中又为NUll,因为if(head == NULL)只进行了一次判断,这个时候链表为NULL,但是已经不会重新回到上面进行判断 ,后边访问这个空的任务队列并对其进行节点删除的时候就会出现段错误
解决方案:
- 需要循环的对链表是否为空进行判断, 需要将if 该成 while
*/

代码改进

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 链表的节点
struct Node
{
int number;
struct Node *next; // 后驱指针
};

// 定义条件变量
pthread_cond_t cond;
// 定义互斥锁变量
pthread_mutex_t mutex;
// 创建链表的头指针
struct Node *head = NULL;

// 生产者的回调函数
void *producer(void *arg)
{
// 一直生产
while(1)
{
// 加锁
pthread_mutex_lock(&mutex);
// 创建新的链表节点
struct Node *pnew = (struct Node*) malloc(sizeof (struct Node));
// 节点初始化
pnew->number = rand() % 100;
// 节点的连接, 添加到链表的头部, 新节点就新的头结点
pnew->next = head;
// 头节点指针指向新添加的节点
head = pnew;
printf("+++producer, number = %d, tid = %ld\n", pnew->number, (pthread_self() % 100));
// 解锁
pthread_mutex_unlock(&mutex);
// 生产慢一点
sleep(rand() % 3);

// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
// 生产了任务, 通知消费者消费
pthread_cond_broadcast(&cond);
}

// 线程退出
pthread_exit(NULL);
}


// 消费者回调函数
void *consumer(void *arg)
{
if(1)
{
// 加锁
pthread_mutex_lock(&mutex);
// 任务队列中已经没有节点可以进行消费了
while(head == NULL)
{
// 1.调用该函数使得消费者线程在此处进行阻塞
// 2.消费者线程在59行添加互斥锁成功,但是线程阻塞在这行代码上面,锁还没有解开
// 3.消费者所有线程在访问这把锁的时均会阻塞,生产者也会阻塞 ==> 如果不使用pthread_cond_wait函数则会造成死锁
// 4.pthread_cond_wait函数会自动将线程拥有的锁解开, ==> 避免死锁
// 5.消费者解开锁之后,生产者就可以继续加锁进行生产
// 6.当任务队列又有节点可以进行消费时,消费者通过函数pthread_cond_broadcast解除消费者阻塞
pthread_cond_wait(&cond, &mutex);
// 7.当消费者线程解除阻塞之后, 会自动将这把锁锁上
// 8.这时候当前这个线程又重新拥有了这把互斥锁
}
// 取出任务队列中的头节点进行消费
struct Node * pnode = head;
printf("--consumer: number: %d, tid = %ld\n", pnode->number, (pthread_self() % 100));
head = pnode->next;
free(pnode);
pthread_mutex_unlock(&mutex);

sleep(rand() % 3);
}
pthread_exit(NULL);
}


int main(int argc,char **argv)
{
// 初始化条件变量
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);

// 创建5个生产者, 5个消费者
pthread_t ptid[5];
pthread_t ctid[5];
for(int i=0; i<5; ++i)
{
pthread_create(&ptid[i], NULL, producer, NULL);
}

for(int i=0; i<5; ++i)
{
pthread_create(&ctid[i], NULL, consumer, NULL);
}

// 释放资源--线程回收
for(int i=0; i<5; ++i)
{
// 阻塞等待子线程退出
pthread_join(ptid[i], NULL);
}

for(int i=0; i<5; ++i)
{
pthread_join(ctid[i], NULL);
}

// 销毁条件变量
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);

return 0;
}

若队伍队列的大小存在限制,则需要对生产者使用条件变量

消费者与生产者需要使用不同的条件变量对各自阻塞的线程进行唤醒

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

//创建一个条件变量
pthread_cond_t producer_cond;
pthread_cond_t consumer_cond;
//创建一个互斥锁
pthread_mutex_t mutex;

//定义生产者生产仓库的容量
#define MAX 100
int count = 0;

//链表节点类型
struct Node
{
int number;
struct Node* next;
};

//头节点
struct Node *head = NULL;


//生产者回调函数
void *producer(void *arg)
{
while (1)
{
//上互斥锁
pthread_mutex_lock(&mutex);
//当生产者生产商品大于仓库容量时
while (count >= MAX)
{
//线程阻塞函数
//条件变量阻塞生产者线程,使其不再生产
pthread_cond_wait(&producer_cond, &mutex);
}
//创建新节点
struct Node *newnode = malloc(sizeof(struct Node));
//初始化节点
newnode->number = rand() % 1000;
newnode->next = head;
head = newnode;
count++;
printf("生产者,id:%ld , number:%d , count = %d\n", (pthread_self() % 100), newnode->number, count);
//解锁
pthread_mutex_unlock(&mutex);
//唤醒被阻塞的消费者线程
pthread_cond_broadcast(&consumer_cond);
sleep(rand() % 3);//休眠控制生产速度
}
return NULL;
}


//消费者回调函数
void *consumer(void *arg)
{
while (1)
{
//添加互斥锁
pthread_mutex_lock(&mutex);
while (head == NULL)
{
//此时链表中无节点---商品为空
//条件变量线程阻塞
pthread_cond_wait(&consumer_cond, &mutex);
}
struct Node *node = head;
head = head->next;
free(node);
count--;
printf("消费者,id:%ld , number:%d , count = %d\n", (pthread_self() % 100), node->number, count);
//消费者count--,此时消费者继续消费,货物数量小于仓库总容量,唤醒阻塞的生产者线程
pthread_cond_broadcast(&producer_cond);
//解锁
pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}
return NULL;
}


int main()
{
//初始化互斥锁
pthread_mutex_init(&mutex, NULL);
//初始化条件变量
pthread_cond_init(&producer_cond, NULL);
pthread_cond_init(&consumer_cond, NULL);

//创建生产者消费者线程ID
pthread_t t1[5], t2[5];
//创建生产者线程
for (int i = 0; i < 5; i++)
{
pthread_create(&t1[i], NULL, producer, NULL);
}
//创建消费者线程
for (int i = 0; i < 5; i++)
{
pthread_create(&t2[i], NULL, consumer, NULL);
}

//线程资源回收
for (int i = 0; i < 5; i++)
{
pthread_join(t1[i], NULL);
pthread_join(t2[i], NULL);
}

//销毁资源释放
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&consumer_cond);
pthread_cond_destroy(&producer_cond);

return 0;
}

运行结果

image-20231219220316694

11.信号量

11.1信号量函数

信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。

信号量(信号灯)与互斥锁和条件变量的主要不同在于”灯”的概念,灯亮则意味着资源可用,灯灭则意味着不可用。信号量主要阻塞线程, 不能完全保证线程安全,如果要保证线程安全, 需要信号量和互斥锁一起使用。


信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程或者消费者线程的运行。信号的类型为sem_t对应的头文件为<semaphore.h>

1
2
#include <semaphore.h>
sem_t sem;

信号量操作函数:

1
2
3
4
5
6
#include <semaphore.h>
// 初始化信号量/信号灯
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 资源释放, 线程销毁之后调用这个函数即可
// 参数 sem 就是 sem_init() 的第一个参数
int sem_destroy(sem_t *sem);
  • 参数:

    sem:信号量变量地址

    pshared

    0:线程同步

    非0:进程同步

    value:初始化当前信号量拥有的资源数(>=0),如果资源数为0,线程就会被阻塞了。


1
2
3
// 参数 sem 就是 sem_init() 的第一个参数  
// 函数被调用sem中的资源就会被消耗1个, 资源数-1
int sem_wait(sem_t *sem);

当线程调用这个函数,并且sem中的资源数>0,线程不会阻塞,线程会占用sem中的一个资源,因此资源数-1,直到sem中的资源数减为0时,资源被耗尽,因此线程也就被阻塞了。


1
2
3
// 参数 sem 就是 sem_init() 的第一个参数  
// 函数被调用sem中的资源就会被消耗1个, 资源数-1
int sem_trywait(sem_t *sem);

当线程调用这个函数,并且sem中的资源数>0,线程不会阻塞,线程会占用sem中的一个资源,因此资源数-1,直到sem中的资源数减为0时,资源被耗尽,但是线程不会被阻塞,直接返回错误号,因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况。


1
2
3
4
5
6
7
8
// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};
// 调用该函数线程获取sem中的一个资源,当资源数为0时,线程阻塞,在阻塞abs_timeout对应的时长之后,解除阻塞。
// abs_timeout: 阻塞的时间长度, 单位是s, 是从1970.1.1开始计算的
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

该函数的参数abs_timeoutpthread_cond_timedwait的最后一个参数是一样的,使用方法不再过多赘述。当线程调用这个函数,并且sem中的资源数>0,线程不会阻塞,线程会占用sem中的一个资源,因此资源数-1,直到sem中的资源数减为0时,资源被耗尽,线程被阻塞,当阻塞指定的时长之后,线程解除阻塞。


1
2
// 调用该函数给sem中的资源数+1
int sem_post(sem_t *sem);

调用该函数会将sem中的资源数+1,如果有线程在调用sem_waitsem_trywaitsem_timedwait时因为sem中的资源数为0被阻塞了,这时这些线程会解除阻塞,获取到资源之后继续向下运行。


1
2
3
// 查看信号量 sem 中的整形数的当前值, 这个值会被写入到sval指针对应的内存中
// sval是一个传出参数
int sem_getvalue(sem_t *sem, int *sval);

通过这个函数可以查看sem中现在拥有的资源个数,通过第二个参数sval将数据传出,也就是说第二个参数的作用和返回值是一样的。


11.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
// 生产者线程 
sem_t psem;
// 消费者线程
sem_t csem;

// 信号量初始化
sem_init(&psem, 0, 5); // 5个生产者可以同时生产
sem_init(&csem, 0, 0); // 消费者线程没有资源, 因此不能消费

// 生产者线程
// 在生产之前, 从信号量中取出一个资源
sem_wait(&psem);
// 生产者商品代码, 有商品了, 放到任务队列
......
......
......
// 通知消费者消费,给消费者信号量添加资源,让消费者解除阻塞
sem_post(&csem);

////////////////////////////////////////////////////////
////////////////////////////////////////////////////////

// 消费者线程
// 消费者需要等待生产, 默认启动之后应该阻塞
sem_wait(&csem);
// 开始消费
......
......
......
// 消费完成, 通过生产者生产,给生产者信号量添加资源
sem_post(&psem);

通过上面的代码可以知道,初始化信号量的时候没有消费者分配资源,消费者线程启动之后由于没有资源自然就被阻塞了,等生产者生产出产品之后,再给消费者分配资源,这样二者就可以配合着完成生产和消费流程了。

11.3信号量的使用

如果生产者和消费者线程使用的信号量对应的总资源数为1,那么不管线程有多少个,可以工作的线程只有一个,其余线程由于拿不到资源,都被迫阻塞了。

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

// 链表的节点
struct Node
{
int number;
struct Node* next;
};

// 生产者线程信号量
sem_t psem;
// 消费者线程信号量
sem_t csem;

// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
11.3.1总资源数为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
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
//信号量头文件
#include <semaphore.h>

/*
使用信号量
编写生产者和消费者模型
总资源数为1的情况
*/

//生产者的信号量
sem_t semp;
//消费者信号量
sem_t semc;

//创建一个互斥锁
pthread_mutex_t mutex;

//链表节点类型
struct Node
{
int number;
struct Node* next;
};

//头节点
struct Node *head = NULL;

//生产者回调函数
void *producer(void *arg)
{
while (1)
{
//检测生产者是否有资源
sem_wait(&semp);//资源>0就不会被阻塞---生产者线程会占用一个资源-1

//创建新节点
struct Node *newnode = malloc(sizeof(struct Node));
//初始化节点
newnode->number = rand() % 1000;
newnode->next = head;
head = newnode;
printf("生产者,id:%ld , number:%d\n", (pthread_self() % 100), newnode->number);
//通知消费者消费---消费者资源+1
sem_post(&semc);
sleep(rand() % 3);//休眠控制生产速度
}
return NULL;
}

//消费者回调函数
void *consumer(void *arg)
{
while (1)
{
//消费者尝试消费,若有资源则消费者资源-1
sem_wait(&semc);

struct Node *node = head;
printf("消费者,id:%ld , number:%d\n", (pthread_self() % 100), node->number);
head = head->next;
free(node);
//通知生产者生产
sem_post(&semp);
sleep(rand() % 3);
}
return NULL;
}


int main()
{
//初始化互斥锁
pthread_mutex_init(&mutex, NULL);
//初始化信号变量
//1.生产者
sem_init(&semp, 0, 1);//生产者总共的资源数为1--->则可同时工作的线程为1个
//2.消费者 -> 资源初始化为0 消费则线程启动就阻塞
sem_init(&semc, 0, 0);


//创建生产者消费者线程ID
pthread_t t1[5], t2[5];
//创建生产者线程
for (int i = 0; i < 5; i++)
{
pthread_create(&t1[i], NULL, producer, NULL);
}
//创建消费者线程
for (int i = 0; i < 5; i++)
{
pthread_create(&t2[i], NULL, consumer, NULL);
}

//线程资源回收
for (int i = 0; i < 5; i++)
{
pthread_join(t1[i], NULL);
pthread_join(t2[i], NULL);
}

//销毁资源释放
pthread_mutex_destroy(&mutex);
//信号量的销毁
sem_destroy(&semp);
sem_destroy(&semc);

return 0;
}

通过测试代码可以得到如下结论:如果生产者和消费者使用的信号量总资源数为1,那么不会出现生产者线程和消费者线程同时访问共享资源的情况,不管生产者和消费者线程有多少个,它们都是顺序执行的。

运行结果

可以看到生产者与消费者交替运行

image-20231219222122218

11.3.2总资源数大于1

如果生产者和消费者线程使用的信号量对应的总资源数为大于1,这种场景下出现的情况就比较多了:

  • 多个生产者线程同时生产
  • 多个消费者同时消费
  • 生产者线程和消费者线程同时生产和消费

以上不管哪一种情况都可能会出现多个线程访问共享资源的情况,如果想防止共享资源出现数据混乱,那么就需要使用互斥锁进行线程同步,线程线性执行任务 ,处理代码如下:

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

// 链表的节点
struct Node
{
int number;
struct Node* next;
};

// 生产者线程信号量
sem_t psem;
// 消费者线程信号量
sem_t csem;

// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针

在编写上述代码的时候还有一个需要注意是事项,不管是消费者线程的处理函数还是生产者线程的处理函数内部有这么两行代码:

1
2
3
4
5
6
7
// 消费者
sem_wait(&csem);
pthread_mutex_lock(&mutex);

// 生产者
sem_wait(&csem);
pthread_mutex_lock(&mutex);

这两行代码的调用顺序是不能颠倒的,如果颠倒过来就有可能会造成死锁,下面来分析一种死锁的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void* producer(void* arg)
{
// 一直生产
while(1)
{
pthread_mutex_lock(&mutex);
// 生产者拿一个信号灯
sem_wait(&psem);
......
......
// 通知消费者消费
sem_post(&csem);
pthread_mutex_unlock(&mutex);

// 生产慢一点
sleep(rand() % 3);
}
return NULL;
}

// 消费者的回调函数
void* consumer(void* arg)

在上面的代码中,初始化状态下消费者线程没有任务信号量资源,假设某一个消费者线程先运行,调用pthread_mutex_lock(&mutex);对互斥锁加锁成功,然后调用sem_wait(&csem);由于没有资源,因此被阻塞了。其余的消费者线程由于没有抢到互斥锁,因此被阻塞在互斥锁上。对应生产者线程第一步操作也是调用pthread_mutex_lock(&mutex);,但是这时候互斥锁已经被消费者线程锁上了,所有生产者都被阻塞,到此为止,多余的线程都被阻塞了,程序产生了死锁。

完整代码:

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
//信号量头文件
#include <semaphore.h>

/*
使用信号量
编写生产者和消费者模型
总资源数为5的情况--->需要线程同步
*/

//生产者的信号量
sem_t semp;
//消费者信号量
sem_t semc;

//创建一个互斥锁
pthread_mutex_t mutex;

//链表节点类型
struct Node
{
int number;
struct Node* next;
};

//头节点
struct Node *head = NULL;

//生产者回调函数
void *producer(void *arg)
{
while (1)
{
//检测生产者是否有资源---是否有空闲的生产者线程
sem_wait(&semp);//资源>0就不会被阻塞---生产者线程会占用一个资源-1
//上锁
pthread_mutex_lock(&mutex);
//上锁和sem_wait()两句代码顺序不能乱


//创建新节点
struct Node *newnode = malloc(sizeof(struct Node));
//初始化节点
newnode->number = rand() % 1000;
newnode->next = head;
head = newnode;
printf("生产者,id:%ld , number:%d\n", (pthread_self() % 100), newnode->number);
//解锁
pthread_mutex_unlock(&mutex);
//通知消费者消费---消费者资源+1
sem_post(&semc);
sleep(rand() % 3);//休眠控制生产速度
}
return NULL;
}

//消费者回调函数
void *consumer(void *arg)
{
while (1)
{
//消费者尝试消费,若有资源则消费者资源-1(任务队列中是否有多余的任务)
sem_wait(&semc);
//上锁
pthread_mutex_lock(&mutex);
struct Node *node = head;
printf("消费者,id:%ld , number:%d\n", (pthread_self() % 100), node->number);
head = head->next;
free(node);
//解锁
pthread_mutex_unlock(&mutex);
//通知生产者生产
sem_post(&semp);
sleep(rand() % 3);
}
return NULL;
}


int main()
{
//初始化互斥锁
pthread_mutex_init(&mutex, NULL);
//初始化信号变量
//1.生产者
sem_init(&semp, 0, 5);//生产者总共的资源数为5--->为了不造成数据的混乱还是需要线程同步--->需要互斥锁的帮助(锁在临界区)
//2.消费者 -> 资源初始化为0 消费则线程启动就阻塞
sem_init(&semc, 0, 0);


//创建生产者消费者线程ID
pthread_t t1[5], t2[5];
//创建生产者线程
for (int i = 0; i < 5; i++)
{
pthread_create(&t1[i], NULL, producer, NULL);
}
//创建消费者线程
for (int i = 0; i < 5; i++)
{
pthread_create(&t2[i], NULL, consumer, NULL);
}

//线程资源回收
for (int i = 0; i < 5; i++)
{
pthread_join(t1[i], NULL);
pthread_join(t2[i], NULL);
}

//销毁资源释放
pthread_mutex_destroy(&mutex);
//信号量的销毁
sem_destroy(&semp);
sem_destroy(&semc);

return 0;
}

运行结果

image-20231219223407481