linux 之线程基础 (三)、多线程编程的同步和互斥

linux 之线程基础 (三) 多线程编程的同步和互斥

  • 线程机制的优点:线程间很容易进行通信,通过全局变量实现数据共享和交换。
  • 线程机制缺点:多个线程同时访问共享对象时需要引入同步和互斥机制。

1. 同步与互斥的基本概念

1.1 同步

在多任务的操作系统环境下,多个进程/线程会同时运行。多个任务可能为了完成同一个目标会相互协作,按一定规则有序运行,这样就形成了任务之间的同步关系

同步概念:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。

1.2 互斥

同样,在不同任务之间为了争夺有限的资源(硬件或者是软件资源)会进入竞争状态,这就是任务之间的互斥关系

互斥:是指同时只允许一个访问者对临界资源进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

1.3 临界资源

任务之间出现互斥和同步关系存在的根源在于临界资源。临界资源是指同一时间只能被一段指令序列所占用的资源就是所谓的临界资源。 stdout 就是临界资源。
临界资源举例子:

  • stdin
  • stdout
  • 打印机

上述资源,同一个时刻只能被一个线程占用,否则会造成打印紊乱。这类资源如果不被看成临界资源加以保护,那么很有可能因为访问冲突造成数据错乱的问题。
线程间机制,多线程共享同一个进程的地址空间

1.4 临界区

程序内访问临界资源的代码序列被称为临界区。

2. 实现同步和互斥的方式

2.1 实现同步

我们可以通过信号量的机制,来实现线程的同步,以达到控制线程执行顺序的目的。

2.2 实现互斥

由于同步的基础是在互斥上的,因此,信号量的机制也可以实现互斥。但是,实现互斥还有一种方式,就是互斥锁的方式

  • 通过信号量 实现互斥
  • 通过互斥锁 实现互斥

3. 使用互斥锁实现线程间的互斥

3.1 互斥锁简介

我们对线程的互斥主要介绍NPTL库自带的 pthread_mutex_t,该互斥锁有一下特点:

  • 引入互斥(mutual exclusion)锁的目的是用来保证共享数据操作的完整性。
  • 互斥锁主要用来保护临界资源。
  • 每个临界资源都由一个互斥锁来保护,任何时刻最多只能有一个线程能访问该资源。
  • 线程必须先获得互斥锁才能访问临界资源,访问完临界资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止

3.2 互斥所的两种状态

互斥锁只有两种状态:上锁和解锁,可以将互斥锁看成某种意义上的全局变量。在同一时刻只能有一个线程拥有互斥锁,拥有互斥锁的线程才能能够对共享资源进行操作。若线程对一个已经被上锁的互斥锁加锁时,该线程就会睡眠,直到其他线程释放互斥锁位置。

注意:
上锁和解锁的操作是原子的。所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。互斥只能保证同一时刻只有一个人获得锁,但是不保证线程获得锁的顺序。

4. 互斥锁API

4.1 初始化互斥锁

需要首先定义一个全局变量的 互斥锁变量,互斥锁变量的类型为pthread_mutex_t。

4.1.1 动态初始化互斥锁
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);
//restrict 的意思是 这块区域只能这一个指针访问,这是建议性的 不是强制的。
  • 函数参数
    mutex: 互斥锁
    attr: 互斥锁的属性(一般置NULL表示缺省属性)
  • 返回值
    成功: 0
    失败 : 错误码
4.1.2 静态初始化互斥锁
//静态初始化可以通过PTHREAD_MUTEX_INITIALIZER宏
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

4.2 获得互斥锁

4.2.1 函数原型及功能
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);

功能: 获得互斥锁。

4.2.2 函数参数
  • mutex :初始化的互斥锁变量
4.2.3 函数函数返回值
  • 成功 获取成功返回0
  • 失败 获取失败返回错误码

注意:
如果想获得的锁已经被别的线程获取了,此时pthread_mutex_lock将引起调用者阻塞。

4.3 释放互斥锁

4.3.1 函数原型 及功能
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);

函数功能: 释放互斥锁

4.3.2 函数参数:
  • mutex : 需要释放的互斥锁
4.3.3 返回值 :
  • 成功 : 释放成功返回0
  • 失败 :释放失败返回错误码

4.4 销毁互斥锁

4.4.1 函数原型及功能
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);

功能:在不需要时应调用pthread_mutex_destroy销毁

4.4.2 函数参数:
  • mutex: 互斥锁
4.4.3 返回值
  • 成功 : 释放成功返回0
  • 失败 :释放失败返回错误码

5. 信号量机制简介

5.1 信号量概念

信号量是用来解决进程/线程之间的同步与互斥问题的一种通信机制,它表示代表某一类资源,其值表示系统中该资源的数量(可用的资源的数目)。

信号量机制 根据使用,既可以实现简单互斥,也可以实现顺序的同步。

5.2 信号量的三种操作

信号量是一个受保护的变量,只能通过以下三种操作来访问。

5.2.1 信号量初始化

初始化就是指定某类共享资源初始可以用的(资源的个数)。

