【Linux】一篇文章搞定 多线程编程

1. 线程概念

线程的理解

  • Linux内核当中没有线程的概念,只有轻量级进程(LWP),线程是C库中的概念
  • 一个程序中只有一个main线程,可以有多个工作线程
  • 一个线程 是执行代码的 一个执行流
  • 多个线程 可以同步执行

线程的优点

  • 创建进程的耗费 高于 创建线程的耗费

进程与线程的对比:

  • 进程 是资源分配的基本单位

  • 线程 是调度的基本单位

  • 一个进程 至少拥有 一条线程

  • 线程共享进程的数据,但也拥有自己的一部分数据

  • 共享数据:

    文件描述符表 + 信号处理方式 + 当前工作目录 + 用户id + 组id

    独有数据:

    线程ID + 一组寄存器 + 调用栈 + errno + 信号屏蔽字(哪些信号该进程需要阻塞) + 调度优先级

进程与线程的现实比喻:

  • 进程可以比作一个工厂
  • 线程可以比作一个工厂内生产产品生产线
  • 一个工厂中生产产品的生产线可以有多条

线程与进程的组合图解:

在这里插入图片描述

2. 线程控制

2.1 POSIX线程库

  • 线程相关的的函数构成的函数库,绝大多数函数是以pthread_开头的
  • 要使用这些函数需要引入头文件pthread.h
  • 编译含有多线程函数的源文件时,要加上编译器命令-lpthread选项

2.2 线程创建

函数原型:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

功能:创建一个新的线程

参数:

  • pthread_t: 线程标识符,是一个输出型参数,从函数返回的
  • pthread_attr_t: 设置线程的属性,attr为NULL表示使用默认属性,一般不设置线程属性,传入NULL
  • start_routine : 线程启动函数指针,表示线程创建后需要执行什么函数
  • arg : 传给线程启动函数start_routine的参数

返回值:成功

线程创建程序演示

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* work_thread(void* arg)
{
    
                                       
  while(1)
  {
    
    
    printf("I'm a work thread!\n");
    sleep(1);
  }
  return NULL;
}
int main()
{
    
    
  pthread_t pid;
  pthread_create(&pid,NULL,work_thread,NULL);

  while(1)
  {
    
    
    printf("I'm a main thread!\n");
    sleep(1);
  }

  return 0;
}
[gongruiyang@localhost createMulThread]$ gcc create1.c -o test.c -lpthread
[gongruiyang@localhost createMulThread]$ ./test.c 
I'm a main thread!
I'm a work thread!
I'm a main thread!
I'm a work thread!
I'm a main thread!
I'm a work thread!
I'm a work thread!
I'm a main thread!
I'm a work thread!
I'm a main thread!
^C

2.3 线程终止

线程终止的方法:

  1. 线程入口函数中return返回
  2. pthread_exit(void*)函数,void*参数是线程退出的信息,是返回给等待线程的,可以传递也可以不传递,谁调用谁退出
  3. pthread_cancel(pthread_t)参数是一个线程标识符,想要取消哪个线程,就传递哪个线程的标识符

测试pthread_exit程序

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* work_thread(void* arg)
{
    
    
  printf("1\n");
  pthread_exit(NULL);
  printf("2\n");
  //pthread_cancel(pthread_self());
  printf("3\n");                                               
  return NULL;
}

int main()
{
    
    
  pthread_t pid;
  int ret_create = pthread_create(&pid,NULL,work_thread,NULL);
  if(ret_create < 0)
  {
    
    
    perror("pthread_create");
    return -1;
  }
  while(1)
  {
    
    
    printf("main_thread:%d\n",pthread_self());
    sleep(1);
  }

  return 0;
}
[gongruiyang@localhost createMulThread]$ ./test
main_thread:1703049024
1
main_thread:1703049024
main_thread:1703049024
main_thread:1703049024
main_thread:1703049024
main_thread:1703049024
^C
  • pthread_exit函数会立即退出该进程,后续代码不再执行

测试pthread_cancel函数

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* work_thread(void* arg)
{
    
    
  printf("1\n");
  //pthread_exit(NULL);
  printf("2\n");
  pthread_cancel(pthread_self());
  printf("3\n");                                               
  return NULL;
}

