Linux系统编程:线程互斥与同步

目录

1.线程互斥

相关背景概念

互斥量mutex

互斥量实现原理

互斥量的封装

2.线程同步

概念

条件变量

生产者消费者模型

是什么

目的

优点

怎么做

基于BlockingQueue的生产者消费者模型


1.线程互斥

相关背景概念

• 共享资源

• 临界资源:多线程执⾏流被保护的共享的资源就叫做临界资源。

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

• 互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起

保护作⽤。

• 原⼦性(后⾯讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,

要么未完成。

互斥量mutex

• ⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量

归属单个线程,其他线程⽆法获得这种变量。

• 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完

成线程之间的交互。

• 多个线程并发的操作共享变量,会带来⼀些问题。

例如:

操作共享变量出问题的售票系统demo

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

int ticket=100;

void*route(void*args)
{
    std::string name=static_cast<const char*>(args);
    while(1){
        if(ticket>0)
        {
            usleep(1000);
            std::cout<<name<<"sells ticke:"<<ticket<<std::endl;
            --ticket;
        }
        else break;
    }
    return nullptr;
}

int main()
{
    pthread_t t1,t2,t3,t4;

    pthread_create(&t1,nullptr,route,(void*)"thread-1");
    pthread_create(&t2,nullptr,route,(void*)"thread-2");
    pthread_create(&t3,nullptr,route,(void*)"thread-3");
    pthread_create(&t4,nullptr,route,(void*)"thread-4");

    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    pthread_join(t4,nullptr);

    return 0;
}

输出结果发现ticket小于0的时候也打印了

问题引发原因有三点:

• if 语句检查不是原子性的,判断条件为真以后,代码可以并发的切换到其他线程。

• usleep 这个模拟漫⻓业务的过程,在这个漫⻓的业务过程中,可能有很多个线程会进⼊该代码段。

(以上两种情况会导致:多个线程可能同时访问临界区导致超卖或者数据不一致。)

• --ticket 操作本⾝就不是⼀个原⼦操作。

-- 操作并不是原⼦操作,⽽是对应三条汇编指令:

• load :将共享变量ticket从内存加载到寄存器中。

• update : 更新寄存器⾥⾯的值,执⾏-1操作。

• store :将新值,从寄存器写回共享变量ticket的内存地址。

要解决以上问题,需要做到三点:

• 代码必须要有互斥⾏为:当代码进⼊临界区执⾏时,不允许其他线程进⼊该临界区。

• 如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执⾏,那么只能允许⼀个线程

进⼊该临界区。(互斥性)

• 如果线程不在临界区中执⾏,那么该线程不能阻⽌其他线程进⼊临界区。

要做到这三点,本质上就是需要一把“锁”。Linux中提供的这把锁叫做互斥量/互斥锁。

互斥量本质就是一个结构体

互斥量的接口:

初始化互斥量

初始化互斥量有两种方法:

方法一:静态初始化(编译时初始化)

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

PTHREAD_MUTEX_INITIALIZER 是 POSIX 线程库中用于静态初始化互斥锁(mutex)的宏定义,它提供了一种简单的方式来初始化具有默认属性的互斥锁。

注:

  • 适用于全局或静态存储期

  • 简单、无需显式销毁(程序退出时自动清理)

缺点:

不能设置互斥量属性,只能设置为默认属性。

方法二:动态初始化(运行时初始化)

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL

注:

  • 可以设置各种互斥锁属性

  • 需要显式调用pthread_mutex_destroy()释放资源

销毁互斥量

• 使⽤ 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);
返回值:成功返回0,失败返回错误号

调⽤ pthread_ lock 时,可能会遇到以下情况:

• 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功

发起函数调⽤时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到

互斥量,那么pthread_ lock调⽤会陷⼊阻塞(执⾏流被挂起),等待互斥量解锁。

改进上面的售票系统:

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

int ticket = 100;
pthread_mutex_t mutex;

