Linux操作系统-线程互斥,线程同步,生产者消费者模型

本文详细介绍了线程互斥的概念,通过多线程抢票程序展示了不加锁时可能出现的数据不一致问题,并解释了互斥锁如何通过确保同一时刻只有一个线程执行临界区代码来避免此类问题。此外,还探讨了死锁的现象和避免策略,以及线程同步中的条件变量在解决线程饥饿问题中的应用。最后,以生产者消费者模型为例,展示了阻塞队列在多线程同步中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程互斥

线程互斥及相关概念

线程互斥(Mutual Exclusion)是指在多线程环境下,同一时刻只能有一个线程访问共享资源,以避免对该资源的不正确访问,造成数据不一致等问题。

例如,如果有多个线程都要同时对同一个全局变量进行修改,那么就需要使用线程互斥来保证对该变量的访问是互斥的,也就是说,在任意时刻只能有一个线程对该变量进行修改。

临界资源(Critical Resource)是指在多线程环境下需要被多个线程共享访问的资源,对该资源的访问需要进行同步(如使用互斥进行互斥)以避免出现不正确的访问。

临界区:每个线程内部,访问临界资源的代码,就叫做临界区

互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。是对临界资源保护的一种手段。

原子性:不会被任何调度机制影响的操作,该操作只有两态,要么完成,要么未完成。

多线程抢票

#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>

using namespace std;

// 共享资源,多线程同时访问(未来的临界资源)
int tickets = 10000;

void* getTickets(void* args)
{
    string* ps = (string*)args;
    while(true)
    {
        if(tickets > 0)  // 未来的临界区
        {
            usleep(1000);
            printf("%s : %d\n", ps->c_str(), tickets); // 未来的临界区
            // cout << *ps << " get ticket " << tickets << endl; 
            tickets--; // 未来的临界区
        }
        else
        {
            break;
        }
    }
    delete ps;
    return nullptr;
}


// 多线程抢票程序
int main()
{
    pthread_t tid[3];
    for(int i = 0; i < 3; ++i)
    {
        string* ps = new string("thread");
        ps->operator+=(to_string(i+1));
        pthread_create(tid + i, nullptr, getTickets, (void*)ps);
    }
    for(int i = 0; i < 3; ++i)
    {
        pthread_join(tid[i], nullptr);
        // printf("主线程等待回收新线程%d成功\n", i + 1);
        // cout << "主线程等待回收新线程" << i+1 << "成功" << endl;
    }
    return 0;
}

分析

上方程序为多线程抢票程序,全局数据tickets为共享资源(未来的临界资源,此时还没有进行互斥保护),多个线程对getTickets函数进行了重入,getTickets方法中对全局tickets变量访问和修改的代码都是未来的临界区,如if判断,printf打印及tickets--代码都是未来的临界区代码。

因为多线程并发执行,访问共享资源,因时序及线程切换等原因造成的数据不一致等问题。我们则需要对访问共享资源的代码进行加锁保护,进行线程互斥。

为什么多个线程并发访问共享资源时,因为线程切换就会造成数据不一致呢?下面举两点解释说明:

  1. if(tickets > 0):tickets > 0判断的本质也是计算,则该代码执行时需要CPU进行逻辑运算,tickets全局数据存储在内存中,则需要将tickets数据load到CPU寄存器中,本质就是将数据load到当前线程的上下文中。执行if判断的后面代码块时,因为多线程并发执行,此时的执行线程随时可能被切换(此时寄存器中的线程上下文数据也会被线程保存起来),则就可能造成多个执行线程同时进入if判断内部。若此时tickets为1,则就会因为线程切换造成tickets减到0甚至-1。(上方程序中的usleep更加提高了这种情况发生的可能性)

  1. tickets--:这条C语句在不进行优化的情况下翻译为汇编时,最少会变为三条:1. load到CPU寄存器中 2. 对寄存器内容进行-- 3. 将寄存器数据load回内存中。因此这个--操作并不是原子的,执行到哪一步时都有可能进行线程切换。则存在以下场景:两个线程,线程1和线程2接下来要进行tickets--操作,此时 tickets为10,线程1执行完load到寄存器之后,被切换了,此时线程1会保存它的上下文数据,比如此时保存tickets的寄存器值,其他临时数据,程序计数器eip,栈顶栈底指针的值等。线程2执行tickets--的过程中没有被切换,此时内存中的tickets的值成功被--到了9。再切换为线程1,线程1执行第二步和第三步。此时内存中的tickets又被重复写入到了9。

造成上方多线程抢票程序问题的主要原因其实是第一点,第二点也会存在,只是概率相对更低。实际的执行的情况会比上方所述复杂的多,总之这样的多线程并发访问共享资源的程序是有问题的。

因此我们需要进行线程互斥,常见的保护措施就是互斥锁。使得加锁和解锁之间的代码区域同一时刻只能有一个线程执行,这样的代码区域称为临界区,tickets数据称为临界资源。

pthread线程库mutex互斥锁

// 初始化互斥锁
    // 1. 静态分配,适用于全局或静态的互斥锁
       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    // 2. 动态分配,适用于局部的互斥锁
       int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);   // 参数二设为nullptr即可
// 销毁互斥锁
    // 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥锁不需要销毁
       int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 加锁,解锁
       int pthread_mutex_lock(pthread_mutex_t *mutex);
       int pthread_mutex_unlock(pthread_mutex_t *mutex);
    // int pthread_mutex_trylock(pthread_mutex_t *mutex);

调用pthread_mutex_lock时,可能出现以下情况。

  1. 互斥锁处于未锁状态,该函数会将互斥锁锁定,同时返回成功(0)

  1. 调用时,互斥锁已经被其他线程锁定,或者有其他线程同时申请互斥锁且此线程没有竞争到互斥锁。则pthread_mutex_lock会将调用线程进行阻塞等待,等待

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值