重点1:锁

自旋锁

特点:当线程获取锁失败时,线程会一直处于忙等待状态,直到拿到锁。

使用场景:非抢占式内核中非常有用。用户层适用于不允许抢占的实时调度中。并且当==被锁住的代码执行时间很短时==。

注:因为在抢占式的内核或实时调度中,当一个线程获取自旋锁失败后会一直处于阻塞状态时,操作系统会将该线程挂起,让别的线程进行工作。

使用方法:

1
2
3
4
5
6
7
8
9
#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock,int pshared); // 初始化
// pashared表示进程共享属性,PTHREAD_PROCESS_SHARED表示自旋锁能被可以访问锁底层内存的线程所访问,即使这些线程不属于同一个进程;PTHREAD_PROCESS_PRIVATE表示自旋锁只能被初始化该锁的进程内部线程所访问。
int pthread_spin_destory(pthread_spinlock_t *lock); // 反初始化
// 加锁
int pthread_spin_lock(pthread_spinlock_t *lock); // 加锁
int pthread_spin_trylock(pthread_spinlock_t *lock); // 加锁
int pthread_spin_unlock(pthread_spinlock_t *lock); // 解锁
// 解锁

互斥锁

特点:当互斥锁加锁失败后,线程会释放CPU,给其他线程。

使用方法:

1
2
3
4
5
6
7
8
9
10
11
#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t * restrict mutex,const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destory(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);

缺点:互斥锁加锁失败,会从用户态陷入到内核态,让内核帮我们切换线程,虽然简化了使用锁的难度,但是==存在两次线程上下文切换的成本。==

  • 当线程加锁失败时,内核会把线程的状态从**[运行]状态设置为[睡眠状态]**,然后把CPU切换给其他线程运行。
  • 接着,当锁被释放时,之前的**[睡眠]状态的线程会变成[就绪]状态**,然后内核会在合适的时间,把CPU切换给该进程运行。
img

使用场景:==被锁住的代码执行时间很长时==

读写锁🔒

特点:读锁写锁两部分构成。如果只读取共享资源【读锁】加锁,如果要修改共享内存使用【写锁】加锁

使用场景:适用于能够明确区分读操作和写操作的场景。

工作原理:

  • 写锁没有被线程持有时,多个线程能够并发的持有读锁,这大大的提高了共享资源的访问效率,因此读锁是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。
  • 写锁被线程持有时,读线程获取读锁的操作会被阻塞,而其他写线程获取写锁的操作也会被阻塞

实现方式:根据锁的实现方式可以分为读优先锁写优先锁

  • 读优先锁:期望读锁能够被更多的线程获取,以便提高线程的并发性。例如,如下线程A获取了读锁,写线程B再获取写锁时,被阻塞,并且在后续的阻塞过程中,后续来的读线程C仍可以成功的获取读锁,最后直到线程A和线程C释放读锁后,写线程B才能获取写锁。 如下图所示:

    img
  • 写优先锁:优先写线程工作,当读进程A获取读锁后,写线程尝试获取写锁失败,处于阻塞。读线程C获取读锁时,==也获取失败,处于阻塞状态==。当读进程A执行完毕,释放读锁后,写线程B获取写锁。

img

读优先锁的缺点是可能会导致可能会使得写线程处于饥饿状态;而写优先锁的缺点是可能会导致读进程处于饥饿状态。

因此为了解决该问题,引入==【公平读写锁】:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁,这样读线程仍然可以并发,也不会出现饥饿现象。