void *route(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (1)
    {
        //上锁
        pthread_mutex_lock(&mutex);
        if (ticket > 0)
        {
            usleep(1000);
            std::cout << name << "sells ticke:" << ticket << std::endl;
            --ticket;
            //解锁
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            //解锁
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_t t1, t2, t3, t4;

    //初始化互斥量
    pthread_mutex_init(&mutex, NULL);

    pthread_create(&t1, nullptr, route, (void *)"thread-1");
    pthread_create(&t2, nullptr, route, (void *)"thread-2");
    pthread_create(&t3, nullptr, route, (void *)"thread-3");
    pthread_create(&t4, nullptr, route, (void *)"thread-4");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);

    //互斥量销毁
    pthread_mutex_destroy(&mutex);

    return 0;
}

互斥量实现原理

• 经过上⾯的例⼦,我们发现单纯的进行 i++ 或者 ++i 都不是原⼦的,有可能会有数据⼀致性

问题。

• 为了实现互斥锁操作,⼤多数体系结构都提供了swap或exchange指令,该指令的作⽤是把寄存器和

内存单元的数据相交换,由于只有⼀条指令,保证了原⼦性,即使是多处理器平台,访问内存的 总线周

期也有先后,⼀个处理器上的交换指令执⾏时另⼀个处理器的交换指令只能等待总线周期。

我们来分析一下下面这段lock和unlock的伪代码:

A 进程 ,mov 第一步将 0 放入寄存器中,xchgb 第二步将寄存器和内存单元中的数据交换,假设内存单元中的数据为 1,寄存器中的初始数据为 0,交换之后内存中的数据为 0,寄存器中的数据为 1,代表申请到了锁,此时由于时间片的到来,CPU调度另一个 B 进程,B 进程第一步也是将 0 放入寄存器中,第二步进行交换,内存中的数据和寄存器中的数据都是 0,代表没有申请到锁,if 判断的时候,就会将 B 进程挂起,当再度调度 A 进程时,if 条件为真,就会访问临界区的资源。如此只有持有锁的进程才能访问临界区,避免了线程并发执行临界区内容,保证了各线程对与临界区代码的互斥性。

交换的本质:就是把1这个数字,以非拷贝的形式,从共享(内存中)变成私有。(到寄存器)谁抢到共享区的1,谁就上锁。

解锁:把1重新写入共享区(内存mutex),唤醒等待Mutex的线程即可。恰好下一个等待的线程goto lock,将寄存器的数值写为0,拿到共享区的1,和寄存器交换,这个线程进入临界区进行访问。

互斥量的封装

Mutex.hpp

#pragma once

#include<iostream>
#include<mutex>
#include<pthread.h>
class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_lock,nullptr);
    }
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;
};

class LockGuard
{
public:
    LockGuard(Mutex*_mutex):_mutexp(_mutex)
    {
        _mutex->Lock();
    }
    ~LockGuard()
    {
        _mutexp->Unlock();
    }
private:
    Mutex *_mutexp;
};

test.cc

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include "Mutex.hpp"
int ticket = 100;
Mutex lock;
void *route(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (1)
    {
        // 以一个花括号内容为单位(临界区),加锁,运行到末尾,自动调用析构解锁
        {
            LockGuard lcokguard(&lock); // RAII风格加锁
            if (ticket > 0)
            {
                usleep(1000);
                std::cout << name << "sells ticke:" << ticket << std::endl;
                --ticket;
            }
            else
            {
                break;
            }
        }
        //other code...
    }
    return nullptr;
}

int main()
{
    pthread_t t1, t2, t3, t4;

    pthread_create(&t1, nullptr, route, (void *)"thread-1");
    pthread_create(&t2, nullptr, route, (void *)"thread-2");
    pthread_create(&t3, nullptr, route, (void *)"thread-3");
    pthread_create(&t4, nullptr, route, (void *)"thread-4");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);

    return 0;
}

2.线程同步

概念

假如现在有一个超级自习室,里面有空调,一次只能进去一个人,有许多人都想去这个自习室自习,你也想去,所以,你很早就起来了,拿着自习室的钥匙进去学习了,外面有许多人想进去但是进不去,只能在外面等你自习完出来才能进去。你已经自习了很长时间了,现在想出来透透气,但是你又不想归还钥匙,因为等一下还要进去,所以你在外面停了几分钟,然后又进去了,如此循环往复许多次,让其他人始终得不到进去的机会。这虽然没有错,但是不合理。满足了互斥的条件,但资源没有得到充分的利用,导致效率低下。

其它线程因为申请不到锁,导致其它线程无法执行,进而导致饥饿问题。所以,为了CPU公平公正的调度每一个线程,仅仅满足互斥是不够的,还需要满足以下条件:

1.凡是从自习室出来的,归还钥匙之后不能立即申请。

2.外面的人排好队,出来的人必须排到队列尾部。

也就是,在临界资源安全的情况下,让不同的线程访问临界资源,具有一定的顺序性,叫做线程同步

