互斥量(mutex)

Posted qwangxiao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了互斥量(mutex)相关的知识,希望对你有一定的参考价值。

互斥量(mutex)

很多变量需要在线程间共享,这个变量就称为共享变量,可以通过共享数据完成线程之间的交互.
但是,多个线程并发的操作共享变量就会出现问题.
 
如下模拟实现一个网上购票系统:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
 
int ticket = 100; //表示当前的票有多少张
 
void* BuyTicket(void* arg)
{
    char* s = (char*)arg;
    while(1)
    {
        if(ticket > 0)
        {
            usleep(1000);
            printf("%s buy ticket, %d ",s,ticket);
            --ticket;
        }
        else
        {
            break;
        }
    }
    return NULL;
}
 
int main()
{
    pthread_t t1,t2,t3,t4;
    pthread_create(&t1,NULL,BuyTicket,(void*)"thread 1");
    pthread_create(&t2,NULL,BuyTicket,(void*)"thread 2");
    pthread_create(&t3,NULL,BuyTicket,(void*)"thread 3");
    pthread_create(&t4,NULL,BuyTicket,(void*)"thread 4");
 
    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    pthread_join(t3,NULL);
    pthread_join(t4,NULL);
    return 0;
}
 
演示出现错误的结果:
技术分享图片
 
出现以上情况的原因:
  • if语句判断为真以后,代码可以并发的切换到其他的线程
  • usleep这个是模拟漫长的业务过程,在这个业务过程中,就可能有多个线程进入该代码段
  • --ticket本就不是一个原子操作
技术分享图片
 
要解决上面的问题,需要做到以下三点:
  • 代码必须要有互斥行为: 当代码进入临界区执行的时候,不允许其他线程进入该临界区
  • 如果多个线程的代码同时要求执行临界区的代码,并且临界区没有线程在执行,那么只允许一个线程进入临界区
  • 如果线程不在临界区执行,那么该线程不能阻止其他线程进入临界区
技术分享图片
所以就此引入我们的互斥量
 
互斥量接口

初始化互斥量
  • 静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  • 动态分配
int pthread_mutex_init(pyhread_mutex_t* restrict mutex,const pthread_mutexattr_t* restrict attr);
 
参数:
    mutex: 要初始化的互斥量
    attr:填为NULL即可
 
销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t www.feifanyule.cn *mutex);
 
注意:
  • 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
 
互斥量的加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex); 
int pthread_mutex_unlock(pthread_mutex_t *mutex); 
 
返回值:
    成功返回0,失败返回错误号
 
调用 pthread_lock 时可能会出现以下情况:
  • 互斥量处于未加锁状态,那么就会对这个互斥量加锁,同时返回成功
  • 如果这个互斥量已经加锁,这是 pthread_lock 就会进入阻塞状态,等待互斥量解锁
 
根据互斥量改进上面的购票系统:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
 
pthread_mutex_t g_www.chaoyueyule.com lock; //创建一个互斥量
 
int ticket = 100; //表示当前的票有多少张
 
void* BuyTicket(void* arg)
{
    char* s = (char*)arg;
    while(1)
    {
        pthread_mutex_lock(&g_www.120xh.cn  lock); //加锁
        if(ticket > 0)
        {
            usleep(100);
            printf("%s buy ticket, %d ",s,ticket);
            --ticket;
            pthread_mutex_unlock(&g_lock); //解锁
        }
        else
        {
            pthread_mutex_unlock(&g_lock); //解锁
            break;
        }
    }
    return NULL;
}
 
int main()
{
    pthread_mutex_init(&g_www.dashuj5.com  lock,NULL); //初始化互斥量
 
    pthread_t t1,t2,t3,t4;
    pthread_create(&t1,NULL,BuyTicket,(void*)"thread 1");
    pthread_create(&t2,www.tygj1178.com NULL,BuyTicket,(void*)"thread 2");
    pthread_create(&t3,NULL,BuyTicket,(void*)"thread 3");
    pthread_create(&t4,www.2018yulpt.com NULL,BuyTicket,(void*)"thread 4");
 
    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    pthread_join(t3,www.thqpt.com/  NULL);
    pthread_join(t4,NULL);
 
    pthread_mutex_destroy(&g_lock); //销毁互斥量
    return 0;
}
 