int main()
{
    
    
  pthread_t pid;
  int ret_create = pthread_create(&pid,NULL,work_thread,NULL);
  if(ret_create < 0)
  {
    
    
    perror("pthread_create");
    return -1;
  }
  while(1)
  {
    
    
    printf("main_thread:%d\n",pthread_self());
    sleep(1);
  }

  return 0;
}
[gongruiyang@localhost createMulThread]$ ./test
main_thread:2103330624
1
2
3
main_thread:2103330624
main_thread:2103330624
main_thread:2103330624
^C
  • pthread_cancel函数不会立即退出指定进程,而是需要一段时间才能退出

2.4.1 退出不当僵尸进程的产生

  • 主线程创建完工作线程后立即终止,导致主线程变成僵尸进程,其资源未被回收
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* work_thread_task(void* arg)
{
    
    
  while(1)
  {
    
    
    printf("I'm a work thread : %d\n",pthread_self());
    sleep(1);
  }
  return NULL;
}
int main()
{
    
    
  pthread_t pid;
  int ret_create = pthread_create(&pid,NULL,work_thread_task,NULL);
  if(ret_create < 0)
  {
    
    
    perror("pthread_create");
    return -1;
  }
  pthread_exit(NULL);                                                     
  return 0;
}
[gongruiyang@localhost ~]$ ps -aux | grep test
gongrui+   8516  0.0  0.0      0     0 pts/0    Zl+  17:23   0:00 [test] <defunct>
gongrui+   8530  0.0  0.0 112828   980 pts/1    R+   17:24   0:00 grep --color=auto test

[gongruiyang@localhost ~]$ top -H -p 8516
top - 17:26:32 up  4:31,  3 users,  load average: 0.04, 0.04, 0.05
Threads:   2 total,   0 running,   1 sleeping,   0 stopped,   1 zombie
%Cpu(s):  0.0 us,  0.7 sy,  0.0 ni, 99.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  3861272 total,  2015232 free,   916552 used,   929488 buff/cache
KiB Swap:  4063228 total,  4063228 free,        0 used.  2675052 avail Mem 
   PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND                             
  8516 gongrui+  20   0       0      0      0 Z  0.0  0.0   0:00.00 test                                 
  8517 gongrui+  20   0       0      0      0 S  0.0  0.0   0:00.00 test                                 

主进程pid为8561,进程状态为Z,即僵尸进程

工作进程pid未8517,进程状态为S,即正常运行

2.4 线程等待

  • 线程在创建出来的时候,属性默认是joinable属性,意味着线程在退出的时候需要其他执行流(线程)来回收线程的资源

线程等待函数接口

int pthread_join(pthread_t thread, void **retval);

功能:若线程A调用了该函数等待B线程,A线程会阻塞,直到B线程退出后,A线程才会解除阻塞状态

参数:

  • pthread_t : 线程标识符,要等待哪一个线程,就传递哪个线程的标识符
  • retval : 保存的是一个常数,