5.2.2 P操作(申请资源)
if (信号量的值 > 0) {
	申请资源的任务继续;
	信号量值--} else {
	申请资源的任务阻塞
}
5.2.3 V操作(释放资源)
if(没有任务在等待该资源)
{
    信号量的值加一;
}else{
    唤醒第一个等待的任务,让其继续运行;
}

注意:

  • 一个任务申请资源时有可能被阻塞,一个任务释放资源时一定不会被阻塞。
  • 上述伪码中的加或者减是由执行 P 操作和V操作的函数来完成的,并不是我们手动书写代码。也就是P操作会使得信号量的资源减少一个。V操作会使得信号量的资源增加一个。

5.3 信号量机制的分类

posix中定义了两类信号量

  • 无名信号量 *(线程间的同步互斥)/ 进程间的同步互斥(优点限制,需要用到共享内存)
  • 有名信号量 应用场景:需要指定一个与它关联的文件名,一般用于进程间同步与互斥,也可以用于线程间。每那么多限制。

5.4 无名与有名信号量的比较

  • 有名信号量必须指定一个相关联的文件名称,这个name通常是文件系统中的某个文件;无名信号量不需要指定名称。
  • 有名信号量既可用于线程间的同步,又能用于进程间的同步;无名信号量通过shared参数来决定是进程内还是相关进程间共享。
  • 有名信号量是随内核持续的,一个进程创建一个信号量,另外的进程可以通过该信号量的外部名(创建信号量使用的文件名)来访问它。进程结束后,信号量还存在,并且信号量的值也不会改动。
  • 无名信号量的持续性却是不定的:如果无名信号量是由单个进程内的各个线程共享的,那么该信号量就是随进程持续的,当该进程终止时它也会消失。如果某个无名信号量是在不同进程间同步的,该信号量必须存放在共享内存区中,只要该共享内存区存在,该信号量就存在。

6. POSIX 无名信号量API

6.1 信号量初始化

需要首先定义一个或多个信号量类型,信号量的类型为sem_t。

6.1.1 函数原型及功能
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

函数功能: 初始化信号量。需要先定义一个全局变量,信号量的数据类型是sem_t。

6.1.2 函数参数
  • sem : 无名信号量
  • pshared : 该参数 决定信号量是由进程内线程共享,还是由进程之间共享。 (0:表示在当前进程下的线程之间使用;非0表示在进程间共享)
  • value :信号量的初始值(即资源的个数)
6.1.3 函数返回值
  • 成功 :初始化成功返回0
  • 失败 :初始化失败返回-1,并设置出错码

6.2 p操作(申请资源)

6.2.1 函数原型及功能
#include <semaphore.h>
int sem_wait(sem_t *sem);

功能:sem_wait的作用是以原子操作的方式给信号量的值减1,但它会等到信号量非0时才会开始减法操作。如果此时信号量的值为0,这个函数就会等待(线程阻塞),直到有线程增加了该信号量的值使其不再为0。

6.2.2 函数参数
  • sem:初始化来的 信号量
6.2.3 函数返回值
  • 成功 :获取成功返回0
  • 失败 :获取失败返回-1,并设置出错码

6.3 v操作

6.3.1 函数原型及功能
#include <semaphore.h>
int sem_post(sem_t *sem);

功能:sem_post的作用是以原子操作的方式给信号量的值加1。

6.3.2 函数参数
  • sem:初始化来的 信号量
6.3.3 函数返回值
  • 成功 :释放成功返回0
  • 失败 :释放失败返回-1,并设置出错码

6.4 信号量的销毁

6.4.1 函数原型及功能
#include <semaphore.h>
int sem_destroy(sem_t *sem);

功能:信号量销毁。无名信号量会随着进程的终结,自动销毁。在不终止进程的情况下,销毁信号量可以使用sem_destroy;

注意:
无名信号量如果每销毁的话会随着进程的终止而自动销毁。
有名信号量,如果不释放的话会一直存在。

6.4.2 函数参数
  • sem:初始化来的 信号量
6.4.3 函数返回值
  • 成功 :销毁成功返回0
  • 失败 :销毁失败返回-1,并设置出错码

7. 信号量实现同步和互斥的原理

7.1 实现互斥

信号量如用于互斥,几个进程/线程往往只设置一个信号量。如果用于同步操作时,往往会设置多个信号量,并设置不同的初始值实现它们之间的顺序执行。
在这里插入图片描述使用信号量实现互斥的原理如上图,其与互斥锁的实现在使用上非常相似。需要注意是线程A进行P操作还是线程B先进行P操作是不可知的,图示只是其中的一种情况。

7.2 实现同步

信号量如果用于同步操作时,往往会设置多个信号量,并设置不同的初始值实现它们之间的顺序执行。
在这里插入图片描述
同步的示意图的顺序不是随机的,而是固定的。

总结

  • 互斥锁和信号量都需要定义成全局变量。
  • 互斥锁 信号量都可以看成一种数据类型
posted @ 2020-03-28 18:15  江南又一春  阅读(662)  评论(0编辑  收藏  举报