条件变量

• 当⼀个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

• 例如⼀个线程访问队列时,发现队列为空,它只能等待,只到其它线程将⼀个节点添加到队列

中。这种情况就需要⽤到条件变量。

初始化

静态初始化

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

动态初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t
*restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL

销毁

int pthread_cond_destroy(pthread_cond_t *cond)

等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict
mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量

唤醒等待

//唤醒一个进程
int pthread_cond_signal(pthread_cond_t *cond);
//唤醒所有进程
int pthread_cond_broadcast(pthread_cond_t *cond);

条件变量本质上可以用一个结构体实现,里面有条件变量的状态,等待队列等。

代码演示:

结果如下:

生产者消费者模型

是什么

生活中,哪些场景是符合生产和消费场景?比如说,学校食堂,超市…,这里我们以超市为例,带大家了解什么是生产和消费模型。

超市是生产者吗?不是,超市只是一个生产者和消费者之间的一个交易场所,生产者以火腿肠供应商为例,消费者以学生为例,如果没有超市,学生要去买火腿肠,还得跑到供应商去买,而供应商一般都在遥远的郊区外,你打车过去都得不少钱,而供应商为了你一个人去生产商品,也是需要开机器,有成本的。所以,超市的存在减少了生产和消费过程中产生的成本。

假如现在临近过年了,供应商为了满足市场上人们的需求,他就要在临近过年之前生产足够多的商品,将这些商品在超市上架,等过年了就可以给工人放假,而学生在临近过年之前对商品没有足够的需求,等到过年了,才需要大量的商品。所以,生产者和消费者之间的供需关系并不一致,超市的存在就可以支持生产和消费的忙闲不均。

如果没有超市的存在,消费者需要商品的时候就会要求供应商生产,不需要商品的时候供应商就不用生产了,这是一种强耦合的关系,但是有了超市的存在,供应商就可以生产出一批商品,囤积在超市里,满足市场的需求,这不就相当于解耦合了吗!

目的

⽣产者消费者模式就是通过⼀个容器来解决⽣产者和消费者的强耦合问题。⽣产者和消费者彼此之间不直接通讯,⽽通过阻塞队列来进⾏通讯,所以⽣产者⽣产完数据之后不⽤等待消费者处理,直接扔给阻塞队列,消费者不找⽣产者要数据,⽽是直接从阻塞队列⾥取,阻塞队列就相当于⼀个缓冲区,平衡了⽣产者和消费者的处理能⼒。这个阻塞队列就是⽤来给⽣产者和消费者解耦的。

优点

解耦

⽀持并发

⽀持忙闲不均

怎么做

本质是维护生产者和消费者之间的关系。

生产者与生产者:竞争关系,也就是互斥关系。

消费者与消费者:资源不足的情况下,消费者需要竞争资源,也就是互斥关系。

生产者与消费者:互斥和同步关系。生产者和消费者本质就是线程,在生产者生产数据的时候,消费者如果此时读取数据就会造成数据混乱,所以写数据和读数据是分隔开的,不论是写数据还是读数据都要对临界区进行加锁解锁,保证只有一个线程在执行临界区代码,这是互斥;消费者得等待生产者生产完数据再进行消费数据,这种顺序性是同步。

生产者消费者模型遵循“321”原则(实际上并没有这个原则,方便记忆而已huaji)

3种关系(生产者与生产者,消费者与消费者,生产者与消费者)

2种角色(生产者,消费者)

1个交易场所(一种数据结构对象)

基于BlockingQueue的生产者消费者模型

在多线程编程中阻塞队列(Blocking Queue)是⼀种常⽤于实现⽣产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放⼊了元素;当队列满时,往队列⾥存放元素的操作也会被阻塞,直到有元素被从队列中取出。