线程退出方式 *retval保存的定西
return 入口函数返回值
pthread_exit函数 pthread_exit函数参数
pthread_cancel函数 PTHREAD_CANCEL宏定义
( #define PTHREAD_CANCEL (void*) -1 )

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

程序演示

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define THREAD_COUNT 4

void* work_thread_task(void* arg)
{
    
    
  sleep(1);
  printf("work thread:%d\n",pthread_self());
    //线程退出
  pthread_exit(NULL);
}
int main()
{
    
    
  pthread_t pid[THREAD_COUNT];
    
  //创建线程
  for(int i = 0; i < THREAD_COUNT; i++)
  {
    
    
    int ret_create = pthread_create(&pid[i],NULL,work_thread_task,NULL);
    if(ret_create < 0)
    {
    
    
      perror("pthread_create");
      return -1;
    }
  }
    
  //阻塞在join中等待线程退出
  for(int i = 0 ; i < THREAD_COUNT; i++)
    pthread_join(pid[i],NULL);

  while(1)
  {
    
    
    printf("main thread : %d\n",pthread_self());
    sleep(1);
  }

  return 0;                                                               
}
[gongruiyang@localhost wait]$ gcc wait.c -o test -lpthread -std=gnu99
[gongruiyang@localhost wait]$ ./test 
work thread:-1124186368
work thread:-1149364480
work thread:-1140971776
work thread:-1132579072
main thread : -1115846848
main thread : -1115846848
main thread : -1115846848
main thread : -1115846848
main thread : -1115846848
^C

2.5 分离线程

  • 分离线程是将线程标记成已分离,其属性从joinable变成detach,对于detach属性的线程终止后,系统会自动回收其资源

分离线程函数接口

int pthread_detach(pthread_t thread);

功能:将线程标记为已分离,目的是当分离的线程终止时,其资源会自动释放防止产生僵尸进程,防止内存泄漏

参数:

  • pthread_t :需要标记分离的线程标识符
  • 调用pthread_detach函数的位置可以是:
    1. 在主线程中调用分离创建出来的线程,即主线程标记分离工作线程;
    2. 在工作线程的线程入口函数中调用,即自己标记分离自己;
  • 线程分离的实质就是将线程的属性设置为detach

程序演示

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define THREAD_COUNT 4

void* work_thread_task(void* arg)
{
    
    
    //线程分离自身
  pthread_detach(pthread_self());
  printf("work thread : %d\n",pthread_self());
  pthread_exit(NULL);
}

int main()
{
    
    
  pthread_t ptid[THREAD_COUNT];

  for(int i = 0; i < THREAD_COUNT; i++)
  {
    
    
    int ret_create = pthread_create(&ptid[i],NULL,work_thread_task,NULL);
    if(ret_create < 0)
    {
    
    
      perror("pthread_create");
      return -1;
    }
  }

  while(1)
  {
    
    
    printf("main thread : %d\n",pthread_self());
    sleep(1);                                                              
  }
  return 0;
}
[gongruiyang@localhost detach]$ ./test 
main thread : 1528891200
work thread : 1520551680
work thread : 1512158976
work thread : 1503766272
work thread : 1495373568
main thread : 1528891200
main thread : 1528891200
main thread : 1528891200
^C

2.6 pthread_t源码和pthread_attr_t源码

pthread_t源码

typedef unsigned long int pthread_t;    

pthread_attr_t源码

union pthread_attr_t
{
    
    
    char __size[__SIZEOF_PTHREAD_ATTR_T];
    long int __align;
};
#ifndef __have_pthread_attr_t
typedef union pthread_attr_t pthread_attr_t;
# define __have_pthread_attr_t  1
#endif

3. 线程安全

使用一个 抢票程序 演示线程安全的概念及重要性

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

// 2个线程共同抢票
#define THREAD_COUNT 2                                                      

// 100张票
int ticket = 100;

void* work_thread_task(void* arg)
{
    
    
   // 创建出来的线程只要 有票 ,就一直抢票
  while(1)
  {
    
    
    if(ticket > 0)
    {
    
    
      ticket--;
      printf(" %p get one! rest : %d\n",pthread_self(),ticket);
    }
    else
      break;
  }
    //票没了,线程退出
  return NULL;
}

int main()
{
    
    
  //线程标识符
  pthread_t ptid[THREAD_COUNT];
	
  //线程创建
  for(int i = 0; i < THREAD_COUNT; i++)
  {
    
    
    int ret_create = pthread_create(&ptid[i],NULL,work_thread_task,NULL);
    if(ret_create < 0)
    {
    
    
      perror("pthread_create");
      return -1;
    }
  }
	
  //主线程 等待子线程的退出,并回收他们的资源
  for(int i = 0; i < THREAD_COUNT; i++)
    pthread_join(ptid[i],NULL);

  return 0;
}

线程不安全的原因:同一时刻两个线程对同一个数据进行修改,导致程序结果出现二义性

详细解释:

  1. 假设同一个程序中有两个线程,分别为线程A和线程B,并且有一个int类型的全局变量,其值为10,线程A和线程B在各自的入口函数当中都对这样的一个全局变量进行++操作
  2. 当线程A拥有CPU之后,对全局变量进行++操作是非原子性的操作,也就意味着,这个操作随时可能会被打断,假设线程A刚刚将全局变量的数值10读取到CPU的寄存器中时就被切换出去了;(程序计数器中保存着线程A下一步要执行的指令,上下文信息中保存寄存器的值,这两个东西是为了当线程A再次拥有CPU使用权的时候,还原当时线程A切换出去的线程现场使用的)
  3. 当线程A被切换出去后,这会儿,可能线程B获得CPU时间片,对全局变量进行了++操作,此时全局变量从10变成了11,并且回写到了内存中去
  4. 当线程A再次拥有CPU时间片之后,恢复当时切换出去的现场,继续往下执行,由于上下文信息中保存的全局变量值仍然是10,执行完++操作后变成11,然后再写回内存中,全局变量的值仍然还是11
  5. 虽然,两个线程都对这个全局变量进行了++操作,但是从值上面来看,全局变量仅仅进行了一次++操作
  6. 这就是线程的不安全

4. 线程同步与互斥

互斥:保证各个线程对共享资源独占式访问

同步:保证各个线程对于共享资源访问具有合理性

  • 为了控制线程对共享资源独占式访问,并且访问次序具有合理性,有以下三种方式实现线程同步
    1. POSIX信号量
    2. 互斥量
    3. 条件变量

什么是对共享资源的独占式访问呢?

举一个栗子:同学们在上厕所的时候,坑位就是一个共享资源,同学们在上厕所时,将厕所门关上,就是对共享资源的独占式访问,在你上厕所的时候,别人进不来(无法访问),引申到代码中,就是相当:将对于一个变量的操作变成原子性操作,要么操作成功,要么没操作,不存在操作一半被打断的情况

4.1 POSIX 信号量

4.1.1 信号量概念

  • 信号量本质是 计数器 + PCB等待队列 + 函数接口
  • 计数器:对共享资源的计数
    • 当执行流获取信号量成功后,信号量当中的计数器会进行减一操作,当获取失败后,该执行流就会被放入PCB等待队列当中
    • 当执行流释放信号量成功之后,信号量当中的计数器会进行加一操作
  • PCB等待队列:用于存放等待信号量的线程
  • 函数接口:用于操作信号量的一组函数

4.1.2 信号量保证同步互斥的原理探究

  • 信号量不仅仅可以完成线程之间的同步与互斥,也可以完成进程之间的同步与互斥

互斥原理

  1. 初始化信号量后,信号量中的计数器保存的数值为1表示说只有一个资源可以被使用
  2. 当执行流A想要访问共享资源时,首先获取信号量,由于计数器中的值为1,表示可以访问,执行流A获取到信号量后,计数器的值从1变成0,从而执行流A去访问共享资源
  3. 此时,执行流B想要去访问共享资源,执行流B首先得去获取信号量,但是信号量中的计数器中的值为0,表示无法获取该信号量,进而无法访问共享资源,因此,执行流B的PCB被放进了PCB等待队列当中,等待目标信号量的释放,同时信号量当中的计数器的值进行减一操作计数器中的值变成了-1,这里的-1表示当前还有1个执行流在等待访问共享资源

同步原理

  1. 当执行流想要访问共享资源的时候,首先需要获取信号量
  2. 如果信号量中的计数器大于0,则表示够获取信号量,并且访问共享资源
  3. 如果信号量中的计数器小于或等于0,则表示不能获取信号量,并且无法访问共享资源,该执行流被放入PCB等待队列中,同时计数器进行减一操作
  4. 释放信号量的时候,会对信号量中的计数器进行加一操作
  5. 如果信号量中的计数器大于0,则唤醒PCB等待队列中的线程
  6. 如果信号量中的计数器小于或等于0,则不唤醒PCB等待队列中的线程

4.1.3 信号量相关函数

  • POSIX信号量的函数的名字都是以sem_开头,常用的POSIX信号量函数有以下这些
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);

