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_PRIVATEPTHREAD_PROCESS_SHARED

6、实现

除提供pthread线程库的Linux平台外,如果想使用读写锁可能需要自行实现(C++17中std::shared_lock支持读写锁)。读写锁实现方法有多种,其具有代表性的两类方案如下:

1)使用2个互斥锁

需要2个互斥锁(rg),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

3)https://www.cnblogs.com/fortunely/p/15211775.html

posted @ 2023-02-02 17:42  HOracle  阅读(171)  评论(0编辑  收藏  举报