Linux 资源锁
一、自旋锁
1、概述
自旋锁(Spin Lock)类似于互斥量,不过自旋锁不是通过休眠阻塞线程(进程),而是在取得锁之前一直处于循环等待的阻塞状态,因此得名“自旋”。自旋锁常作为底层原语实现其他类型的锁。
2、适用场景
1)锁被持有的时间短,而且线程不希望在重新调度上花费太多成本;
2)在非抢占式内核中,会阻塞中断,这样中断处理程序不会让系统陷入死锁状态,因为中断处理程序无法休眠,只能使用这种锁。
3、缺点
1)线程自旋等待锁变成可用时CPU不能做其他事情,浪费CPU资源。
4、伪代码
S = 1;
// 线程代码段
// 进入区
while(S <= 0); // 自旋
S--; // P 操作
... // 临界区
// 退出区
S++; // V 操作
5、接口
// 头文件 pthread.h
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(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);
注意:
1)如果自旋锁当前在解锁状态,pthread_spin_lock()
不用自旋就可以对它加锁;
2)如果自旋锁当前在加锁状态,再获得锁的结果是未定义的,此时调用pthread_spin_lock()
会返回EDEADLK
错误或其他错误,或者调用者可能会永远自旋(取决于具体实现);
3)试图对没有加锁的自旋锁解锁,结果也是未定义的。
6、示例
#define THREAD_NUM 100
pthread_spinlock_t spinlock;
void *thread_main(void *arg)
{
int id = (int)arg;
pthread_spin_lock(&spinlock);
printf("thread main %d get the lock begin\n", id);
printf("thread main %d get the lock end\n", id);
pthread_spin_unlock(&spinlock);
return NULL;
}
int main()
{
pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE);
std::cout << "PTHREAD_PROCESS_PRIVATE = " << PTHREAD_PROCESS_PRIVATE << std::endl;
int i;
pthread_t tids[THREAD_NUM];
for(i = 0; i < THREAD_NUM; i++)
{
pthread_create(&tids[i], NULL, thread_main, i);
}
for(i = 0; i < THREAD_NUM; i++)
{
pthread_join(tids[i], NULL);
}
pthread_spin_destroy(&spinlock);
return 0;
}
二、互斥锁
1、概述
互斥锁(Mutex)通过休眠阻塞线程(进程),确保同一时间只有一个线程访问数据。休眠,也就意味着放弃CPU资源。对互斥锁加锁后,任何其他试图再次对互斥锁加锁的线程都会被阻塞,直到当前线程释放该互斥锁;如果阻塞在该互斥锁上的线程有多个,当锁被解锁时所有线程都变成可运行状态,第一个变为运行状态的线程获得锁的使用权(各个线程处于竞争关系),当其上锁时其他线程再次进入休眠阻塞状态。
2、适用场景
1)多线程或者多进程运行环境需要对临界资源进行保护时;
2)线程需长时间持锁。
3、缺点
1)使用不当易导致死锁;
2)无法表示临界资源可用数量(引入信号量解决);
3)短时间内如果涉及大量线程加(解)锁,则频繁的阻塞(唤醒)会因为大量的线程上下文切换而降低系统性能。
4、接口
// 头文件 pthread.h
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
5、示例
#define THREAD_NUM 100
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *thread_main(void *arg)
{
int id = (int)arg;
pthread_mutex_lock(&mutex);
printf("thread main %d get the lock begin\n", id);
printf("thread main %d get the lock end\n", id);
pthread_mutex_unlock(&mutex);
return NULL;
}
int main()
{
pthread_mutex_init(&mutex, NULL);
int i;
pthread_t tids[THREAD_NUM];
for(i = 0; i < THREAD_NUM; i++)
{
pthread_create(&tids[i], NULL, thread_main, i);
}
for(i = 0; i < THREAD_NUM; i++)
{
pthread_join(tids[i], NULL);
}
pthread_mutex_destroy(&mutex);
return 0;
}
三、读写锁
1、概述
读写锁(readers-writer lock),又称为多读单写锁(multi-reader single-writer lock,或者MRSW lock),共享互斥锁(shared-exclusive lock),以下简称RW lock。读写锁用来解决读写操作并发的问题。多个线程可以并行读数据,但只能独占式地写数据。
RW lock有两种模式:
-
write-mode
在write-mode下,一个writer取得RW lock。当writer写数据时,其他所有writer或reader将阻塞,直到该writer完成写操作;
-
read-mode
在read-mode下,至少一个reader取得RW lock。当reader读数据时,其他reader也能同时读取数据,但writer将阻塞,直到所有reader完成读操作;
RW lock升级与降级:
当writer取得RW lock,进入write-mode,对数据进行写操作时,进入read-mode进行读操作。我们把这个称为锁降级(downgraded RW lock);当reader取得RW lock,进入read-mode,对数据进行读操作时,进入write-mode进行写操作。我们把这个称为锁升级(upgradable RW lock)。锁降级是安全的;而锁升级是不安全的,容易造成死锁,应当避免。
2、读写锁与互斥锁的区别
互斥锁 要么是加锁状态,要么是不加锁状态,而且一次只有一个线程能取得锁、对其加锁。
读写锁 可以有3种状态:读模式加锁,写模式加锁,不加锁。一次只有一个线程能占有写模式的读写锁,不过多个线程可以同时占有读模式的读写锁。
1)当读写锁是写加锁状态时,在被解锁前,所有试图对其加锁的线程都会被阻塞;
2)当读写锁是读加锁状态时,在被解锁前,所有以读模式加锁的线程都可以得到访问权,以写模式加锁的线程会被阻塞。
简而言之,读写锁是读状态与读状态之间共享,与写状态之间互斥,写状态是与任何状态互斥。互斥锁是只有加锁和解锁状态,加锁状态之间互斥。
3、优先级策略
针对reader与writer访问,RW lock能设计成不同的优先级策略:read-preferring(读优先),write-preferring(写优先),unspecified priority(不确定优先级)。
1)read-preferring,允许最大并发量,但如果争用较多时,将导致写饥饿:writer线程将长期不能完成写操作。因为只要有一个reader线程持有lock,writer就无法取得RW lock。而连续不断新来的reader,将导致writer长期无法取得RW lock;
2)write-preferring,能有效避免写饥饿问题,但相对地,会带来读饥饿问题;
3)unspecified priority,不保证优先读访问,或写访问。
3、适用场景
1)临界资源读操作次数远大于写次数。
4、接口
// 头文件 pthread.h
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
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);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
5、读写锁属性
attr = NULL
表示使用默认的读写锁属性:PTHREAD_PROCESS_PRIVATE
表示只在单个进程内的不同线程间共享。另外,还支持属性PTHREAD_PROCESS_SHARED
,表示读写锁将在不同进程间共享。
要设置非默认属性,就要使用下面2个函数初始化、销毁读写锁属性:
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destory(pthread_rwlockattr_t *attr);
要设置的当前值 value,其值只能是PTHREAD_PROCESS_PRIVATE
或PTHREAD_PROCESS_SHARED
。
6、实现
除提供pthread线程库的Linux平台外,如果想使用读写锁可能需要自行实现(C++17中std::shared_lock
支持读写锁)。读写锁实现方法有多种,其具有代表性的两类方案如下:
1)使用2个互斥锁
需要2个互斥锁(r
和g
),1个int
计数器(b
),其中计数器b
记录阻塞等待的reader数量,互斥锁r
用来保护b
只被一个reader使用,另一个互斥锁g
是个全局锁,确保writers都互斥。
伪码:
// Initialize
Set b to 0;
r is unlocked;
g is unlocked;
// rdlock
Lock r; // 内部资源锁
b++;
if b = 1, lock g; // g的lock线程和unlock线程可能并非同一个
UnLock r;
// unlock
Lock r;
b--;
if b = 0; unlock g;
Unlock r;
// wrlock & unlock
Lock g; // 只有处于write-mode时,对g进行unlock和lock的才要求是同一个线程
Unlock g;
2)使用1个条件变量和1个互斥锁
需要1个条件变量cond
,1个互斥锁g
,若干计数器、标志,用于表示线程当前处于激活或阻塞状态。
伪码:
// Initialize
Declare:
num_readers_active; // lock的readers数量
num_writers_waiting; // 阻塞等待lock的writers数量
writer_active; // 表示一个writer是否已经取得lock
// rdlock
// 采用写优先策略(影响加锁方式)
Lock g;
while num_writers_waiting > 0 or writer_active: // 等待所有writer
wait cond, g; // 等待条件变量,释放互斥锁
num_readers_active++;
Unlock g;
// un_rdlock
Lock g;
num_readers_active--;
if num_readers_active == 0:
Notify cond(broadcast);
Unlock g;
// wrlock
Lock g;
num_wirters_waiting++;
while num_readers_active > 0 or writer_active: // 等待所有readers和其他writers
wait cond, g;
num_writers_waiting--;
Set writer_active ot true;
Unlock g;
// un_wrlock
Lock g;
Set writer_active to false;
Notify cound(broadcast);
Unlock g;
四、References
1)http://www.manongjc.com/detail/27-fxxxnjpqezcnqfz.html
2)https://blog.csdn.net/lijunfeng513/article/details/125832676