int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, struct timespec*abs_timeout);
  • sem_t是一个信号量共用体

sem_t源码

typedef union
{
    
    
    char __size[__SIZEOF_SEM_T];
    long int __align;
} sem_t;

4.1.3.1 初始化信号量函数

int sem_init(sem_t *sem, int pshared, unsigned int value);

功能:初始化一个信号量

参数:

  • sem:指向被操作的信号量,传入信号量的地址
  • pshared:表示该信号量是用于进程间的还是用于线程间的,填入以下的值
数值 含义
0 用于线程间
非0 用于进程间
  • value:资源的个数,本质上就是计数器的值

4.1.3.2 等待信号量函数

int sem_wait(sem_t *sem);

功能:执行流调用该函数后,将会对计数器进行减一操作

  1. 如果说:减一操作后,计数器的值大于0,表示其他执行流仍然可以访问共享资源

  2. 如果说:减一操作后,计数器的值等于0,表示该执行流可以访问共享资源,其他执行流若想访问需要进入PCB等待队列

  3. 如果说:减一操作后,计数器的值小于0,表示当前执行流需要进入PCB等待队列,其他执行流若想访问也需要进入PCB等待队列

参数:

  • sem:指向被操作的信号量,传入信号量的地址

4.1.3.3 释放信号量函数

