线程同步
为什么需要线程同步?
线程同步是为了对共享资源的访问进行保护
- 线程同步的目的是为了保护多个线程共享的资源,如全局变量,以避免数据一致性问题
保护的目的是为了解决数据一致性的问题
- 只有当一个线程可以修改的变量,其他线程也可以读取或修改时,才存在数据一致性问题。此时,需要进行线程同步操作以确保线程在访问变量时不会读取到无效的值
出现数据一致性问题其本质在于进程中的多个线程对共享资源的并发访问(同时访问)
- 要防止并发访问,就需要保护共享资源,避免并发访问
当一个线程在修改变量时,其他线程可能会读取到不一致的值,从而导致数据一致性问题
- 多线程并发访问数据不一致
互斥锁
为了解决共享资源并发访问导致的数据不一致问题,可以使用Linux提供的线程同步技术。这些技术确保在同一时间只有一个线程可以访问特定的变量,从而避免并发访问和消除数据不一致。Linux系统提供了多种线程同步机制,如互斥锁、条件变量、自旋锁和读写锁等
- 线程同步访问变量
简介
-
互斥锁(mutex)本质上是一把锁,是在访问共享资源之前进行上锁,并在访问完成后释放(解锁)的机制
-
上锁后,任何其他试图再次上锁的线程都会被阻塞,直到当前线程解锁。如果在解锁时有多个线程阻塞,那么这些线程会被唤醒并尝试上锁,如果有一个线程成功上锁,其他线程则再次被阻塞
-
举例,卫生间(共享资源)被一人(线程)使用(上锁)时,其他人(线程)只能等待(阻塞),等使用完的人出来(解锁)后,其他人才能使用(尝试上锁),但只有一个人能成功进入,剩下的人则需再次等待
-
在程序设计中,所有线程访问共享资源都需要遵循相同的数据访问规则,即只有在得到锁的情况下才能访问共享资源,否则即使其他线程在使用资源前都申请锁,还是会出现数据不一致的问题
-
互斥锁使用pthread_mutex_t数据类型表示,使用前需要进行初始化操作,有两种方式可以初始化互斥锁
互斥锁初始化
-
使用 PTHREAD_MUTEX_INITIALIZER 宏初始化互斥锁
{ { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }
- pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
-
使用 pthread_mutex_init()函数初始化互斥锁
-
PTHREAD_MUTEX_INITIALIZER宏只适用于在定义的时候就直接进行初始化,对于其他情况,如先定义互斥锁后再进行初始化,或在堆中动态分配的互斥锁(例如使用malloc函数分配的互斥锁对象),可以使用pthread_mutex_init()函数进行初始化
-
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);-
参数mutex是需要初始化的互斥锁对象的指针
-
参数attr是互斥锁的属性指针,如果设置为NULL,则互斥锁的属性设置为默认值
-
函数返回值为0表示成功,非0表示失败
-
-
在ubuntu系统下执行"man 3 pthread_mutex_init"命令,如果找不到pthread_mutex_init()函数的手册,可以通过安装manpages-posix-dev来解决
-
互斥锁加锁和解锁
-
初始化后的互斥锁处于未锁定状态,可以通过调用 pthread_mutex_lock() 函数进行加锁,通过 pthread_mutex_unlock() 函数进行解锁
-
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);-
参数 mutex 指向互斥锁对象
-
成功调用返回 0,失败返回非 0 的错误码
-
-
pthread_mutex_lock() 函数用于加锁互斥锁
-
如果互斥锁未锁定,调用将立即成功
-
如果已锁定,调用将阻塞直到锁被释放
-
-
pthread_mutex_unlock() 函数用于解锁已锁定的互斥锁。错误操作包括
-
解锁一个未锁定的互斥锁
-
解锁由其他线程锁定的互斥锁
-
-
当互斥锁被解锁时,如果有多个线程在等待,它们将尝试加锁,但无法预知哪个线程会成功获得锁
pthread_mutex_trylock()函数
-
pthread_mutex_trylock()函数用于尝试锁定互斥锁,当互斥锁已被其他线程锁定时,该函数不会阻塞,而是返回错误码EBUSY,当互斥锁未被锁定时,该函数将锁定互斥锁并立即返回
-
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);-
参数 mutex 指向目标互斥锁
-
成功返回0,失败返回一个非0的错误码。如果目标互斥锁已经被其他线程锁定,调用失败返回EBUSY
-
销毁互斥锁
-
不再需要的互斥锁应通过调用 pthread_mutex_destroy() 函数进行销毁
-
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);-
参数 mutex 指向目标互斥锁
-
在调用成功时返回 0,失败时返回非 0 错误码
- 错误情况包括销毁未解锁的互斥锁和未初始化的互斥锁
-
-
销毁后的互斥锁不能再进行锁定和解锁,需要再次调用 pthread_mutex_init() 函数对互斥锁进行初始化才能再次使用
互斥锁死锁
-
一个线程试图对同一个互斥锁加锁两次会导致死锁,线程会陷入阻塞状态
-
死锁也可能发生在多个线程同时尝试锁定多个互斥锁时,如果两个线程各自持有对方需要的互斥锁并等待对方释放,就会产生死锁
-
避免死锁的一种方法是在程序中定义互斥锁的层级关系,确保所有线程按照相同的顺序锁定互斥锁
-
如果应用程序结构复杂,难以对互斥锁进行排序,可以使用 pthread_mutex_trylock() 函数尝试非阻塞地锁定互斥锁,并在失败时释放所有已锁定的互斥锁,稍后再试
- 使用 pthread_mutex_trylock() 的方法效率较低,因为可能需要多次尝试才能成功锁定所有互斥锁
互斥锁的属性
-
#include <pthread.h>
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);-
使用 pthread_mutex_init() 函数初始化互斥锁时,可以通过参数 attr 设置互斥锁的属性,该参数指向一个 pthread_mutexattr_t 类型的对象,定义了互斥锁的属性。如果 attr 设置为 NULL,则互斥锁属性为默认值
-
如不使用默认属性,必须初始化一个 pthread_mutexattr_t 对象,并用其地址作为 attr 参数。使用 pthread_mutexattr_init() 函数对此对象初始化,使用完后通过 pthread_mutexattr_destroy() 销毁
-
-
互斥锁的类型属性有四种,控制其锁定特性
-
PTHREAD_MUTEX_NORMAL:标准类型,无错误检查或死锁检测,重复加锁会死锁
-
PTHREAD_MUTEX_ERRORCHECK:提供错误检查,适用于调试
-
PTHREAD_MUTEX_RECURSIVE:允许同一线程多次加锁,需与加锁次数相等的解锁次数相匹配
-
PTHREAD_MUTEX_DEFAULT:提供默认行为,类似 PTHREAD_MUTEX_NORMAL
-
-
可以使用 pthread_mutexattr_gettype() 和 pthread_mutexattr_settype() 函数分别获取和设置互斥锁的类型属性
条件变量
简介
-
条件变量是线程同步的另一种方法。条件变量用于自动阻塞线程,直到某个特定事件发生或某个条件满足为止。通常与互斥锁一起使用
-
使用条件变量主要包括两个动作
-
一个线程等待某个条件满足而被阻塞
-
另一个线程在条件满足时发出“信号”
-
-
条件变量通常搭配互斥锁使用,因为条件的检测是在互斥锁的保护下进行的。线程在改变条件状态之前必须先锁住互斥锁,以避免线程不安全的问题
条件变量初始化
-
条件变量使用 pthread_cond_t 数据类型表示,使用前必须初始化,有两种初始化方式:使用宏 PTHREAD_COND_INITIALIZER 或函数 pthread_cond_init()
-
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
-
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);-
pthread_cond_init() 函数用于初始化条件变量
-
pthread_cond_destroy() 函数用于销毁条件变量
-
参数 cond 指向条件变量对象
-
参数 attr 指向条件变量属性对象,可设为 NULL 使用默认属性
-
-
-
初始化和销毁操作的注意事项
-
使用前必须初始化条件变量
-
对已初始化的条件变量再次初始化可能导致未定义行为
-
对未初始化的条件变量进行销毁可能导致未定义行为
-
仅当没有线程等待条件变量时,销毁它才是安全的
-
销毁的条件变量可以再次通过 pthread_cond_init() 初始化
-
通知和等待条件变量
-
条件变量的主要操作是发送信号(signal)和等待。发送信号是通知处于等待状态的线程,某个共享变量的状态已改变。等待操作则是线程在收到通知前一直处于阻塞状态
-
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);-
pthread_cond_signal() 至少唤醒一个线程
-
pthread_cond_broadcast() 唤醒所有线程
-
cond:指向目标条件变量,向该条件变量发送信号
-
返回值:成功返回 0;失败将返回一个非 0 值的错误码
-
-
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);-
pthread_cond_wait() 函数会使线程阻塞,直到收到条件变量的通知。该函数在其内部对传入的互斥锁进行操作,解锁后阻塞,唤醒后重新锁定互斥锁
-
cond:指向需要等待的条件变量,目标条件变量
-
mutex:参数 mutex 是一个 pthread_mutex_t 类型指针,指向一个互斥锁对象
-
返回值:调用成功返回 0;失败将返回一个非 0 值的错误码
-
-
pthread_cond_signal() 和 pthread_cond_broadcast() 均可向指定的条件变量发送信号,唤醒一个或多个等待状态的线程。pthread_cond_wait() 是线程阻塞直到收到通知
-
条件变量不保留状态信息,只是传递应用程序状态信息的通讯机制。如果无线程等待条件变量,发送的信号会无效
-
例如:当消费者线程没有产品可消费时,让它等待,直到生产者生产产品;当生产者生产产品后,通知消费者
条件变量的判断条件
-
使用条件变量时,通常涉及到一个或多个共享变量作为判断条件
-
当线程从 pthread_cond_wait() 返回时,应使用 while 循环而不是 if 语句重新检查判断条件,因为线程无法确定判断条件的状态是否仍然满足
-
线程从 pthread_cond_wait() 返回后不能确定判断条件为真还是假的原因包括
-
多个线程可能竞争修改共享变量,改变判断条件状态
-
以及可能接收到虚假通知
-
条件变量的属性
-
初始化条件变量时,pthread_cond_init() 函数允许通过 attr 参数设置条件变量的属性,attr 是指向 pthread_condattr_t 类型对象的指针
-
如果 attr 参数设置为 NULL,则使用默认属性初始化条件变量
-
条件变量的属性主要包括进程共享属性和时钟属性,每个属性都有相应的获取(get)和设置(set)方法
自旋锁
自旋锁与互斥锁之间的区别
-
自旋锁与互斥锁类似,都是用于保护共享资源访问的锁,但自旋锁更底层,互斥锁基于自旋锁实现
-
自旋锁在获取锁时,如果锁已被占用,线程会在原地“自旋”等待,直到锁被释放;而互斥锁在无法获取锁时会让线程进入阻塞状态
-
自旋锁的缺点是占用CPU资源,如果不能快速获取锁,会导致CPU效率降低
-
对同一自旋锁加锁两次会导致死锁,而对同一互斥锁加锁两次不一定会导致死锁,因为互斥锁有错误检查机制
-
自旋锁适用于保护执行时间很短的代码段,效率高;而互斥锁适用于等待时间较长的场景
-
自旋锁通常在内核代码中使用较多,因为可以在中断服务函数中使用,而互斥锁则不行
自旋锁初始化
-
自旋锁使用 pthread_spinlock_t 数据类型表示,需要通过 pthread_spin_init() 函数进行初始化,通过 pthread_spin_destroy() 函数进行销毁
-
#include <pthread.h>
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);-
参数 lock 指向了需要进行初始化或销毁的自旋锁对象
-
参数 pshared 表示自旋锁的进程共享属性,可以取值为
-
PTHREAD_PROCESS_SHARED:共享自旋锁。该自旋锁可以在多个进程中的线程之间共享
-
PTHREAD_PROCESS_PRIVATE:私有自旋锁。只有本进程内的线程才能够使用该自旋锁
-
-
成功时返回 0,失败时返回非 0 的错误码
-
自旋锁加锁和解锁
-
自旋锁可以通过 pthread_spin_lock() 函数或 pthread_spin_trylock() 函数进行加锁,前者在未获取到锁时会一直“自旋”,后者在未能获取到锁时会立即返回 EBUSY 错误
-
无论采取何种加锁方式,自旋锁都可以通过 pthread_spin_unlock() 函数进行解锁
-
#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);-
参数 lock 指向自旋锁对象
-
成功时返回 0,失败时返回非 0 错误码
-
-
如果自旋锁未被锁定,pthread_spin_lock() 会将其锁定,如果已被其他线程锁定,则会“自旋”等待,对同一自旋锁加锁两次会导致死锁
-
将互斥锁替换为自旋锁后,程序运行时间显著减少,自旋锁效率比互斥锁高,但需要注意自旋锁的使用场景
读写锁
简介
-
互斥锁和自旋锁只有加锁和不加锁两种状态,且一次只能有一个线程加锁
-
读写锁有三种状态
-
读加锁状态
-
写加锁状态
-
不加锁状态
-
可以有多个线程同时占有读模式的读写锁,但一次只能有一个线程占有写模式的读写锁
-
-
读写锁的两个规则
-
当读写锁处于写加锁状态时,所有试图加锁的线程都会被阻塞
-
当读写锁处于读加锁状态时,所有以读模式加锁的线程都可以成功,但以写模式加锁的线程会被阻塞,直到所有读模式锁被释放
-
-
读写锁适用于读操作远多于写操作的场景,因为读模式下允许多个线程同时读取,而写模式下只允许一个线程进行写操作
-
使用读写锁进行线程同步时,读操作获取读模式锁,写操作获取写模式锁,操作完成后释放相应的锁
-
读写锁也称为共享互斥锁,读模式锁住时称为共享模式,写模式锁住时称为互斥模式
读写锁初始化
-
读写锁使用 pthread_rwlock_t 类型进行声明,并且在使用前必须进行初始化
-
初始化读写锁可以使用宏 PTHREAD_RWLOCK_INITIALIZER 直接初始化或者使用函数 pthread_rwlock_init() 进行初始化
-
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
- 使用 PTHREAD_RWLOCK_INITIALIZER 进行初始化时,必须在读写锁定义时完成初始化
-
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);-
pthread_rwlock_init() 函数用于在运行时初始化读写锁,如果不再使用,需要调用 pthread_rwlock_destroy() 函数销毁读写锁
-
其参数 attr 可以指定读写锁属性,如果参数为 NULL,读写锁属性设置为默认值
-
成功调用返回 0,失败返回非 0 的错误码
-
-
读写锁上锁和解锁
-
以读模式对读写锁加锁使用 pthread_rwlock_rdlock() 函数,以写模式加锁使用 pthread_rwlock_wrlock() 函数
-
解锁读写锁使用 pthread_rwlock_unlock() 函数
-
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);-
参数 rwlock 指向读写锁对象
-
成功调用返回 0,失败返回非 0 的错误码
-
-
如果线程不希望阻塞,可以使用 pthread_rwlock_tryrdlock() 和 pthread_rwlock_trywrlock() 尝试加锁,如果不能获取锁,这两个函数会立即返回错误码 EBUSY
-
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);-
参数 rwlock 指向读写锁对象
-
成功返回 0,失败返回 EBUSY
-
读写锁的属性
-
读写锁有属性,使用 pthread_rwlockattr_t 数据类型表示
-
定义 pthread_rwlockattr_t 对象时,需要使用 pthread_rwlockattr_init() 函数进行初始化,初始化后各个读写锁属性为默认值
-
如果不再使用 pthread_rwlockattr_t 对象,需要调用 pthread_rwlockattr_destroy() 函数将其销毁
-
#include <pthread.h>
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);-
参数 attr 指向需要初始化或销毁的 pthread_rwlockattr_t 对象
-
成功返回 0,失败返回非 0 错误码
-
-
读写锁只有一个属性,即进程共享属性,与互斥锁和自旋锁的进程共享属性相同
-
函数 pthread_rwlockattr_getpshared() 用于从 pthread_rwlockattr_t 对象中获取共享属性,pthread_rwlockattr_setpshared() 用于设置 pthread_rwlockattr_t 对象中的共享属性
-
#include <pthread.h>
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);-
pthread_rwlockattr_getpshared()
-
参数 attr 指向 pthread_rwlockattr_t 对象
-
pshared 用于保存获取的共享属性
-
成功返回 0,失败返回非 0 错误码
-
-
pthread_rwlockattr_setpshared()
-
参数 attr 指向 pthread_rwlockattr_t 对象
-
pshared 用于设置读写锁的共享属性,可以取值 PTHREAD_PROCESS_SHARED (共享读写锁)和 PTHREAD_PROCESS_PRIVATE (私有读写锁,这是默认值)
-
成功返回 0,失败返回非 0 错误码
-
-