【Linux操作系统】线程的互斥与同步

一、线程互斥

  • 临界资源:多线程执行流共享的资源就叫做临界资源
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

抢票代码:
一共有5000张票,创建4个线程,最后打印出每个线程抢到的票数

int g_ticket = 5000;
class DataThread
{
public:
    DataThread(int& ticket, const string& name)
        :_ticket(ticket), _name(name), _total(0)
    {}

    ~DataThread()
    {}

    string _name;
    int& _ticket;//全局的
    int _total;
};
void route(DataThread* td)
{
    while(true)
    {
        //抢票
        if(td->_ticket > 0)
        {
            usleep(1000);
            printf("%s is run..., ticket: %d\n", td->_name.c_str(), td->_ticket);
            td->_ticket--;
            td->_total++;
        }
        else 
        {
            break;
        }
    }
}
const int num = 4;
int main()
{
    vector<Thread<DataThread*>> threads;
    vector<DataThread*> datas;
    for(int i = 0; i < num; i++)
    {
        string name = "thread-" + to_string(i+1);
        DataThread* td = new DataThread(g_ticket, name);
        threads.emplace_back(route, td, name);
        datas.emplace_back(td);
    }
    //创建
    for(auto& thread:threads)
    {
        thread.Satrt();
    }
    //等待
    for(auto& thread:threads)
    {
        thread.Join();
    }
    //打印每个线程获取的票数
    for(auto& data:datas)
    {
        cout << data->_name << " total is: " << data->_total << endl;
    }

    return 0;
}

在这里插入图片描述
为什么出现负数?因为ticket是共享资源,没有被保护,对它的判断不是原子的。在ticket等于1的时候,就可能有多个线程进入判断逻辑,然后都要执行把数据从内存读取到CPU,减法操作,再写回内存。这就导致出现了负数的情况

解决方法:加锁
定义的锁是全局的或静态的:
在这里插入图片描述

定义的锁是局部的:
在这里插入图片描述

加锁:
在这里插入图片描述

申请锁成功,函数就会返回,可以继续运行
申请锁失败,函数就会阻塞,不可以继续运行
函数调用失败,出错返回

解锁:
在这里插入图片描述

防止出现并发访问的问题,要保护全局共享资源,保护共享资源本质是保护临界区,访问临界资源的代码叫临界区。加锁是为了让多线程串行执行,即一个线程成功加上锁,让它访问临界资源,其他线程要等待,直到这个线程访问完才可以让其他线程竞争锁。这个过程叫互斥。

全局加锁代码:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//全局加锁
void route(DataThread* td)
{
    while(true)
    {
        pthread_mutex_lock(&mutex);//加锁
        //抢票
        if(td->_ticket > 0)
        {
            usleep(1000);
            printf("%s is run..., ticket: %d\n", td->_name.c_str(), td->_ticket);
            td->_ticket--;
            pthread_mutex_unlock(&mutex);//解锁
            td->_total++;
        }
        else 
        {
            pthread_mutex_unlock(&mutex);//解锁
            break;
        }
    }
}

在这里插入图片描述

局部加锁代码:

class DataThread
{
public:
    DataThread(int& ticket, const string& name, pthread_mutex_t& mutex)
        :_ticket(ticket), _name(name), _total(0), _mutex(mutex)
    {}

    ~DataThread()
    {}

    string _name;
    int& _ticket;//全局的
    int _total;
    pthread_mutex_t& _mutex;//共用一把锁
};

void route(DataThread* td)
{
    while(true)
    {
        pthread_mutex_lock(&td->_mutex);//加锁
        //抢票
        if(td->_ticket > 0)
        {
            usleep(1000);
            printf("%s is run..., ticket: %d\n", td->_name.c_str(), td->_ticket);
            td->_ticket--;
            pthread_mutex_unlock(&td->_mutex);//解锁
            td->_total++;
        }
        else 
        {
            pthread_mutex_unlock(&td->_mutex);//解锁
            break;
        }
    }
}
const int num = 4;
int main()
{
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, nullptr);//初始化锁

    vector<Thread<DataThread*>> threads;
    vector<DataThread*> datas;
    for(int i = 0; i < num; i++)
    {
        string name = "thread-" + to_string(i+1);
        DataThread* td = new DataThread(g_ticket, name, mutex);
        threads.emplace_back(route, td, name);
        datas.emplace_back(td);
    }
    //创建
    for(auto& thread:threads)
    {
        thread.Satrt();
    }
    //等待
    for(auto& thread:threads)
    {
        thread.Join();
    }
    //打印每个线程获取的票数
    for(auto& data:datas)
    {
        cout << data->_name << " total is: " << data->_total << endl;
    }
    pthread_mutex_destroy(&mutex);//释放锁
    return 0;
}

在这里插入图片描述

优化代码,对线程和锁进行封装——RAII风格

#ifndef __LOCK_GUARD_HPP__
#define __LOCK_GUARD_HPP__
#include <iostream>
#include <pthread.h>
using namespace std;

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        pthread_mutex_lock(_mutex);//构造加锁
    }
    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);//析构解锁
    }
private:
    pthread_mutex_t *_mutex;
};

#endif

void route(DataThread* td)
{
    while(true)
    {
        LockGuard guard(&td->_mutex);//临时对象,构造即加锁,析构即解锁
        //抢票
        if(td->_ticket > 0)
        {
            usleep(1000);
            printf("%s is run..., ticket: %d\n", td->_name.c_str(), td->_ticket);
            td->_ticket--;
            td->_total++;
        }
        else 
        {
            break;
        }
    }
}