int sem_post(sem_t *sem);

功能:执行流调用该函数后,将会对计数器进行加一操作

参数:

  • sem:指向被操作的信号量,传入信号量的地址

4.1.3.4 销毁信号量函数

int sem_destroy(sem_t *sem);

功能:销毁目标信号量

参数:

  • sem:指向被操作的信号量,传入信号量的地址

4.2 互斥量 -> 保证互斥性

4.2.1 互斥量概念

  • 互斥量互斥锁内部的一个计数器,这个计数器的取值只能为0或1,可以用于保护关键代码,以确保其被独占式的访问
  • 互斥量为1的时候,代表线程可以获得该互斥锁,也就意味着可以访问到共享资源
  • 互斥量为0的时候,代表该互斥锁已经被其他线程占用,也就意味着除了获得互斥锁的线程之外的线程都无法访问到共享资源
  • 当进入关键代码段之前,我们需要获得互斥锁将关键代码段加锁,由于该关键代码段已被互斥锁锁上,其他线程如果没有该互斥锁,是无法访问该代码段的,只能等待锁被归还后,再拿到该代码段的互斥锁,执行该代码段
  • 关键代码段执行完毕之后,需要对关键代码段进行解锁归还互斥锁,以唤醒其他等待该互斥锁的线程

4.2.2 互斥量的实现原理探究

  • 前面我们说了,单纯的++操作并不是原子性操作,那么互斥量从0变成1和从1变成0是如何保证原子性的呢?

为了保证互斥锁操作,大多数体系结构都提供了swap和exchange指令,该指令作用是把寄存器和内存单元的数据相交换,由于只有一条指令,只会出现执行成功和未执行两种情况,所以是原子性的

图解原理:想要获得互斥锁并加锁

  1. 寄存器中的值赋为0
  2. 寄存器中的值和内存中的值进行交换
  3. 若寄存器中的值为1,表示该锁未被别的线程拿走,表示可以占用该锁
  4. 若寄存器中的值为0,表示该锁已被别的线程拿走,表示不可以占用该锁

在这里插入图片描述

4.2.2 POSIX互斥锁基础API

  • POSIX互斥锁的相关函数主要有以下5个
#include <pyhread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 这些函数的第一个参数mutex都是一个pthread_mutex_t结构体指针变量,指向要操作的目标互斥锁结构体
  • 参数attr是一个pthread_mutexattr_t结构指针变量,指向互斥锁属性结构体,如果填入NULL,表示使用默认属性

4.2.2.1 初始化互斥锁变量函数

4.2.2.1.1 动态初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

功能:初始化一个互斥锁结构体的属性

参数:

  • pthread_mutex_t* : 一个指向pthread_mutex_t结构体的指针变量
  • pthread_mutexattr_t* : 一个指向pthread_mutexattr_t结构的指针变量

返回值:成功返回0,失败返回errno

4.2.2.1.2 静态初始化
  • 使用宏PTHREAD_MUTEX_INITIALIZER来初始化一个互斥锁结构体,实际上该宏是将互斥锁的各个字段初始化为0
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

宏定义源码:

#define PTHREAD_MUTEX_INITIALIZER \
{ { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }

4.2.2.2 加锁函数

4.2.2.2.1 阻塞加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);

功能:给 关键代码 阻塞等待加锁

参数:

  • pthread_mutex_t* : 一个指向pthread_mutex_t结构体的指针变量

返回值:返回0表示成功;加锁失败返回errno

阻塞加锁解释:

如果mutex中互斥量的值为1,则pthread_mutex_lock函数就返回0表示加锁成功

如果mutex中互斥量的值为0,则线程阻塞在pthread_mutex_lock函数中,直到加锁成功

4.2.2.2.1 非阻塞加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);

功能:给 关键代码 非阻塞加锁

参数:

  • pthread_mutex_t* : 一个指向pthread_mutex_t结构体的指针变量

返回值:返回0表示成功;加锁失败返回errno

非阻塞加锁解释:

如果mutex中互斥量的值为1,则pthread_mutex_trylock函数就返回0表示加锁成功

