线程安全:同步(条件变量)加互斥(锁)

线程安全

多个线程同时运行,访问资源,不会导致程序的结果产生二义性

临界资源:在同一时刻,该资源只能被一个线程(执行流所访问)

访问:在临界区当中对临界资源进行非原子操作

 

如何保证我们写出来的代码是线程安全的

互斥:保证在同一时刻只能有一个执行流访问临界资源

同步:保证程序对临界资源的访问

互斥:

互斥锁:

使用互斥锁来保证互斥属性

互斥锁的底层是互斥量,互斥量本质上是一个计数器,该计数器只有两个取值0或者1

0代表,无法获取互斥锁,进而表示需要访问的临界资源不可以被访问

1代表,可以获取互斥锁,进而表示需要访问的临界资源可以被访问

 

 

加锁:对于互斥锁当中的互斥量保存的计数器进行减1操作

解锁:对于互斥锁当中的互斥量保存的计数器进行加1操作

 

互斥量计数器本身也是一个变量,这个变量的取值是0或者1,对于这样的一个变量进行加1或者减1操作的时候,会是原子性的吗?

保证原子性,会将寄存器当中的值变成0

加锁的时候将寄存器当中的值和内存当中的值进行互换,互换的指令为xchgb,而这种互换操作是原子性的,一步完成

当交换完毕之后,需要对寄存器当中的当中的值进行判断

如果寄存器当中的值为0,则表示不能进行加锁,意味着当前加锁操作会被阻塞,从而不能访问临界资源

如果寄存器当中的值为1,则表示可以加锁,对计数器进行减1操作之后,会得到锁资源,并且加锁操作返回,从而去访问临界资源。

总结:

当计数器的值为1的时候,表示可以加锁操作,当寄存器和内存的值进行一步交换之后,相当于给计数器进行减1操作了

当计数器的值为0的时候,表示不可以加锁,当寄存器和内存的值进行一步交换之后,并没有对计数器当中的值做到真实修改,当前的执行流就被挂起来等待

 

 

互斥锁的使用流程

1.定义互斥锁:

互斥锁变量类型 pthread_mutex_t

2.初始化互斥锁

动态:需要进行销毁

int pthread_mutex_init(pthread_mutex_t * restrict mutex, const pthread_mutexattr_t * restrict attr);

restrict mutex:互斥锁的变量,传参的时候传入互斥锁变量的地址

restrict attr :互斥锁的属性,一般情况下,我们不关心,采用默认的属性,传值为NULL就可以了

静态:不需要进行销毁

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutexattr_t 本身也是一个结构体类型, PTHREAD_MUTEX_INITIALIZER宏定义是一个结构体的值,使用这种初始方式可以直接填充 pthread_mutexattr_t 这个结构体

3.加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

mutex:传入互斥锁变量的地址,来进行加锁操作

如果使用接口进行加锁操作的时候:

如果计数器当中的值为1,意味着可以加锁,加锁操作之后,对计数器当中的1改变成0

如果计数器当中的值为0,意味着不可以加锁,该接口阻塞等待,执行流就不会往下继续执行了

 

int pthread_mutex_trylock(pthread_mutex_t *mutex);

mutex:传入互斥锁变量的地址,来进行加锁操作

如果使用该接口进行加锁操作的时候:

如果计数器当中的值为1,意味着可以加锁,加锁操作之后,对计数器当中的1改变为0

如果计数器当中的值为0,意味着不可以加锁,该接口直接返回,不进行阻塞等待,返回为EBUSY(拿不到互斥锁)

一般在使用trylock这样的接口进行加锁的时候,一般操作那个循环加锁的方式,防止由于拿不到锁资源,而直接返回。从而可以从代码当中直接访问临界资源,从而导致程序产生二义性的结果

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);

带有超时时间的加锁接口

restrict mutex:传入互斥锁变量的地址,来进行加锁操作

restrict abs_timeout :加锁超时间,当加锁的时候,超过加锁的超时时间之后,还没有获取的互斥锁,则报错返回,不进行阻塞等待,返回ETIMEOUT

struct timespec有两个变量,第一个变量代表秒,第二个变量代表纳秒

 

4.解锁

 

int pthread_mutex_unlock(pthread_mutex_t *mutex);

不管是加锁操作的三个函数哪个进行的枷锁,都可以使用 pthread_mutex_unlock进行解锁

 

5.销毁互斥锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥锁销毁接口,如果使用互斥锁完成之后,不调用销毁接口,就会造成内存泄漏

 

