互斥量的接口
1. 初始化互斥量
- 函数:
pthread_mutex_init
- 功能:初始化互斥量,可以动态分配或静态分配
- 参数:
mutex
:需要初始化的互斥量。attr
:初始化互斥量的属性,通常设置为NULL。
- 返回值:成功返回0,失败返回错误码。
2. 销毁互斥量
- 函数:
pthread_mutex_destroy
- 功能:销毁互斥量
- 参数:
mutex
- 需要销毁的互斥量。 - 返回值:成功返回0,失败返回错误码。
- 注意:
- 使用
PTHREAD_MUTEX_INITIALIZER
初始化的互斥量不需要销毁。 - 不要销毁已经加锁的互斥量。
- 销毁后,确保没有线程再尝试加锁。
- 使用
3. 互斥量加锁
- 函数:
pthread_mutex_lock
- 功能:申请互斥锁,阻塞等待,直到获取锁为止。
- 参数:
mutex
- 需要加锁的互斥量。 - 返回值:成功返回0,失败返回错误码。
4. 互斥量解锁
- 函数:
pthread_mutex_unlock
- 功能:释放互斥锁。
- 参数:
mutex
- 需要解锁的互斥量。 - 返回值:成功返回0,失败返回错误码。
关于线程同步和互斥
- 临界资源是多个线程共享的资源,访问这些资源的代码段叫做临界区。
- 互斥是为了保证只有一个线程能够进入临界区,以保护临界资源的完整性。
- 原子性操作是不会被中断的操作,只有两种状态:完成或未完成。
- 互斥量是一种常用的同步工具,用于线程之间的互斥。
- 锁的使用需要注意性能损失,因为它使并行代码变为串行执行。
互斥量实现原理探究:
加锁后的原子性体现在哪里?
- 互斥量确保线程在进入临界区时是原子操作,这意味着只有一个线程能够成功获取锁,其他线程会被阻塞。原子性体现在互斥量的锁定和解锁操作上。
临界区内的线程可能进行线程切换吗?
- 是的,临界区内的线程仍然可以进行线程切换。但在互斥量保护下,其他线程无法获得锁,因此无法进入临界区。只有当拥有锁的线程释放锁后,其他线程才能竞争并进入临界区。
锁是否需要被保护?
- 锁本身是一种临界资源,但它通常会被自己保护。确保申请锁的过程是原子的,这样可以保护锁不会被多个线程同时修改。
如何保证申请锁的过程是原子的?
- 互斥量的实现通常使用硬件指令(如交换指令)来保证申请锁的原子性。线程在申请锁时,会先将自己的寄存器清零,然后执行交换指令将锁与寄存器中的值进行交换。只有一个线程能够成功交换锁,其他线程将被阻塞。这确保了申请锁的过程是原子的。
可重入VS线程安全:
概念:
-
线程安全:多个线程并发执行同一段代码时不会导致不同的结果。线程安全问题通常涉及对全局变量或静态变量的操作,如果没有适当的锁保护,可能会导致问题。
-
重入:同一个函数被不同的执行流程(线程)多次调用,即使在前一个调用还未完成的情况下,函数仍然可以正常工作。可重入函数在重入时不会出现问题。
常见的线程不安全情况:
- 不保护共享变量的函数。
- 函数状态会随着被调用而发生变化。
- 返回指向静态变量的指针的函数。
- 调用线程不安全的函数。
常见的线程安全情况:
- 每个线程只对全局变量或静态变量进行读取操作,没有写入权限。
- 类或接口对线程是原子操作的。
- 多个线程之间的切换不会导致执行结果存在二义性。
常见的不可重入情况:
- 调用malloc/free函数,因为malloc函数通常使用全局链表管理堆。
- 调用标准I/O库函数,因为标准I/O库的实现通常使用不可重入的方式访问全局数据结构。
- 可重入函数体内使用了静态数据结构。
常见的可重入情况:
- 不使用全局变量或静态变量。
- 不使用malloc或new分配的内存。
- 不调用不可重入的函数。
- 不返回静态或全局数据,所有数据由函数的调用者提供。
- 使用本地数据或通过制作全局数据的本地拷贝来保护全局数据。
可重入与线程安全联系:
- 可重入函数是线程安全函数的一种。如果函数是可重入的,那么它也是线程安全的。
- 线程安全不一定是可重入的,但可重入函数一定是线程安全的。
- 在对临界资源进行访问时使用锁可以使函数线程安全,但如果函数的锁未被释放,可能会导致不可重入
常见锁概念
- 死锁是指一组进程都占有资源但由于彼此互相申请不会释放的资源而永远等待的状态。死锁的四个必要条件包括互斥条件、请求与保持条件、不剥夺条件、循环等待条件。
单执行流和死锁
- 单执行流也可能产生死锁,例如,如果一个执行流连续两次申请同一个锁,会导致第二次申请失败,该执行流会被挂起,但它无法释放锁,因此会陷入死锁状态。
阻塞和资源等待队列
- 进程在等待资源时会被挂起,这是因为它需要等待CPU资源或其他资源,如锁、磁盘、网卡等。这种等待被称为阻塞,进程被挂起后会进入资源等待队列,直到资源就绪后再次被唤醒。
避免死锁
- 避免死锁的方法包括破坏死锁的四个必要条件、加锁顺序一致、避免锁未释放场景、资源一次性分配以及使用死锁检测算法和银行家算法等。
Linux线程同步
- 同步用于确保线程能按照一定顺序访问临界资源以避免竞态条件。
- 竞态条件是由于时序问题导致的异常情况。
- 同步通常需要结合互斥锁和条件变量使用。
条件变量
- 条件变量用于线程间同步,描述某种资源是否就绪的机制,通常需要与互斥锁一起使用。
- 条件变量的主要操作包括初始化、销毁、等待条件满足以及唤醒等待线程。
- 线程在等待条件变量时会释放互斥锁,当被唤醒时会重新获得互斥锁。
使用条件变量的规范
- 在等待条件变量时,需要先加锁,然后在条件不满足时等待,这样会自动释放互斥锁。
- 在修改条件并唤醒等待线程时,需要先加锁,设置条件为真,然后使用
pthread_cond_signal
或pthread_cond_broadcast
唤醒等待的线程,最后释放互斥锁。
注意:当某线程被条件变量唤醒时,需要循环检测同步条件是否仍然满足,以防止线程在被激活后,重新获取到锁之前,共享资源被其它线程抢走了的情况。
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(&cond, &mutex);
修改条件
pthread_mutex_unlock(&mutex);
接口总结
pthread_mutex_t - 互斥锁
原型:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:
pthread_mutex_init
: 初始化互斥锁。pthread_mutex_destroy
: 销毁互斥锁。pthread_mutex_lock
: 加锁,阻塞等待互斥锁。pthread_mutex_unlock
: 解锁,释放互斥锁。用法:
- 初始化:
pthread_mutex_init(&mutex, NULL);
- 加锁:
pthread_mutex_lock(&mutex);
- 解锁:
pthread_mutex_unlock(&mutex);
- 销毁:
pthread_mutex_destroy(&mutex);
返回值:
- 成功返回0,失败返回错误码。
参数意义:
mutex
: 互斥锁的指针。attr
: 互斥锁的属性,通常设置为NULL。注意事项:
- 互斥锁用于保护临界区,确保在同一时间只有一个线程能够访问临界资源。
- 加锁和解锁必须成对出现,否则可能导致死锁或资源泄漏。
pthread_cond_t - 条件变量
原型:
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:
pthread_cond_init
: 初始化条件变量。pthread_cond_destroy
: 销毁条件变量。pthread_cond_wait
: 在条件不满足时等待,释放互斥锁。pthread_cond_signal
: 唤醒等待在条件变量上的一个线程。pthread_cond_broadcast
: 唤醒等待在条件变量上的所有线程。用法:
- 初始化:
pthread_cond_init(&cond, NULL);
- 等待:在互斥锁下使用
pthread_cond_wait
等待条件。- 唤醒:使用
pthread_cond_signal
或pthread_cond_broadcast
唤醒等待线程。返回值:
- 成功返回0,失败返回错误码。
参数意义:
cond
: 条件变量的指针。attr
: 条件变量的属性,通常设置为NULL。mutex
: 当前线程所处的互斥锁,用于保护条件等待和唤醒操作。注意事项:
- 条件变量需要与互斥锁一起使用,以确保线程等待和唤醒的正确顺序。
pthread_cond_wait
内部会自动释放互斥锁,并在被唤醒后重新获取互斥锁。