如果mutex中互斥量的值为0,则pthread_mutex_trylock函数就返回错误码EBUSY

4.2.2.2.3 带有超时时间的加锁
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);

功能:如果不能立即获得目标互斥锁,则等待abs_timeout时间,如果在等待时间内加锁成功则直接返回,如果超过等待时间则直接返回表示加锁失败

4.2.2.3 解锁函数

int pthread_mutex_unlock(pthread_mutex_t *mutex);

功能:以原子操作的方式给一个互斥锁解锁,如果此时有其他线程正在等待这个互斥锁,则其他线程会获得该互斥锁

参数:

  • pthread_mutex_t* : 一个指向pthread_mutex_t结构体的指针变量

返回值:返回0表示解锁成功;解锁失败返回errno

将互斥锁中的互斥量从0变成1,表示其他线程可以获取该互斥锁了

4.2.2.4 销毁互斥锁函数

int pthread_mutex_destroy(pthread_mutex_t *mutex);

功能:将目标互斥锁销毁

参数:

  • pthread_mutex_t* : 一个指向pthread_mutex_t结构体的指针变量

返回值:销毁成功返回0;销毁失败返回errno

4.2.3 带互斥锁的抢票程序

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

#define THREAD_COUNT 4	//线程数量

//定义一个互斥锁
pthread_mutex_t lock_ticket;

int ticket = 100;	//票总量

void* work_thread_task(void* arg)
{
    
    
  while(ticket > 0)
  {
    
    
    //锁上互斥锁:在开始访问临界资源的地方加锁
    pthread_mutex_lock(&lock_ticket);
    if(ticket > 0)
    {
    
    
      printf("I'm : %p! rest : %d\n",pthread_self(),ticket);
      ticket--;
    }
    else
    {
    
    
      //解除互斥锁
      pthread_mutex_unlock(&lock_ticket);
      break;
    }
    //解除互斥锁
    pthread_mutex_unlock(&lock_ticket);                                              
  }

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

int main()
{
    
    
  //初始化互斥锁
  pthread_mutex_init(&lock_ticket,NULL);

  pthread_t ptid[THREAD_COUNT];

  //创建线程
  for(int i = 0; i < THREAD_COUNT; i++)
  {
    
    
    int ret_create = pthread_create(&ptid[i],NULL,work_thread_task,NULL);
    if(ret_create < 0)
    {
    
    
      perror("pthread_create");
      return -1;
    }
  }

  //等待工作线程退出并回收资源
  for(int i = 0 ; i < THREAD_COUNT; i++)
    pthread_join(ptid[i],NULL);
  //销毁互斥锁
  pthread_mutex_destroy(&lock_ticket);

  return 0;
}

4.3 条件变量 -> 保证同步性

  • 条件变量提供了一种线程间的通知机制:当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程
  • 条件变量本质是一个PCB等待队列

4.3.1 条件变量基础API

  • 条件变量的相关函数主要以下几个
#include <pthread.h>
int pthread_cond_init(pthread_cond_t * cond,pthread_condattr_t * attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

int pthread_cond_destroy(pthread_cond_t *cond);

int pthread_cond_timedwait(pthread_cond_t * cond, pthread_mutex_t * mutex, struct timespec * abstime);
int pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t * mutex);

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
  • pthread_cond_t条件变量结构体
  • pthread_condattr_t条件变量属性结构体
  • PTHREAD_COND_INITIALIZER是一个宏,用来初始化条件变量结构体的,本质是将条件变量各个字段设置为0

4.3.1.1 初始化条件变量函数

4.3.1.1.1 动态初始化
int pthread_cond_init(pthread_cond_t* cond, pthread_condattr_t* attr);

功能:初始化条件变量结构体

参数:

  • pthread_cond_t : 条件变量结构体指针
  • pthread_condattr_t : 条件变量属性结构体指针,常传入NULL,使用默认的属性
pthread_cond_t cond;
pthread_cond_init(&cond,NULL);
4.3.1.1.2 静态初始化
  • 使用宏PTHREAD_COND_INITIALIZER初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

4.3.1.2 销毁条件变量函数

int pthread_cond_destroy(pthread_cond_t *cond);

功能:销毁一个条件变量

参数:

  • pthread_cond_t : 条件变量结构体指针
pthread_cond_destroy(&cond)

4.3.1.3 等待条件变量函数

int pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t * mutex);

