锁 同步 条件变量

处理多线程的数据二义性

概念

  • 互斥锁(mutex)是用于在线程之间保护共享资源的一种同步机制。它确保在同一时间只有一个线程可以访问共享资源,从而避免竞争条件和数据不一致问题

  • 锁本身就是一个共享资源(一个变量)。为了保证锁的安全,因此申请锁释放锁是原子的。

  • 加锁的本质,使用时间换安全。对于临界区代码串行执行。因此需要保证临界区代码的高效精简。

  • 在临界区中,线程也可以被切换。但是只要持有锁的进程没有释放锁,其他访问锁的线程都会被阻塞。因此对于其他线程来说,当前锁要么没有锁,要么释放锁,是原子的。

其他各种常见的锁

锁的类型 描述
悲观锁 在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
乐观锁 每次取数据时,总是乐观地认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他线程在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
CAS操作 当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
自旋锁(pthread_spin_lock) 如果访问临界资源线程的资源执行时间很短,线程在获取锁的时候不会立即阻塞,而是通过循环的方式不断尝试获取锁,这样可以减少线程上下文切换的开销。
公平锁 锁按照先来先得的顺序分配,确保线程获取锁的公平性。
非公平锁 锁不按照先来先得的顺序分配,可能导致某些线程长时间无法获取锁,从而提高锁的性能。

基本使用步骤

  1. 初始化互斥锁:在使用互斥锁之前,需要先进行初始化。

  2. 加锁:在访问共享资源之前,线程需要调用
    pthread_mutex_lock 函数对互斥锁进行加锁。

加锁
- 如果当前互斥锁已经被其他线程使用,那么再次调用pthread_mutex_lock线程会被阻塞,直到互斥锁可用

  1. 访问共享资源:在成功加锁后,线程可以安全地访问共享资源。
  2. 解锁:在访问完共享资源后,线程需要调用 pthread_mutex_unlock 函数对互斥锁进行解锁,以便其他线程能够获得互斥锁并访问共享资源。
  3. 销毁互斥锁:在不再需要使用互斥锁时,可以使用 pthread_mutex_destroy 函数销毁互斥锁,释放相关资源。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

pthread_mutex_t mutex; // 定义互斥锁
int counter = 0; // 共享资源

void* thread_func(void* arg) {
for (int i = 0; i < 1000000; ++i) {
pthread_mutex_lock(&mutex); // 加锁
counter++; // 访问共享资源
pthread_mutex_unlock(&mutex); // 解锁
}
return NULL;
}

int main() {
pthread_t thread1, thread2;

// 初始化互斥锁
if (pthread_mutex_init(&mutex, NULL) != 0) {
perror("pthread_mutex_init");
return 1;
}

// 创建两个线程
if (pthread_create(&thread1, NULL, thread_func, NULL) != 0) {
perror("pthread_create");
return 1;
}
if (pthread_create(&thread2, NULL, thread_func, NULL) != 0) {
perror("pthread_create");
return 1;
}

// 等待线程完成
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);

// 销毁互斥锁
pthread_mutex_destroy(&mutex);

printf("Final counter value: %d\n", counter);
return 0;
}

饥饿

如果锁的分配不够合理,容易导致进程的饥饿问题。

死锁

死锁是指在一组进程中的各个进程均占有不同的资源,但因互相申请被其他进程不会释放的资源,而导致的一种永久等待状态。

同步

数据安全的情况下,让我们的线程访问资源具有一定的顺序性。例如:一个线程想要访问一个资源,但是发现该资源已被占用,此时就将其放入到等待队列中。

同步方案

  1. Linux中的条件变量,通常和互斥锁一起使用。原因:在进行条件判断的时候,可能需要访问到共享资源(临界资源)因此需要用锁保护,故判断逻辑在加锁之后。
    • 条件变量是一种线程同步机制,它允许线程在某些条件满足之前等待,并在条件满足后被唤醒。
    • 有关条件变量的函数:
函数名 原型 描述
pthread_cond_init int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); 初始化条件变量。
pthread_cond_destroy int pthread_cond_destroy(pthread_cond_t *cond); 销毁条件变量。
pthread_cond_wait int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); 释放互斥锁并等待条件变量信号,条件满足时重新获取互斥锁并继续执行。
pthread_cond_timedwait int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime); 释放互斥锁并等待条件变量信号,直到指定的时间或条件满足时重新获取互斥锁并继续执行。
pthread_cond_signal int pthread_cond_signal(pthread_cond_t *cond); 发送信号,唤醒一个等待该条件变量的线程。
pthread_cond_broadcast int pthread_cond_broadcast(pthread_cond_t *cond); 发送信号,唤醒所有等待该条件变量的线程。

2.

死锁的必要条件

  1. 互斥条件:一个资源每次只能被一个执行流使用。

  2. 请求与保持条件:一个执行流因请求资源而阻塞时,但已获得该资源的线程始终不释放。

  3. 不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺。

  4. 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。

解决死锁问题

  1. 破坏死锁的四个必要条件
  2. 加锁顺序一致
  3. 避免锁未释放的场景
  4. 资源一次性分配

常见函数

函数名 原型 描述
pthread_mutex_init int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); 初始化互斥锁。
pthread_mutex_destroy int pthread_mutex_destroy(pthread_mutex_t *mutex); 销毁互斥锁。
pthread_mutex_lock int pthread_mutex_lock(pthread_mutex_t *mutex); 加锁,如果互斥锁已经被锁住,则阻塞直到锁可用。
pthread_mutex_trylock int pthread_mutex_trylock(pthread_mutex_t *mutex); 尝试加锁,如果互斥锁已经被锁住,则返回错误而不阻塞。
pthread_mutex_timedlock int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *timeout); 在指定的超时时间内尝试加锁。
pthread_mutex_unlock int pthread_mutex_unlock(pthread_mutex_t *mutex); 解锁。
pthread_rwlock_init int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); 初始化读写锁。
pthread_rwlock_destroy int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); 销毁读写锁。
pthread_rwlock_rdlock int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); 获取读锁,如果写锁已经被锁住,则阻塞。
pthread_rwlock_tryrdlock int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); 尝试获取读锁,如果写锁已经被锁住,则返回错误而不阻塞。
pthread_rwlock_timedrdlock int pthread_rwlock_timedrdlock(pthread_rwlock_t *rwlock, const struct timespec *timeout); 在指定的超时时间内尝试获取读锁。
pthread_rwlock_wrlock int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); 获取写锁,如果读锁或写锁已经被锁住,则阻塞。
pthread_rwlock_trywrlock int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); 尝试获取写锁,如果读锁或写锁已经被锁住,则返回错误而不阻塞。
pthread_rwlock_timedwrlock int pthread_rwlock_timedwrlock(pthread_rwlock_t *rwlock, const struct timespec *timeout); 在指定的超时时间内尝试获取写锁。
pthread_rwlock_unlock int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); 释放读写锁。
pthread_spin_init int pthread_spin_init(pthread_spinlock_t *lock, int pshared); 初始化自旋锁。
pthread_spin_destroy int pthread_spin_destroy(pthread_spinlock_t *lock); 销毁自旋锁。
pthread_spin_lock int pthread_spin_lock(pthread_spinlock_t *lock); 加自旋锁,直到锁可用。
pthread_spin_trylock int pthread_spin_trylock(pthread_spinlock_t *lock); 尝试加自旋锁,如果锁已经被锁住,则返回错误而不阻塞。
pthread_spin_unlock int pthread_spin_unlock(pthread_spinlock_t *lock); 解自旋锁。

锁 同步 条件变量
https://weihehe.top/2024/07/29/互斥锁/
作者
weihehe
发布于
2024年7月29日
许可协议