注:

  1. 互斥锁定义为全局变量
  2. 在创建线程之前进行初始化
  3. 在线程退出之后进行销毁
  4. 在访问临界资源之前进行加锁操作
  5. 在所有可能退出的地方都需要解锁(如果一个执行流进行加锁之后,该执行流直接退出掉,会导致其他想要获取该互斥锁的执行流卡死:死锁的现象之一)
  6. 如果加锁了但是没有进行解锁,会造成所有想要加该互斥锁的执行流陷入阻塞等待(死锁)。

 

 

死锁:

什么是死锁:

程序当中有一个执行流没有释放锁资源,会导致其他想要获取该互斥锁的执行流陷入阻塞等待,这种情况称之为死锁

程序当中,每一个执行流都占有一把互斥锁,但是由于各个执行流在占有互斥锁的情况下,还想申请其他的锁,这种情况下称之为死锁

 

死锁的4个必要条件:

  1. 互斥条件:每一把锁只能被一个执行流所占用
  2. 请求与保持条件:当前执行流已经占用了一个互斥锁,还想去申请新的互斥锁
  3. 循环等待条件:若干个执行流在请求锁资源的情况下,形成了一个闭环
  4. 不可剥夺:只有当前拿着互斥锁的线程可以释放该互斥锁

 

避免死锁的方法

  • 破坏必要条件
  • 加锁顺序一致
  • 避免锁不被释放
  • 一次性分配资源:对于执行流在完成某件事请的时候,需要的资源,一次性分配股,防止需要的资源被其他加锁的线程所占用,导致该执行流阻塞

 

 

同步

保证了各个执行流对临界资源访问的合理性

同步实现的事情:

        当有资源的时候,可以直接去获取资源,

        没有资源的时候,线程进行等待,等待另外的线程生产一个资源,当生产完成一个资源的时候,通知等待的进程

 

条件变量: 本质 两个接口(等待接口+唤醒接口)+PCB等待队列

 

条件变量的接口

1.定义条件变量

pthread_cond_t 条件变量类型

2.初始化条件变量

动态:需要调用销毁接口

int pthread_cond_destroy(pthread_cond_t *cond);

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

restrict cond:传入条件变量的变量的地址

restrict attr:条件变量的属性,一般设置为NULL,采用默认属性

静态:不调用销毁接口

pthread_cond_t cond = PTHREAD_COND_INITIALIZER

3.等待接口:将调用该等待接口的执行流 放到PCB等待队列当中去,进行等待

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

restrict cond:传入条件变量的变量的地址

restrict mutex:传入互斥锁变量的地址

4.唤醒接口:通知PCB等待队列当中的执行流来访问临界资源

int pthread_cond_broadcast(pthread_cond_t *cond);

唤醒PCB等待队列当中所有的线程

int pthread_cond_signal(pthread_cond_t *cond);

唤醒至少一个PCB等待队列当中的线程


注:

 pthread_cond_wait接口为什么需要使用互斥锁?

如果单纯的同步,不保证互斥,也就意味着不同的执行流可以在同一时刻去访问临界资源,所以在条件变量当中需要使用互斥锁来保证互斥属性,保证各个执行流在同一时刻访问临界资源的时候,只有一个执行流在访问。

条件变量当中的互斥锁也是同样的道理,保证了各个执行流在访问临界资源的时候,只有一个执行流可以访问。从条件变量的使用角度而言,完成了生产线程和消费线程之间的互斥。

在pthread_cond_wait函数当中的实现逻辑,如何使用互斥锁?

  1. 将调用者的PCB放到PCB等待队列当中去
  2. 对互斥锁进行解锁操作
  3. 等待其他执行流通知PCB等待队列,被唤醒之后,移出PCB等待队列当中,进而进行抢锁操作
  •                     抢到互斥锁(从函数中返回,顺序执行代码逻辑)
  •                     没有抢到(卡在pthread_cond_wait 函数内的抢锁逻辑),当获取到时间片的时候,从上下文信息和程序计数器                      当中恢复上一次执行的场景,继续执行抢互斥锁的逻辑

pthread_cond_wait函数实现逻辑当中为什么需要将调用者PCB先放到PCB等待队列当中才释放互斥锁?

以消费者生产者模型为例

防止消费者线程先释放互斥锁,被生产者线程抢在消费者线程进入PCB等待队列之前拿到互斥锁,并且执行生成逻辑后通知PCB等待队列,而这个时候,再将消费者线程放到PCB等待队列当中,就可能造成没有生产者线程再去调用pthread_cond_signal/broadcast这样的接口来唤醒PCB等待队列了

 

你可能感兴趣的:(Linux)