死锁

如果在上面的代码中,加了锁以后没有解锁,会出现什么问题呢?
 
比内存泄露更可怕的事情!!! 死锁!!! 
出现内存泄露, 对于一个256G内存的服务器来说, 往往需要很久才会将内存消耗殆尽. 而且企业中的服务 器往往会定期进行 "例行重启".
出现死锁, 系统中的某些线程会直接停止工作, 导致整个服务器的功能瞬间失效!! 
 
死锁的两个常见场景: 
  • 一个线程获取到锁之后, 又尝试获取锁, 就会出现死锁.
  • 两个线程A和B. 线程A获取了锁1, 线程B获取了锁2. 然后A尝试获取锁2, B尝试获取锁1. 这个时候双方都 无法拿到对方的锁. 并且会在获取锁的函数中阻塞等待. 如果线程数和锁的数目更多了, 就会使死锁问题更容易出现, 问题场景更复杂. 对于这种需要获取多个锁的场景, 规定所有的线程都按照固定的顺序来获取锁, 能够一定程度上避免死锁
 
总结成一句话就是,拿了锁却没有及时释放,就会产生死锁
 
线程安全和可重入

可重入函数: 在多个执行流中被同时调用不会存在逻辑上的问题. 
线程安全函数: 在多线程中被同时调用不会存在问题. 
 
这两个概念都是在描述函数在多个执行流中调用的情况.
  • 线程安全 -> 线程
  • 可重入函数 -> 线程&信号处理函数
 
因此可重入的要求比线程安全的要求要更严格! 
  • 可重入函数一般情况下都是线程安全的.
  • 线程安全函数不一定是可重入的.
 
代码演示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
 
pthread_mutex_t g_lock;
int g_count = 0;
 
//当前是线程安全,不可重入的
void Fun()
{
    pthread_mutex_lock(&g_lock);
    printf("lock ");
    ++g_count;
    sleep(2);
    printf("unlock ");
    pthread_mutex_unlock(&g_lock);
}
 
void* ThreadEntry(void* arg)
{
    (void)arg;
    while(1)
    {
        Fun();
    }
    return NULL;
}
 
void MyHandler(int sig)
{
    (void)sig;
    Fun();
}
 
int main()
{
    signal(SIGINT,MyHandler);
    pthread_mutex_init(&g_lock,NULL);
 
    pthread_t t1;
    pthread_create(&t1,NULL,ThreadEntry,NULL);
    pthread_join(t1,NULL);
 
    ThreadEntry(NULL);
 
    pthread_mutex_destroy(&g_lock);
    return 0;
}
 
当前的执行流程为:
  1. main函数创建线程,并且调用线程入口函数ThreadEntry.
  2. 线程入口函数ThreadEntry内部死循环的调用Fun()函数
  3. Fun()函数进行 加锁 -> 操作 -> 解锁
如果在线程入口函数执行到加锁以后,解锁之前我们按下了 ctrl+c 会发生什么情况呢?
技术分享图片
 
当我们在ThreadEntry调用lock以后(unlock以前)按下 ctrl+c,这是会触发信号捕捉函数,在信号捕捉函数里会去调用 Fun() 函数.
但是调用Fun()函数的第一步就是解锁,这时锁已经被ThreadEntry函数获取,那么我们的信号捕捉函数就无法获取锁,只能等待锁被释放再去执行.
但是,之前在信号捕捉时候提到过,在执行完信号捕捉函数以前,主函数会一直阻塞的等待,知道信号捕捉函数退出,在接着执行.
这时候就出现了 ThreadEntry 在等待 MyHandler执行完, MyHandler 函数在等待ThreadEntry函数释放锁.所以就阻塞了.
 
因此对于这个场景下, Fun()函数是线程安全的, 但是不可重入.

以上是关于互斥量(mutex)的主要内容,如果未能解决你的问题,请参考以下文章

windows 之互斥量----mutex

第29课 互斥量与自解锁(std::mutex和lock系列)

linux C 互斥量mutex 使用记录

线程同步方式之互斥量Mutex

21 同步与互斥互斥量

21.互斥量