Linux线程同步之互斥锁和自旋锁

互斥锁

互斥锁(mutex)又叫互斥量,从本质上说是一把锁,在访问共享资源之前对互斥锁进行上锁,在访问完成后释放互斥锁(解锁);对互斥锁进行上锁之后,任何其它试图再次对互斥锁进行加锁的线程都会被阻塞,直到当前线程释放互斥锁。如果释放互斥锁时有一个以上的线程阻塞,那么这些阻塞的线程会被唤醒,它们都会尝试对互斥锁进行加锁,当有一个线程成功对互斥锁上锁之后,其它线程就不能再次上锁了,只能再次陷入阻塞,等待下一次解锁。

在我们的程序设计当中,只有将所有线程访问共享资源都设计成相同的数据访问规则,互斥锁才能正常工作。如果允许其中的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其它的线程在使用共享资源前都申请锁,也还是会出现数据不一致的问题。

互斥锁使用 pthread_mutex_t 数据类型表示,在使用互斥锁之前,必须首先对它进行初始化操作,可以使用两种方式对互斥锁进行初始化操作。

互斥锁初始化

可以使用 pthread_mutex_init()函数对互斥锁进行初始化,其函数原型如下所示:

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

使用该函数需要包含头文件<pthread.h>。

函数参数和返回值含义如下:

mutex:参数 mutex 是一个 pthread_mutex_t 类型指针,指向需要进行初始化操作的互斥锁对象;

attr:参数 attr 是一个 pthread_mutexattr_t 类型指针,指向一个 pthread_mutexattr_t 类型对象,该对象用于定义互斥锁的属性,若将参数 attr 设置为 NULL,则表示将互斥锁的属性设置为默认值。

返回值:成功返回 0;失败将返回一个非 0 的错误码。

Tips:注意,当在 Ubuntu 系统下执行"man 3 pthread_mutex_init"命令时提示找不到该函数,并不是 Linux下没有这个函数,而是该函数相关的 man 手册帮助信息没有被安装,这时我们只需执行"sudo apt-get install manpages-posix-dev"安装即可。

使用 pthread_mutex_init()函数对互斥锁进行初始化示例:

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

或者:

pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(mutex, NULL);

互斥锁加锁和解锁

互斥锁初始化之后,处于一个未锁定状态,调用函数 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);

使用这些函数需要包含头文件<pthread.h>,参数 mutex 指向互斥锁对象;pthread_mutex_lock()和pthread_mutex_unlock()在调用成功时返回 0;失败将返回一个非 0 值的错误码。

调用 pthread_mutex_lock()函数对互斥锁进行上锁,如果互斥锁处于未锁定状态,则此次调用会上锁成功,函数调用将立马返回;如果互斥锁此时已经被其它线程锁定了,那么调用 pthread_mutex_lock()会一直阻塞,直到该互斥锁被解锁,到那时,调用将锁定互斥锁并返回。

调用 pthread_mutex_unlock()函数将已经处于锁定状态的互斥锁进行解锁。以下行为均属错误:

⚫ 对处于未锁定状态的互斥锁进行解锁操作;

⚫ 解锁由其它线程锁定的互斥锁。

如果有多个线程处于阻塞状态等待互斥锁被解锁,当互斥锁被当前锁定它的线程调用pthread_mutex_unlock()函数解锁后,这些等待着的线程都会有机会对互斥锁上锁,但无法判断究竟哪个线程会如愿以偿!

注意,谁来上锁就由谁来解锁;而且,多线程必须要上同一把锁,才能达到互斥访问的目的。

使用示例

以下使用了一个互斥锁来保护对全局变量 g_count 的访问

