Linux 互斥锁 递归锁 自旋锁 读写锁

在多线程中,我们经常会要用到锁,那么,锁是什么,我们为什么要用到锁?回到问题的本质,我们在什么场景下会用到锁?锁是针对程序中的临界资源,也就是公共资源的,当我们有两个或多个线程同时对一个临界资源操作的时候,为了保证共享数据操作的完整性,我们要为这些公共资源加锁。

在Linux中常见的锁主要有互斥锁、自旋锁、读写锁,至于递归锁则是互斥锁的一个特例。

 


互斥锁(mutexlock)

在讲什么是互斥锁之前,我们先来看一下下面这段代码:

#includ 
#includ 

define THREAD_NUM 10

void *thread_proc(void *arg)
{
        int *pcount = (int*)arg;
    
        int i = 0;
        while(i ++ < 100000) {
                (*pcount) ++;
                usleep(1);
        }
}

int main()
{
        pthread_t thread_id[THREAD_NUM] = {0};

        int count = 0;
​
        int i = 0;
        for(i = 0; i < THREAD_NUM; i ++)
        {
                // 创建10个线程,每个线程对count实行自加到10万,count为这10个线程的一个共享资源
                pthread_create(&thread_id[i], NULL, thread_proc, &count);
        }

        // 每隔一秒打印一次count的值
        for(i = 0; i < 100; i ++) {
                printf("count --> %d\n", count);
                sleep(1);
        }

        return 0;
}

他的执行结果为下面这样: 

Linux 互斥锁 递归锁 自旋锁 读写锁_第1张图片

执行这段代码,我们本意是想最后打印出来的count值能到100万,但是实际上,最后我们打印出来的count值只会有99万多,那这是为什么呢?这就是由于多线程对同一个临界资源进行操作,我们代码是只有一行idx++,但是在这行代码翻译成汇编代码的时候就变成了这样:

Linux 互斥锁 递归锁 自旋锁 读写锁_第2张图片

 我们的一行idx++,翻译成汇编代码变成了3行。理想状态下,我们希望者行汇编代码能像下面这样顺序执行:

Linux 互斥锁 递归锁 自旋锁 读写锁_第3张图片

但是实际时,由于多线程的并发操作,使得部分时候执行的顺序变成了这样:

Linux 互斥锁 递归锁 自旋锁 读写锁_第4张图片

这就导致了我们上面的代码会出现最后的打印只有99万多的结果。为了确保不发生这种情况,我们就需要在对临界资源操作时加上锁。

Linux中的互斥锁是我们最常使用于线程同步的锁,标记用来保证在任一时刻,只能有一个线程访问该对象,同一线程多次加锁操作会造成死锁,通常情况下锁操作失败会将该线程睡眠等待锁释放时被唤醒。那么我们怎么使用互斥锁呢?pthread.h头文件中就提供了互斥锁的使用。

  • int pthread_mutex_init( pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr );用于初始化互斥锁。mutexattr参数指定互斥锁的属性。如果将它设置为NULL,则表示默认属性。
  • int pthread_mutex_destroy( pthread_mutex_t* mutex);该函数用于销毁互斥锁,以释放其占用的内核资源。销毁一个已经枷锁的互斥锁将导致不可预期的后果。
  • int pthread_mutex_lock( pthread_mutex_t* mutex);该函数以原子操作的方式给一个互斥锁加锁。如果目标互斥锁已经被锁上,则pthread_mutex_lock调用将阻塞,直到该互斥锁的占有者将其解锁。
  • int pthread_mutex_unlock( pthread_mutex_t* mutex);该函数以原子操作的方式给一个互斥锁解锁。如果此时有其他线程正在等待这个互斥锁,则这些线程中的某一个将获得它。
  • int pthread_mutex_trylock( pthread_mutex_t* mutex);函数pthread_mutex_trylock和pthread_mutex_lock类似,不过它始终立即返回,而不论被操作的互斥锁是否已经被枷锁。相当于pthread_mutex_lock的非阻塞版本。当目标互斥锁未被加锁时,pthread_mutex_trylock对互斥锁执行加锁操作。当互斥锁已经被加锁时,pthread_mutex_trylock将返回错误码EBUSY。当然,pthread_mutex_trylock和pthread_mutex_lock的行为是针对普通锁而言的。

上面这些函数成功时返回0,失败则返回错误码。我们来在之前的代码中加入锁,再打印下结果:

#includ 
#includ 

define THREAD_NUM 10

pthread_mutex_t mutex;

