前言
线程互斥与同步是多线程编程中的两个重要概念,用于解决多线程环境下的共享资源访问问题,避免出现竞态条件和确保线程的正确协作
一.锁
线程互斥的目的是防止多个线程同时访问共享资源,从而避免竞态条件导致的数据不一致问题,我们先来看几个概念
- 临界资源: 多线程执行流共享的并且被保护的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
线程会共享进程的虚拟地址空间, 当一个线程对一个共享资源进程修改时, 如果另一个线程也正在访问该数据, 就会出现数据不一致的问题, 我们可以使用锁来保护共享资源, 使得成为临界资源
我们先来介绍几个常用锁
互斥锁
互斥锁可以看作一把钥匙, 只有拿到这个钥匙的线程才能访问临界区, 其他线程就只能等待上一个线程访问完归还钥匙后竞争钥匙
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,线程也有自己的硬件上下文, 线程申请锁, 可以看作线程将一个公共的唯一的资源私有到线程的硬件上下文中了,释放锁就是线程将这个资源归还外界,下面是一个简单的互斥锁伪代码
lock:
movb $0,al
xchgb %al,mutex//交换锁和寄存器的数据(是原子的)
if(al寄存器的内容>0){
return0;
}else
挂起等待,
goto lock;
unlock:
movb $1,mutex
唤醒等待Mutex的线程;
return0;
pthread库已经提供了相应接口
创建互斥锁
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- pthread_mutex_t : 标识一个锁
- restrict : 输出型参数, 要初始化的锁的指针
- attr : 锁的属性,NULL表示默认
- PTHREAD_MUTEX_INITIALIZER : 创建全局锁使用的初始化
销毁互斥锁
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- mutex : 要销毁的锁
加锁/解锁
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- lock : 申请锁,阻塞式申请
- trylock : 尝试申请锁,会立即返回
- unlock : 释放申请的锁
自旋锁
线程在获取锁的过程中会反复检查锁的状态,而不是进入休眠或被挂起。这种反复检查的行为称为自旋,pthread库也提供了自旋锁
创建/销毁
#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
- pshared : 自旋锁属性
– PTHREAD_PROCESS_PRIVATE : 在进程内多个线程共享
– PTHREAD_PROCESS_SHARED : 跨进程共享 - 其他与互斥锁类似
加锁/解锁
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
与互斥锁使用类似
读写锁
允许多个线程同时读取共享数据,但在写数据时只允许一个线程进行写操作, 并且写入时其他线程不能读数据
创建/销毁
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加锁/解锁
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//读写锁的接口是分开
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态
死锁四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放(比如申请了锁又申请同一把锁)
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
二.条件变量
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
- 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件
当线程在访问临界区的数据时, 可能会因为某个条件不满足而需要等待某个条件就绪, 如果直接在临界区等待, 就可能让后来的线程一直无法申请到锁, 产生死锁问题, 我们可以使用条件变量, 让其在该条件变量下等待, 同时释放自身持有的锁, 当某个条件满足, 就可以重新申请锁继续执行下去
创建/销毁
与互斥锁类似
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//全局条件变量初始化
等待
#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
- wait : 在指定条件变量下等待, 同时传递一个互斥锁
- timewait : 带有时间限制的等待, 如果超出时间限制会直接返回错误码
唤醒
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
- signal : 唤醒指定条件变量的任一个等待的线程
- broadcast : 唤醒指定条件变量的所有线程
下面举个使用条件变量的例子
void* thread_1(void*)
{
pthread_mutex_lock(&mutex);
while(resource_num>0)//假设等待一个全局的整形条件
{
wait_num++;
pthread_cond_wait(&cond, &mutex);
wait_num--;
}
...
//处理资源
...
--resource_num;
if(resource_num>0&&wait_num>0)//说明资源就绪但还有线程在条件变量等待
{
pthread_cond_signal(&cond);//唤醒
}
pthread_mutex_unlock(&mutex);
return nullptr;
}