功能:将调用该函数的线程放入PCB等待队列

参数:

  • pthread_cond_t : 条件变量结构体指针

  • mutex : 该线程等待的互斥锁

4.3.1.4 唤醒条件变量函数

4.3.1.4.1 单个唤醒
int pthread_cond_signal(pthread_cond_t *cond);

功能:唤醒一个等待目标条件变量的线程,至于唤醒的是哪个线程,取决于线程的优先级调度策略

参数:

  • pthread_cond_t : 条件变量结构体指针
4.3.1.4.2 广播唤醒
int pthread_cond_broadcast(pthread_cond_t *cond);

功能:以广播的方式唤醒所有等待目标条件变量的线程

参数:

  • pthread_cond_t : 条件变量结构体指针

4.3.2 为什么条件变量中用到了互斥锁?

  • 条件变量只能保证线程的同步性,但是无法保证线程的互斥性
  • 互斥锁只能保证线程的互斥性,但是无法保证线程的同步性
  • 将两个一起使用,既可以保证线程的同步性又可以保证线程的互斥性

4.4 具有线程同步性与互斥性的 消费者与生产者程序

  • 生产着生产一个,提醒消费者消费
  • 消费者消费完毕后提醒生产者生产
  • 进而,消费与生产达到平衡
#include <stdio.h>                                                                      
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

#define THREAD_COUNT 3
int resource = 1;

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t consumer_cond = PTHREAD_COND_INITIALIZER;
pthread_cond_t producer_cond = PTHREAD_COND_INITIALIZER;

void* consumer_task(void* arg)
{
    
    
  while(1)
  {
    
    
    pthread_mutex_lock(&lock);
    while(resource <= 0)
    {
    
    
      pthread_cond_wait(&consumer_cond,&lock);
    }
    printf("consumer thread : [%p], rest resource : [%d]\n",pthread_self(),resource);
    resource--;
    pthread_mutex_unlock(&lock);
    pthread_cond_signal(&producer_cond);
  }
  return NULL;
}
void* producer_task(void* arg)
{
    
    
  while(1)
  {
    
    
    pthread_mutex_lock(&lock);
    while(resource > 0)
    {
    
    
      pthread_cond_wait(&producer_cond,&lock);
    }
    printf("producer thread : [%p], rest resource : [%d]\n",pthread_self(),resource);
    resource++;
    pthread_mutex_unlock(&lock);
    pthread_cond_signal(&consumer_cond);
  }
  return NULL;
}


int main()
{
    
    
  pthread_t consumer[THREAD_COUNT],producer[THREAD_COUNT];
  for(int i = 0 ; i < THREAD_COUNT; i++)
  {
    
    
    //创建消费者线程
    int ret_cons = pthread_create(&consumer[i],NULL,consumer_task,NULL);
    if(ret_cons < 0)
    {
    
    
      perror("pthread_create");
      return -1;
    }
    //创建生产者线程
    int ret_prod = pthread_create(&producer[i],NULL,producer_task,NULL);
    if(ret_prod < 0)
    {
    
    
      perror("pthread_create");
      return -1;
    }
  }
  for(int i = 0 ; i < THREAD_COUNT; i++)
  {
    
    
    pthread_join(consumer[i],NULL);
    pthread_join(producer[i],NULL);
  }


  pthread_cond_destroy(&consumer_cond);
  pthread_cond_destroy(&producer_cond);
  pthread_mutex_destroy(&lock);

  return 0;
}

Q1:为什么先将线程放入PCB等待队列中后才释放锁?

A:如果先释放锁再进入PCB等待队列可能会出现以下情况:

生产者将锁释放后还没来得及进入PCB等待队列,此时,消费者已经拿到锁并消耗资源并唤醒PCB等待队列,但是由于生产者还未来得及入队,导致没有生产者被唤醒,在消费者唤醒之后,生产者再入队,此时出现,没有生产者生产,消费者无法消费 的局面

Q2:线程被唤醒后,需要做啥事?

A:从PCB等待队列当中移除出来,抢占互斥锁

情况1:拿到互斥锁,从pthread_cond_wait函数返回出来

情况2:没有抢占到互斥锁,阻塞在pthread_cond_wait函数内部

猜你喜欢

转载自blog.csdn.net/weixin_45437022/article/details/112404271