在这里插入图片描述

互斥的底层原理
在这里插入图片描述
内存中有一个数据为1,CPU要处理线程1,首先,线程1会把0带入到寄存器中,然后寄存器中的数据与内存中的数据作交换,这样线程1得到了数据为1,这个过程线程1完成了加锁。如果要解锁,就换回去。又来了个线程2,此时线程1切换为线程2(注意:CPU执行线程1时是可以随时被切换的),线程1要带走寄存器中的1,然后线程2把0带入到寄存器中,再与内存的数据作交换,还是0,说明锁被其他线程占用,线程2就会挂起等待。再到线程1,1放会寄存器中,然后CPU处理线程1,加锁的只有线程1,所以线程1可以访问临界资源,即互斥。

CPU寄存器硬件只有一套,但是CPU寄存器内部的数据,可以有多套;
数据在内存里,所有线程都可以访问,是共享的,但是转移到CPU寄存器中就是一个线程私有的;
交换的本质:不是拷贝到寄存器,而是所有的线程竞争一把锁

正在访问临界区资源的线程可以被OS调度吗?

不能。一个线程正在访问临界区,还没有释放锁,对于其他线程来说,要竞争到锁,要么这个锁已经被释放,要么这个锁没有被前面那个线程申请成功。总结:加锁的线程访问临界区,不能被OS调度切换,此时(加锁的线程)访问临界区是原子的(是线程安全的)。

二、线程同步

概念:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。

条件变量:
是什么?
栗子:一个人放苹果到盘子,但是不许说话;另一个人从盘子上拿苹果,但是蒙上眼睛(看不见);一个负责放,另一个负责拿,不能同时进行,是互斥的。放苹果的要申请到锁,才能放苹果,拿苹果的也要申请到锁,才能去盘子上拿苹果(可能盘子上没有苹果)。总之就是两个人要竞争一把锁,如果拿苹果的人竞争性更强,每次都是他申请到锁,放苹果的人就没机会放苹果了,这也导致拿苹果的人每次申请到锁后去盘子上总是没有苹果拿,但是他又一直占用锁(每次申请到锁的都是他),所以放苹果的不能申请到锁放苹果,拿苹果的在盘子上也没有苹果拿,这就导致整个过程很不合理。
在这里插入图片描述
解决方式:放苹果的人有一个铃铛,他申请到锁,并往盘子上放了苹果,即确定盘子上有苹果时,他就会敲响一下铃铛,提醒拿苹果的人可以来申请锁并且去拿苹果了。如果没有铃铛没有响,拿苹果的人就不要一直去申请锁,这样放苹果的人就有机会去申请锁然后放苹果。

拿苹果的人不止有一个,可以有多个,组成一个队列。铃铛响多次,表示让多个人来申请锁,竞争到锁的去拿苹果,然后返回队列,没竞争到锁的继续等待。条件变量等价于这个铃铛和多个线程组成的队列(保证顺序性)。

认识接口:
全局条件变量:
在这里插入图片描述

初始化:

int pthread_cond_init(pthread_cond_t *restrict cond,constpthread_condattr_t *restrict attr);

销毁:

int pthread_cond_destroy(pthread_cond_t *cond)

等待:

int pthread_cond_wait(pthread_cond_t *restrictcond,pthread_mutex_t *restrict mutex);

唤醒一个线程:

int pthread_cond_signal(pthread_cond_t *cond);

唤醒所有等待的线程:

int pthread_cond_broadcast(pthread_cond_t *cond);

验证线程同步代码:
一个Master控制其他线程,条件变量+锁,然后将等待的线程唤醒

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

using namespace std;

pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;//条件变量
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;//互斥锁
void *MasterCore(void *args)
{
    string name = static_cast<char *>(args);
    while (true)
    {
        sleep(3);
        cout << "Master开始工作.."<<endl;
        //将线程一个一个唤醒
        pthread_cond_signal(&gcond);
        cout<<"唤醒一个线程:"<<endl;
        sleep(1);
    }
    return nullptr;
}
void *SlaverCore(void *args)
{
    string name = static_cast<char *>(args);
    while (true)
    {
        //加锁
        pthread_mutex_lock(&gmutex);
        //设置条件变量,让线程在等待队列
        pthread_cond_wait(&gcond, &gmutex);
        cout << "线程在等待:" << name<<endl;
        //解锁
        pthread_mutex_unlock(&gmutex);
    }
    return nullptr;
}

void StartSlaver(vector<pthread_t> *tidptr, int num)
{
    for (int i = 0; i < num; i++)
    {
        pthread_t tid;
        char *name = new char[64];
        snprintf(name, 64, "thread-%d", i + 1);
        int n = pthread_create(&tid, nullptr, SlaverCore, name);
        if (n == 0)
        {
            cout << "Slaver Thread create success" << endl;
        }
        tidptr->emplace_back(tid);
    }
}
void StartMaster(vector<pthread_t> *tidptr)
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, MasterCore, (void *)"MasterThread");
    if (n == 0)
    {
        cout << "Master Thread create success" << endl;
    }
    tidptr->emplace_back(tid);
}
void WaitThread(vector<pthread_t> tidptr)
{
    for(auto& tid:tidptr)
    {
        pthread_join(tid, nullptr);
    }
}
int main()
{
    vector<pthread_t> tids;
    StartMaster(&tids);
    StartSlaver(&tids, 5);
    WaitThread(tids);
    return 0;
}

在这里插入图片描述

  • 29
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值