void *thread_proc(void *arg)
{
        int *pcount = (int*)arg;
    
        int i = 0;
        while(i ++ < 100000) {
                pthread_mutex_lock(&mutex);          // 加锁
                (*pcount) ++;
                pthread_mutex_unlock(&mutex);        // 解锁
                usleep(1);
        }
}

int main()
{
        pthread_t thread_id[THREAD_NUM] = {0};

        int count = 0;

        // 初始化互斥锁
        pthread_mutex_init(&mutex, NULL);
​
        int i = 0;
        for(i = 0; i < THREAD_NUM; i ++)
        {
                // 创建10个线程,每个线程对count实行自加到10万,count为这10个线程的一个共享资源
                pthread_create(&thread_id[i], NULL, thread_proc, &count);
        }

        // 每隔一秒打印一次count的值
        for(i = 0; i < 100; i ++) {
                printf("count --> %d\n", count);
                sleep(1);
        }

        return 0;
}

Linux 互斥锁 递归锁 自旋锁 读写锁_第5张图片

这里可以看到,可以达到我们想要的结果100万。

 


递归锁(recursivelock)

严格上讲递归锁只是互斥锁的一个特例,同样只能有一个线程访问该对象,但递归锁允许同一个线程在未释放其拥有的锁时反复对该锁进行加锁操作。windows下的临界区默认是支持递归锁的,而linux下的互斥量则需要设置参数PTHREAD_MUTEX_RECURSIVE,默认则是不支持的。我们先来看下下面的代码:

#include 
#include 
​
int count = 0;
​
pthread_mutex_t mutex;
​
void* thread_proc(void*)
{
        int i = 0;
        for (i=0; i<5000; i++)
        {
                pthread_mutex_lock(&mutex);
                pthread_mutex_lock(&mutex);
​
                count ++;
                printf("count = %d\n", count);
​
                pthread_mutex_unlock(&mutex);
                pthread_mutex_unlock(&mutex);      
        }
}
​
int main()
{
        pthread_t tid1, tid2;

        pthread_mutex_init(&mutex, NULL);
​
        pthread_create(&tid1, NULL, thread_proc, NULL);
        pthread_create(&tid2, NULL, thread_proc, NULL);
​
        pthread_join(tid1, NULL);
        pthread_join(tid2, NULL);
​
        return 0;
}

这里,我们在互斥锁lock后又调用了一次lock,这时,程序就会死锁,不输出任何信息。

但是,如果我们这里使用的是递归锁的话,就不会有死锁的问题。

#include 
#include 
​
int count = 0;
​
pthread_mutex_t mutex;
​
void* thread_proc(void*)
{
        int i = 0;
        for (i=0; i<5000; i++)
        {
                pthread_mutex_lock(&mutex);
                pthread_mutex_lock(&mutex);
​
                count ++;
                printf("count = %d\n", count);
​
                pthread_mutex_unlock(&mutex);
                pthread_mutex_unlock(&mutex);      
        }
}
​
int main()
{
        pthread_t tid1, tid2;

        // 需要先定义一个pthread_mutexattr_t变量,用于设置锁的属性
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
​
        //设置锁的属性
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
        pthread_mutex_init(&mutex, &attr);
​
        pthread_create(&tid1, NULL, thread_proc, NULL);
        pthread_create(&tid2, NULL, thread_proc, NULL);
​
        pthread_join(tid1, NULL);
        pthread_join(tid2, NULL);
​
        return 0;
}

使用递归锁,结果就可以正确的输出1~10000。

 


自旋锁(spinlock)

同样用来标记只能有一个线程访问该对象,在同一线程多次加锁操作会造成死锁。使用硬件提供的swap指令或test_and_set指令实现,同互斥锁不同的是在锁操作需要等待的时候并不是睡眠等待唤醒,而是循环检测保持者已经释放了锁。这样做的好处是节省了线程从睡眠状态到唤醒之间内核会产生的消耗,在加锁时间短暂的环境下这点会提高很大效率。

自旋锁的实现是为了保护一段短小的临界区操作代码,主要是用于在SMP上保护临界区,保证这个临界区的操作是原子的,从而避免并发的竞争冒险。在Linux内核中,自旋锁通常用于包含内核数据结构的操作,你可以看到在许多内核数据结构中都嵌入有spinlock,这些大部分就是用于保证它自身被操作的原子性,在操作这样的结构体时都经历这样的过程:上锁-操作-解锁。如果内核控制路径发现自旋锁“开着”(可以获取),就获取锁并继续自己的执行。相反,如果内核控制路径发现锁由运行在另一个CPU上的内核控制路径“锁着”,就在原地“旋转”,反复执行一条紧凑的循环检测指令,直到锁被释放。 自旋锁是循环检测“忙等”,即等待时内核无事可做(除了浪费时间),进程在CPU上保持运行,所以它保护的临界区必须小,且操作过程必须短。不过,自旋锁通常非常方便,因为很多内核资源只锁1毫秒的时间片段,所以等待自旋锁的释放不会消耗太多CPU的时间。