示例代码 12.2.1 使用互斥锁保护全局变量的访问 
#include <stdio.h> 
#include <stdlib.h> 
#include <pthread.h> 
#include <unistd.h> 
#include <string.h> 
static pthread_mutex_t mutex; 
static int g_count = 0; 
static void *new_thread_start(void *arg) 
{ 
     int loops = *((int *)arg); 
     int l_count, j; 
     for (j = 0; j < loops; j++) { 
         pthread_mutex_lock(&mutex); //互斥锁上锁 
  
         l_count = g_count; 
         l_count++; 
         g_count = l_count; 
         pthread_mutex_unlock(&mutex);//互斥锁解锁 
     } 
     return (void *)0; 
}
static int loops; 
int main(int argc, char *argv[]) 
{ 
     pthread_t tid1, tid2; 
     int ret; 
     /* 获取用户传递的参数 */ 
     if (2 > argc) 
         loops = 10000000; //没有传递参数默认为 1000 万次 
     else 
         loops = atoi(argv[1]); 
     /* 初始化互斥锁 */ 
     pthread_mutex_init(&mutex, NULL); 
     /* 创建 2 个新线程 */ 
     ret = pthread_create(&tid1, NULL, new_thread_start, &loops); 
     if (ret) { 
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
     ret = pthread_create(&tid2, NULL, new_thread_start, &loops); 
     if (ret) { 
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
     /* 等待线程结束 */ 
     ret = pthread_join(tid1, NULL); 
     if (ret) { 
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
     ret = pthread_join(tid2, NULL); 
     if (ret) { 
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
     /* 打印结果 */ 
     printf("g_count = %d\n", g_count);
     exit(0); 
} 

在测试运行,使用默认值 1000 万次,如下所示:

可以看到确实得到了我们想看到的正确结果,每次对 g_count 的累加总是能够保持正确,但是在运行程序的过程中,明显会感觉到锁消耗的时间会比较长,这就涉及到性能的问题了,一般情况下没啥影响!

pthread_mutex_trylock()函数

当互斥锁已经被其它线程锁住时,调用 pthread_mutex_lock()函数会被阻塞,直到互斥锁解锁;如果线程不希望被阻塞,可以使用 pthread_mutex_trylock()函数;调用 pthread_mutex_trylock()函数尝试对互斥锁进行加锁,如果互斥锁处于未锁住状态,那么调用 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);

使用该函数需要包含头文件<pthread.h>,参数 mutex 指向目标互斥锁;同样在调用成功情况下返回 0,失败返回一个非 0 值的错误码。

⚫ 不能销毁还没有解锁的互斥锁,否则将会出现错误;

⚫ 没有初始化的互斥锁也不能销毁。

被 pthread_mutex_destroy()销毁之后的互斥锁,就不能再对它进行上锁和解锁了,需要再次调用pthread_mutex_init()对互斥锁进行初始化之后才能使用。

实战总结: 

互斥锁(Mutex)是“mutual exclusion”的缩写。互斥锁是实现线程同步,和保护同时写共享数据的主要方法。互斥锁通常又被称为互斥量。

互斥锁对共享数据的保护就像一把锁。在Pthreads中,任何时候仅有一个线程可以锁定互斥量,因此,当多个线程尝试去锁定该互斥量时仅有一个会成功。直到锁定互斥锁的线程解锁互斥量后,其他线程才可以去锁定互斥锁。线程必须轮着访问受保护数据。 

互斥量可以防止“竞争”条件。

一个拥有互斥锁的线程经常用于更新全局变量。确保了多个线程更新同样的变量以安全的方式运行,最终的结果和一个线程处理的结果是相同的(在受保护的地方,多个线程对于资源的操作,就像是一个线程中对数据的先后处理)。这个更新的变量属于一个“临界区(critical section)”。 

使用互斥量的典型顺序如下: 

  • 创建和初始一个互斥量 
  • 多个线程尝试去锁定该互斥量 
  • 仅有一个线程可以成功锁定改互斥量 
  • 锁定成功的线程做一些处理 
  • 线程解锁该互斥量 
  • 另外一个线程获得互斥量,重复上述过程 
  • 最后销毁互斥量 

当多个线程竞争同一个互斥量时,失败的线程会阻塞在lock调用处。可以用“trylock”替换“lock”,则失败时不会阻塞。 

当保护共享数据时,程序员有责任去确认是否需要使用互斥量。如,若四个线程会更新同样的数据,但仅有一个线程用了互斥量,则数据可能会损坏。 

为了方便演示,我们暂时补充下另外一个函数pthread_join

上面我们提到了一个问题,那就是线程创建后,如何避免主线程先于子线程结束,并且附了一篇参考文章。

线程创建后,避免主线程先于子线程结束的四种方式_怎么让新创建的线程不随主线程结束运行-CSDN博客

在前一个代码例程中,我们是直接使用pthread_exit(NULL)来退出了主线程。

这里,为了演示线程安全问题,我们需要等待其他线程结束,最后才退出主线程。

使用的函数就是pthread_join

linux中pthread_join()与pthread_detach()详解_pthread detach join-CSDN博客

pthread_join()即是子线程合入主线程,主线程阻塞等待子线程结束,然后回收子线程资源。

​
#include <pthread.h> 
int pthread_join(pthread_t thread, void **value_ptr);

pthread_join()函数,以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable的。

参数 :thread: 线程标识符,即线程ID,标识唯一线程。retval: 用户定义的指针,用来存储被等待线程的返回值,如果不关心的话,设置为NULL即可。

查看如下程序示例:

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

#define D_EXE_COUNT 100000

static int count = 0;

void *thread_fun1(void *param)
{
    for(int i = 0; i < D_EXE_COUNT; i++)
    {
        count++;
    }
    
    return NULL;
}

void *thread_fun2(void *param)
{
    for(int i = 0; i < D_EXE_COUNT; i++)
    {
        count++;
    }
    
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;
    int ret;

    ret = pthread_create(&tid1, NULL, thread_fun1, NULL);
    if (ret < 0)
    {
         printf("ERROR; return code from pthread_create() is %d\n", ret);
         exit(-1);
    }

    ret = pthread_create(&tid2, NULL, thread_fun2, NULL);
    if (ret < 0)
    {
         printf("ERROR; return code from pthread_create() is %d\n", ret);
         exit(-1);
    }

    //waiting for thread end
    ret = pthread_join(tid1, NULL);
    if (ret < 0) 
    {
        printf("ERROR; return code from pthread_create() is %d\n", ret);
        exit(-1);
    }

    ret = pthread_join(tid2, NULL);
    if (ret < 0) 
    {
        printf("ERROR; return code from pthread_create() is %d\n", ret);
        exit(-1);
    }

    //print result
    printf("count = %d\n", count);

    return 0;
}

结果如下

理论上的结果应该是200000,结果明显不对。

这里面其实就存在线程安全的问题。

怎么解决呢?

使用互斥锁即可。

具体内容直接参考《【正点原子】I.MX6U嵌入式Linux C应用编程指南V1.4》互斥锁部分。

本文不再赘述。

也可以直接参考这篇文章:

Posix多线程编程学习笔记 - 凌峰布衣 - 博客园 (cnblogs.com)

总的来说,使用互斥锁只需要以下几个步骤:

1、定义互斥锁对象pthread_mutex_t mutex;;

2、初始化互斥锁对象pthread_mutex_init(&mutex, NULL); ;

3、对共享资源部分进行上锁pthread_mutex_lock(&mutex);和解锁pthread_mutex_unlock(&mutex);

4、可选,最后不用时可以手动销毁pthread_mutex_destroy(&mutex);

示例代码如下:

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

#define D_EXE_COUNT 100000

static pthread_mutex_t mutex;//in fact,mutex is a global var,but handle for thread safe
static int count = 0;

void *thread_fun1(void *param)
{
    for(int i = 0; i < D_EXE_COUNT; i++)
    {
        pthread_mutex_lock(&mutex);
        count++;
        pthread_mutex_unlock(&mutex);
    }
    
    return NULL;
}

void *thread_fun2(void *param)
{
    for(int i = 0; i < D_EXE_COUNT; i++)
    {
        pthread_mutex_lock(&mutex);
        count++;
        pthread_mutex_unlock(&mutex);
    }
    
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;
    int ret;

    //init mutex
    pthread_mutex_init(&mutex, NULL);

    ret = pthread_create(&tid1, NULL, thread_fun1, NULL);
    if (ret < 0)
    {
         printf("ERROR; return code from pthread_create() is %d\n", ret);
         exit(-1);
    }

    ret = pthread_create(&tid2, NULL, thread_fun2, NULL);
    if (ret < 0)
    {
         printf("ERROR; return code from pthread_create() is %d\n", ret);
         exit(-1);
    }

    //waiting for thread end
    ret = pthread_join(tid1, NULL);
    if (ret < 0) 
    {
        printf("ERROR; return code from pthread_create() is %d\n", ret);
        exit(-1);
    }

    ret = pthread_join(tid2, NULL);
    if (ret < 0) 
    {
        printf("ERROR; return code from pthread_create() is %d\n", ret);
        exit(-1);
    }

    //print result
    printf("count = %d\n", count);

    return 0;
}

结果如下:

可以看到,结果是对的。

注意,同一个共享资源,只要线程操作了,就得上锁,并且,上的必须是同一把锁。

当保护共享数据时,程序员有责任去确认是否需要使用互斥量。如,若四个线程会更新同样的数据,但仅有一个线程用了互斥量,则数据可能会损坏。

经验证,去掉一个线程中的锁,结果如下:

思考下:

线程访问共享资源的两个方式,一个是直接在各自线程里访问,另一种是各线程都调用同一个函数,这个函数里操作了共享资源,上述的例程属于第二种情况。

二者有区别吗?

这两种场景很类似,也很容易搞混,捋一捋。

多个线程都调用同一个函数,该函数是不可重入的,也就是说,里面有共享资源,此时,可以给共享资源部分上锁,因此,每次就只会有一个线程访问该共享资源。相当于每个人都需要进同一个门里去拿东西,这时候这个门就上把锁。这种情况比较好处理。

但是,另外一种情况下,各个线程直接操作的共享资源,首先,线程A操作共享资源时,是有可能被线程B抢占执行的,这时候就会出现线程安全的问题。那么,这种情况怎么解决呢?是给每个线程里的操作部分都给上个锁?换另一种思路就是,如果没法在被调用函数里面上锁,那么如何在调用处解决这个线程同步的问题?这里我有个想法,是不是,只要是同一把锁,同一时间就只能有一把钥匙?如果多个线程里直接操作了同一个全局变量,那么在各自线程里都给操作部分上锁,因为是同一把锁,所以,同时只有一个线程能够处于执行中,只有当线程释放锁之后,其他线程才能进入,就算是在各自线程里操作。如果是这样的话,那就可以解决这个问题了。

这个问题的关键是:使用的是同一把锁。

自旋锁

参考:

自旋锁使用场景和实现分析(转载)_自旋锁的应用场景-CSDN博客

内核当发生访问资源冲突的时候,可以有两种锁的解决方案选择:

  • 一个是原地等待

  • 一个是挂起当前进程,调度其他进程执行(睡眠)

spinlock 是内核中提供的一种比较常见的锁机制,自旋锁是“原地等待”的方式解决资源冲突的,即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地“打转”(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制 —— 自旋锁不应该被长时间的持有(消耗 CPU 资源)。

自旋锁的使用

在linux kernel的实现中,经常会遇到这样的场景:共享数据被中断上下文和进程上下文访问,该如何保护呢?如果只有进程上下文的访问,那么可以考虑使用semaphore或者mutex的锁机制,但是现在中断上下文也参和进来,那些可以导致睡眠的lock就不能使用了,这时候,可以考虑使用spin lock。

这里为什么把中断上下文标红加粗呢?因为在中断上下文,是不允许睡眠的,所以,这里需要的是一个不会导致睡眠的锁——spinlock。

换言之,中断上下文要用锁,首选 spinlock

使用自旋锁,有两种方式定义一个锁:

动态的:

spinlock_t lock; 
spin_lock_init (&lock);

静态的:

DEFINE_SPINLOCK(lock);

自旋锁通常用于以下情况:需要保护的代码段执行时间很短,这样就会使得持有锁的线程会很快释放锁,而“自旋”等待的线程也只需等待很短的时间;在这种情况下就比较适合使用自旋锁,效率高!

综上所述,再来总结下自旋锁与互斥锁之间的区别:

⚫ 实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;

⚫ 开销上的区别:获取不到互斥锁会陷入阻塞状态(休眠),直到获取到锁时被唤醒;而获取不到自旋锁会在原地“自旋”,直到获取到锁;休眠与唤醒开销是很大的,所以互斥锁的开销要远高于自旋锁、自旋锁的效率远高于互斥锁;但如果长时间的“自旋”等待,会使得 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);

使用这两个函数需要包含头文件<pthread.h>。

参数 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);

使用这些函数需要包含头文件<pthread.h>。

参数 lock 指向自旋锁对象,调用成功返回 0,失败将返回一个非 0 值的错误码。

如果自旋锁处于未锁定状态,调用 pthread_spin_lock()会将其锁定(上锁),如果其它线程已经将自旋锁锁住了,那本次调用将会“自旋”等待;如果试图对同一自旋锁加锁两次必然会导致死锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值