(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

demo:

BlockQueue.hpp

#pragma once

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

const static u_int32_t gcap=5;   

template<typename T>
class BlockQueue
{
private:
    bool IsFull()
    {
        return _bq.size()>=_cap;
    }
    bool IsEmpty()
    {
        return _bq.empty();
    }
public:
    BlockQueue(u_int32_t cap=gcap):_cap(cap),_c_wait_num(0),_p_wait_num(0)
    {
        pthread_mutex_init(&_lock,nullptr);
        pthread_cond_init(&_c_cond,nullptr);
        pthread_cond_init(&_p_cond,nullptr);
    }
    //纯输入const &
    //生产
    void Enqueue(const T  &in)
    {
        pthread_mutex_lock(&_lock);
        //还需判断队列未满,方可生产
        //while避免pthread_cond_wait调用失败 or 伪唤醒情况
        while(IsFull())
        {
            //问题1:进行等待的时候,是处于临界区中等待。但此时是持有锁的,所以需要自动让线程释放锁!
            //问题2:为什么要设计成在临界区内部等待?为什么不可以先判断再申请锁??
            //因为要先判断队列是否为满,才能进行生产。判断队列是否为满本身是在访问临界资源。
            //也就是说,判断队列是否为满,必须在临界区内部判断。
            //所以生产者必须先申请锁,在临界区内部判断
            //判断为满的结果,需要等待的结构也一定在临界区内部
            //所以,等待时,在临界区内部释放锁是必然的。
            //所以,锁作为了pthread_cond_wait的参数
            
            _p_wait_num++;
            pthread_cond_wait(&_p_cond,&_lock);//特征1:自动释放锁!特征2:唤醒后自动重新竞争并持有锁
            _p_wait_num--;
            //当我们被唤醒的时候,就一定又从这个位置唤醒了
            //是在临界区内被唤醒的

        }
        //队列未满->生产
        _bq.push(in);
        if(_c_wait_num>0)
            pthread_cond_signal(&_c_cond);  //唤醒消费者
        pthread_mutex_unlock(&_lock);
    }
    //纯输出指针(即输入又输出的直接用引用)
    //消费
    void Pop(T*out)
    {
        pthread_mutex_lock(&_lock);
        while(IsEmpty())
        {
            _c_wait_num++;
            pthread_cond_wait(&_c_cond,&_lock);
            _c_wait_num--;
        }

        *out=_bq.front();
        _bq.pop();
        if(_p_wait_num>0)
            pthread_cond_signal(&_p_cond);  //唤醒生产者
        pthread_mutex_unlock(&_lock);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_lock);
        pthread_cond_destroy(&_c_cond);
        pthread_cond_destroy(&_p_cond);
    }
private:
    //临界资源
    std::queue<T> _bq; //blockqueue
    u_int32_t _cap;     //容量

    pthread_mutex_t _lock;
    pthread_cond_t _c_cond; //消费者使用的条件变量
    pthread_cond_t _p_cond; //生产者使用的条件变量

    int _c_wait_num;    //当前消费者等待的个数
    int _p_wait_num;    //当前生产者等待的个数
};

main.cc

#include"BlockQueue.hpp"
#include<unistd.h>
struct  ThreadData
{
    BlockQueue<int> *bq;
    std::string name;
};


void *consumer(void*args)
{
    ThreadData *td=static_cast<ThreadData*>(args);
    while(true)
    {
        //sleep(1);
        int data=0;
        td->bq->Pop(&data);
        std::cout<<"消费者消费了一个数据:"<<data<<std::endl;
    }
}

void *productor(void*args)
{
    ThreadData*td=static_cast<ThreadData*>(args);
    int data=1;
    while(true)
    {
        td->bq->Enqueue(data);
        std::cout<<"生产者生产了一个数据:"<<data++<<std::endl;
    }
}

int main()
{
    BlockQueue<int> *bq=new BlockQueue<int>();
    pthread_t c,p;

    ThreadData ctd={bq,"消费者"};
    pthread_create(&c,nullptr,consumer,(void*)&ctd);

    ThreadData ptd={bq,"生产者"};
    pthread_create(&p,nullptr,productor,(void*)&ptd);

    pthread_join(c,nullptr);
    pthread_join(p,nullptr);

    delete bq;
    return 0;
}

makefile

blockqueue_test:main.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f blockqueue_test

上面是单个生产者对单个消费者,多对多这样修改即可

问题:生产者和消费者竞争同一把锁,同一时间只允许一个线程进入临界区访问资源,生产者和消费者的过程也是串行的,那生产消费模型高效在哪?

向阻塞队列进行生产和消费过程当然得是串行的,不串行会导致并发访问数据,但是交易场所的数据从哪里来呢?顾客在超市里消费时,供应商在快速生产火腿肠,顾客买到商品之后,这并不是真正的消费,商品必须被消费者使用之后,才算真正的消费,而在超市里购买商品只是生产者消费者模型的一个小过程而已,在顾客真正消费商品时,供应商也在制造火腿肠,这个过程是并发的,所以说生产者消费者模型高效。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_dindong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值