自旋锁的初始化有两种方式:

  • pthread_spin_lock_t lock = SPIN_LOCK_UNLOCKED;自旋锁的宏常量初始化。
  • pthread_spinlock_init(spin_lock_t* lock)函数初始化。lock是读写锁的spin_lock_t结构指针。

自旋锁的加锁和解锁:

  • int pthread_spinlock_lock(pthread_spinlock_t* lock);获取一个自旋锁。如果该自旋锁当前没有被其它线程所持有,则调用该函数的线程获得该自旋锁.否则该函数在获得自旋锁之前不会返回。如果调用该函数的线程在调用该函数时已经持有了该自旋锁,则结果是不确定的。
  • int pthread_spinlock_trylock(pthread_spinlock_t* lock);尝试获取一个自旋锁。如果无法获取则理解返回失败。
  • int pthread_spinlock_unlock(pthread_spinlock_t* lock);用于释放自旋锁。

在使用方法上,自旋锁和互斥锁差不多,这里还用上面互斥锁的那个例子:

#includ 
#includ 

define THREAD_NUM 10

pthread_spinlock_t spinlock;

void *thread_proc(void *arg)
{
        int *pcount = (int*)arg;
    
        int i = 0;
        while(i ++ < 100000) {
                pthread_spin_lock(&spinlock);          // 加锁
                (*pcount) ++;
                pthread_spin_unlock(&spinlock);        // 解锁
                usleep(1);
        }
}

int main()
{
        pthread_t thread_id[THREAD_NUM] = {0};

        int count = 0;

        // 初始化自旋锁
        pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);
​
        int i = 0;
        for(i = 0; i < THREAD_NUM; i ++)
        {
                // 创建10个线程,每个线程对count实行自加到10万,count为这10个线程的一个共享资源
                pthread_create(&thread_id[i], NULL, thread_proc, &count);
        }

        // 每隔一秒打印一次count的值
        for(i = 0; i < 100; i ++) {
                printf("count --> %d\n", count);
                sleep(1);
        }

        return 0;
}

互斥锁和自旋锁的应用:

互斥锁用于临界区持锁时间比较长的操作,比如下面这些情况都可以考虑

  • 临界区有IO操作
  • 临界区代码复杂或者循环量大
  • 临界区竞争非常激烈
  • 单核处理器

至于自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下,自旋锁一般用于多核的服务器。

 


读写锁(rwlock)

高级别锁,区分读和写,符合条件时允许多个线程访问对象。处于读锁操作时可以允许其他线程和本线程的读锁, 但不允许写锁, 处于写锁时则任何锁操作都会睡眠等待。常见的操作系统会在写锁等待时屏蔽后续的读锁操作以防写锁被无限孤立而等待,在操作系统不支持情况下可以用引用计数加写优先等待来用互斥锁实现。 读写锁适用于大量读少量写的环境,但由于其特殊的逻辑使得其效率相对普通的互斥锁和自旋锁要慢一个数量级。值得注意的一点是按POSIX标准在线程申请读锁并未释放前本线程申请写锁是成功的,但运行后的逻辑结果是无法预测。

读写锁中的读操作可以共享,写操作是排它的,读可以有多个在读,写只有唯一个在写,写的时候不允许读操作。对于读数据较修改数据频繁的应用,用读写锁代替互斥锁可以提高效率。因为使用互斥锁时,即使是读出数据(相当于操作临界区资源)都需要上互斥锁;而采用读写锁则允许在任一时刻多个读出。

读写锁的初始化有两种方式:

  • pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;宏常量初始化。
  • pthread_rwlock_init(pthread_rwlock_t*, pthread_rwattr_t*);函数初始化。rwlock是读写锁的pthread_rwlock_t结构指针,attr是读写锁的属性结构指针,不需要别的属性默认为NULL。

读写锁的加锁和解锁:

  • int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);该函数为读写锁的读锁
  • int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);该函数为读写锁的写放
  • pthread_rwlock_unlock(pthread_rwlock_t*);该函数为读写锁的释放。
  • int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);该函数用于销毁读写锁。

获取读写锁的读操作有两种方式:

  • 阻塞式:pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
  • 非阻塞式:pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);如果获取不到锁,会立即返回错误EBUSY。

如果对应的读写锁被其它写者持有,或者读写锁被读者持有,该线程都会阻塞等待。

 

你可能感兴趣的:(Linux,C++,Linux,锁)