linux线程 (2)——互斥、同步、基于Blockqueue的生产者消费者模型

目录

 一.多线程并发问题

并发问题

二、互斥锁

问题1:锁的申请      

问题2:加锁时切换

问题3:加锁和解锁究竟怎么实现原子性?

三、Linux线程同步

1.线程同步

2.生产者消费者模型

        ①提高效率。

        ②解耦。

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

构造与析构

produce与consume 

ProWaitCon与ConWaitPro

wakePro与wakeCon 

lock、unlock等

运行

4.再认识生产者消费者模型


 一.多线程并发问题

并发问题

        Hello!我们又见面了,紧接上篇文章,我希望我能把我学到的写好,既充实自己,也帮助你们。冲!                 

        为什么线程要互斥,因为多个线程在访问同一临界资源的时候会导致,不可意料的错误。

        什么是临界资源?多个执行流都能看到并访问的资源,叫做临界资源。

        伴随临界资源这个概念的还有临界区,在执行流中访问临界资源的代码则为临界区。

        至于不可意料的错误,我们可以演示一下。

        我们调用两个线程分别对全局变量count++,一个线程要在自己的逻辑里加我们给定的次数,所以最后的结果应是count = 2*我们给定的次数。这个count就为临界资源,线程内的代码count++则为临界资源。

代码:

int count = 0;
void *fuc(void *argc)
{
    for (int i = 0; i < *(int *)argc; i++)
    {
        count++;
    }
}
int main()
{
    int num = 500000;
    pthread_t tid;
    pthread_t tid1;
    pthread_create(&tid, NULL, fuc, (void *)&num);
    pthread_create(&tid1, NULL, fuc, (void *)&num);
    pthread_join(tid, NULL);
    pthread_join(tid1, NULL);
    cout << "The count is : " << count << endl;
    return 0;
}

结果:执行这么多次总是不尽如人意。linux线程 (2)——互斥、同步、基于Blockqueue的生产者消费者模型_第1张图片

        按理来说,我们给定的数为500000,最后的结果应为1000000.为什么?请往下看。

        count++这个简单语句,os究竟做了什么?

linux线程 (2)——互斥、同步、基于Blockqueue的生产者消费者模型_第2张图片

        ①将内存中的count的值加载到CPU中的寄存器中;

        ②将寄存器中的值经运算器(ALU)自增1;

        ③将寄存器的值放回内存中。

        在执行语句的任何地方,线程都可能被切走。

        CPU内的寄存器是被所有执行流共享的,但是寄存器中的数据是属于当前执行流的上下文数据。所以当该执行流被切走时,上下文会被保存。当该执行流被切回时,该上下文就会被恢复。

        理解这些知识,我们看图就可以理解了。

linux线程 (2)——互斥、同步、基于Blockqueue的生产者消费者模型_第3张图片

         

        当线程A将数据放入寄存器并被计算之后,突然被切走,则数值1就会被保存在线程A的上下文。这时线程B来了,它开始反复的将数据加加的过程并不断地更新内存中的值。但线程A被切回,线程A的上下文被恢复,将执行未执行的步骤,将数值1放回内存中,这时就会造成严重的问题。我线程B都已经跑了这么多次,但是当来回切换之后,内存中的值变成了数值1。这就是多个线程同时访问临界资源,会导致的不可意料的错误。

二、互斥锁

        解决上述问题我们需要互斥锁来解决。

        我们首先来用一下互斥锁来解决,剩下的细节会讲到。

代码:


int count = 0;

pthread_mutex_t mutex;

void *fuc(void *argc)
{
    for (int i = 0; i < *(int *)argc; i++)
    {
        pthread_mutex_lock(&mutex);
        count++;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    int num = 100000;
    pthread_mutex_init(&mutex, nullptr);

    pthread_t tid;
    pthread_t tid1;
    
    pthread_create(&tid, NULL, fuc, (void *)&num);
    pthread_create(&tid1, NULL, fuc, (void *)&num);
    
    pthread_join(tid, NULL);
    pthread_join(tid1, NULL);

    pthread_mutex_destroy(&mutex);
    cout << "The count is : " << count << endl;
    return 0;
}

另一种写法:

int count = 0;

int num = 100000;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

typedef struct ThreadDate
{
    string threadname;
    pthread_mutex_t *mutex_p;
} Td;

void *fuc(void *argc)
{
    Td *td = static_cast(argc);
    for (int i = 0; i < num; i++)
    {
        pthread_mutex_lock(td->mutex_p);
        count++;
        cout << "我是" << td->threadname << "我的线程ID是: " << pthread_self() << "当前count为 : " << count << endl;
        pthread_mutex_unlock(td->mutex_p);
    }
}

int main()
{
    Td *td1 = new Td();
    td1->threadname = "我是thread 1";
    td1->mutex_p = &mutex;

    Td *td2 = new Td();
    td2->threadname = "我是thread 2";
    td2->mutex_p = &mutex;

    pthread_t tid;
    pthread_t tid1;

    pthread_create(&tid, NULL, fuc, (void *)td1);
    pthread_create(&tid1, NULL, fuc, (void *)td2);

    pthread_join(tid, NULL);
    pthread_join(tid1, NULL);

    pthread_mutex_destroy(&mutex);
    
    delete td1;
    delete td2;
    cout << "The count is : " << count << endl;
    return 0;
}

还有一种做法:形如智能指针,释放Mutex_smart_ptr,会调用自己的析构,析构中调用unlock,随即调用Mutex的析构函数。

class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_mutex, nullptr);
    }
    void lock()
    {
        pthread_mutex_lock(&_mutex);
    }
    void unlock()
    {
        pthread_mutex_unlock(&_mutex);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_mutex);
    }

private:
    pthread_mutex_t _mutex;
};

class Mutex_smart_ptr
{
public:
    Mutex_smart_ptr(Mutex *mutex) 
        :_mutex(mutex)
    {                                                                           
        _mutex->lock();
    }
    ~Mutex_smart_ptr()
    {
        _mutex->unlock();
    }
private:
    Mutex *_mutex;
};


int count = 0;

int num = 10000;

Mutex mutex;

void *fuc()
{
    Mutex_smart_ptr msp(&mutex);
    cout<<"I'm "<(argc);
    for (int i = 0; i < num; i++)
    {
        fuc();
    }
}

int main()
{
    pthread_t tid;
    pthread_t tid1;

    pthread_create(&tid, NULL, func, (void *)"我是线程 1 ");
    pthread_create(&tid1, NULL, func, (void *)"我是线程 2 ");

    pthread_join(tid, NULL);
    pthread_join(tid1, NULL);

    cout << "The count is : " << count << endl;
    return 0;
}

结果:

linux线程 (2)——互斥、同步、基于Blockqueue的生产者消费者模型_第4张图片

       利用形如smart_ptr这样的特性,我们还可以这样做:
 

        注意:加锁只要对临界区资源,并且加锁力度越细越好;

                   加锁的本质是让线程通过临界区访问临界资源的时候串行化;

                   加锁是一套规范,只要一个线程加了,其他线程也要加。

问题1:锁的申请      

        我又有问题了,既然线程要访问临界资源,要通过对临界区加锁来访问,这些线程都需要去申请锁,那么这个锁岂不是也是临界资源,大家能看到也能访问,如何保证我们在申请锁这个临界资源的时候,不会出现多线程并发的问题呢?

        其实线程在竞争和申请锁的这个过程是原子的!

        原子性:指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行。

        所谓原子是一个性质,这个操作具有原子性,即为这个操作要么不间断的去做直到做完,要么没做。对应线程竞争和申请锁的这个过程,就是要么申请到了,要么就没有申请,不存在申请一半被切走。

问题2:加锁时切换

        我还有问题,既然我申请到锁了,在执行上锁和解锁之间的代码时,线程会不会被切换?

        会切换。但是在切换之后不会有任何线程进入上锁的部分。因为每个线程进入临界区都必须申请锁。一个线程申请到锁之后,被切换走,是带着锁走的。一旦一个线程持有了锁,对其他线程而言,只有持有锁的线程释放锁,才对它们有意义。由这样看来说明具有锁的线程访问临界区也是具有原子性的。

问题3:加锁和解锁究竟怎么实现原子性?

        为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用时把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。

linux线程 (2)——互斥、同步、基于Blockqueue的生产者消费者模型_第5张图片

        %al:CPU中的寄存器。

        mutex:内存中的锁的数量。

        假设mutex只有一个,线程A将0放入CPU中的寄存器中,再将内存中的mutex的1的值和寄存器中的0交换(exchange),即完成了对锁的申请。线程B来了,将0放入CPU中的寄存器中,再将内存中的mutex的0(线程A已经申请到了锁)的值和寄存器中的0交换(exchange),再条件判断之后,挂起等待。之所以加锁是原子性的,因为在申请mutex时只有一条指令,swap或exchange指令,将这一条语句执行完毕,即代表申请所成功。

        mutex是所有线程都共享的,竞争的过程,就是在将资源从共享的属性变成单一线程私有。

三、Linux线程同步

1.线程同步

        线程互斥,它可以让临界资源安全的被访问。但是它很有可能导致某个执行流一直处于饥饿状态。就是一个执行流长时间得不到资源,处于挂起状态。所以会引出一个方法叫做线程同步,让线程访问临界资源时具有顺序性,来防止饥饿,让线程具有协同访问临界资源。

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

        同步究竟要解决什么?

        当有很多个线程同时去申请锁,具有锁的线程去访问临界资源。各各线程都去申请并使用完释放,但有几个线程优先级不高,死活申请不到锁,就一直没有办法访问临界资源,从而处一直于饥饿状态。线程同步就是来解决这个问题的。

        实现这个同步的过程又要引入一个概念,叫做条件变量。没有条件变量之前,当一个锁被使用完毕,所有线程都会被唤醒去申请锁,而有了条件变量,就可以选择性的去唤醒线程。由系统唤醒线程变为由程序员自己唤醒线程。

        首先,我们要清楚,同步的目的是防止线程饥饿(线程饥饿又涉及到了锁),而实现同步要使用条件变量,所以使用条件变量要和锁一块使用。

        我们先使用一下看看条件变量的威力。

代码:


//条件变量
pthread_cond_t cond;
//mutex
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//线程运行的函数
void *fuc(void *argc)
{
    while (1)
    {
        //等待唤醒
        pthread_cond_wait(&cond, &mutex);
        cout << "my thread id : " << pthread_self() << endl;
    }
}

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

    pthread_create(&t1, nullptr, fuc, NULL);
    pthread_create(&t2, nullptr, fuc, NULL);
    pthread_create(&t3, nullptr, fuc, NULL);

    while (1)
    {
        char a;
        cout << "请输入n/q : ";
        cin >> a;
        //n为next q为quit
        if (a == 'n')
        {
            //通过条件变量唤醒一个线程
            pthread_cond_signal(&cond);
        }
        else
        {
            break;
        }

        sleep(0.5);
    }

    pthread_cancel(t1);
    pthread_cancel(t2);
    pthread_cancel(t3);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_cond_destroy(&cond);
    return 0;
}

结果:

linux线程 (2)——互斥、同步、基于Blockqueue的生产者消费者模型_第6张图片

        从结果看出明显线程在被排队唤醒。这就是使用条件变量,来达到线程同步的效果。

        注意这次代码有问题,因为在pthread_cond_signal()之后,代码会跳到pthread_cond_wait()之处,并为当前线程申请锁,然而只有一把锁,当前线程申请到之后,并且我们的代码没有在线程进行完之后释放锁,所以其他线程只会一直阻塞,然而当线程退出时,则只有一个线程退出,那是因为其他线程还在阻塞中,无法退出。

解决方案:

//条件变量
pthread_cond_t cond;
//mutex
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//线程运行的函数
void *fuc(void *argc)
{
    while (1)
    {
        pthread_mutex_lock(&mutex);
        //等待唤醒
        pthread_cond_wait(&cond, &mutex);
        cout << "my thread id : " << pthread_self() << endl;
        pthread_mutex_unlock(&mutex);
        break;
    }
}

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

    pthread_create(&t1, nullptr, fuc, NULL);
    pthread_create(&t2, nullptr, fuc, NULL);
    pthread_create(&t3, nullptr, fuc, NULL);

    while (1)
    {
        char a;
        cout << "请输入n/q : ";
        cin >> a;
        //n为next q为quit
        if (a == 'n')
        {
            //通过条件变量唤醒一个线程
            pthread_cond_signal(&cond);
        }
        else
        {
            break;
        }

        sleep(0.5);
    }
    cout<<"ready to cancel"<

        第一:要将所有的线程都唤醒,因为线程因pthread_cond_wait()而阻塞,一旦有资源可以申请了,这些阻塞的线程并不会自己去运行。唤醒的目的是让线程都去竞争锁,并可以退出。

        第二:要在pthread_cond_wait()前加锁,pthread_cond_wait()会自动解锁

                   要在pthread_cond_wait()后解锁,pthread_cond_wait()会自动加锁,如果没有解锁,那当前线程就算运行完了,也会一直占用锁,导致其他线程苦苦等待。

结果:linux线程 (2)——互斥、同步、基于Blockqueue的生产者消费者模型_第7张图片

2.生产者消费者模型

        在这个模型中我们可以更深入了解条件变量。

        什么是生产者消费者模型?

        说的通俗易懂的是,我们去超市买东西,我们就是消费者,制造商制作东西交给超市贩卖,制造商就是生产者。那超市是个什么角色,它是被生产者和消费者都能访问的一块区域。

     linux线程 (2)——互斥、同步、基于Blockqueue的生产者消费者模型_第8张图片

        这个模型在我们生活到处都是,那么这样做有什么好处呢?

        ①提高效率。

        生产者只需去生产将生产出来的事物直接放到超市,不用积压在工厂内部,便于再次生产;消费者只用去超市买,不用去工厂等待生产,买到就可以使用。 

        ②解耦。

        消费者只管消费,生产者只管生产。通过一个容器来解决生产者和消费者的强耦和关系。

        既然这个超市是临界资源,我们就要指定规则去保护它。如何保护它,先要理解访问它的都是什么关系。

        ①消费者和消费者:竞争关系 —— 互斥 

        ②生产者和生产者:竞争关系 —— 互斥 

        ③生产者和消费者:同步、互斥    保证消费者消费的必定是完整的资源,所以在访问这个区域时生产者和消费者必定是互斥的。消费者必须要等待生产者生产出来才能消费,而生产者必须等待消费者将临界资源中的资源消耗才能生产,我们用到同步来实现这种现象。 

        简单的可以理解为:

        3个关系:3个互斥关系

        2个角色:消费者生产者分别为两种身份的线程

        1个区域:交易产所

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

        在多线程编程中阻塞队列是一种常用于实现生产者消费者模型的数据结构。与普通队列不一样的是,当队列为空时,从队列拿出元素的操作会被阻塞,直到其中被放入元素;当队列为满的时候,向队列中放入元素的操作会被阻塞,直到队列有空余的位置。linux线程 (2)——互斥、同步、基于Blockqueue的生产者消费者模型_第9张图片

阻塞队列框架:

template 
class Blockqueue
{
public:
    // 生产
    void produce()
    {
        // 加锁
        // 判断是否有条件进行生产
        // 解锁
    }
    // 消费
    void consume()
    {
        // 加锁
        // 判断是否有条件进行消费
        // 解锁
    }
private:
    // 队列
    queue _q;
    // size
    uint32_t _capacity;
    // mutex
    pthread_mutex_t _mutex;
};

        话说上面都在聊条件变量,这里的阻塞队列仅仅生产和消费跟条件变量有什么关系。

        这里我只有一把锁,如果当前队列是满的,但是消费者没有去消费,生产者判断队列为满无法生产,消费者还是没有去消费,生产者再次如同轮询检测般不断加锁去判断解锁,周而复始,就会导致一个线程恶意竞争锁资源,而导致另一个线程饥饿的现象。有可能消费者不去消费就是生产者每次都能申请到锁还判断为满,导致消费者消费不了,恶性循环。这时就要有我大名鼎鼎的条件变量出来办事了,生产者生产成功唤醒消费者,消费者消费成功唤醒生产者,多么和谐。

    // 让消费者等待的条件变量
    pthread_cond_t _concond;
    // 让生产者等待的条件变量
    pthread_cond_t _procond;

        现在我们向框架内填一点细节。

构造与析构

    Blockqueue(const uint32_t cap = capacity)
        : _capacity(cap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_concond, nullptr);
        pthread_cond_init(&_procond, nullptr);
    }
    ~Blockqueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_concond);
        pthread_cond_destroy(&_procond);
    }

produce与consume 

    Blockqueue(const uint32_t cap = capacity)
        : (_capacity = cap)
    {
        pthread_mutex_init(&mutex, nullptr);
        pthread_cond_init(&_concond, nullptr);
        pthread_cond_init(&_procond, nullptr);
    }
    ~Blockqueue()
    {
        pthread_mutex_destroy(&mutex, nullptr);
        pthread_cond_destroy(&_concond, nullptr);
        pthread_cond_destroy(&_procond, nullptr);
    }

    // 生产
    void produce(const T &in)
    {
        // 加锁
        lock();
        // 判断是否有条件进行生产
        // 不能生产
        if (isFull())
        {
            // 等待消费者将数据消费,留出空位
            ProWaitCon();
        }
        // 能生产
        _q.push(in);
        // 解锁
        unlock();
        // 唤醒消费者
        wakeCon();
    }

    // 消费
    T consume()
    {
        // 加锁
        lock();
        // 判断是否有条件进行消费
        // 不能消费
        if (isEmpty())
        {
            // 等待生产者将数据生产出来
            ConWaitPro();
        }
        // 能消费
        T out = _q.front();
        _q.pop();
        // 解锁
        unlock();
        // 唤醒生产者
        wakePro();
        return out;
    } 

ProWaitCon与ConWaitPro

        线程在阻塞时,mutex会自动释放

        能自动释放,当被唤醒时也会自动上锁

    void ProWaitCon()
    {
        // 线程在阻塞时,mutex会自动释放
        pthread_cond_wait(&_procond, &mutex);
        // 能自动释放,当被唤醒时也会自动上锁
    }
    void ConWaitPro()
    {
        pthread_cond_wait(&_concond, &mutex);
    }

wakePro与wakeCon 

    void wakePro()
    {
        pthread_cond_signal(&_procond);
    }
    void wakeCon()
    {
        pthread_cond_signal(&_concond);
    }

lock、unlock等

    void lock()
    {
        pthread_mutex_lock(&_mutex);
    }
    void unlock()
    {
        pthread_mutex_unlock(&_mutex);
    }
    bool isEmpty()
    {
        return _q.empty();
    }
    bool isFull()
    {
        return _capacity == _q.size();
    }
    void push(const T& in)
    {
        _q.push(in);
    }
    T pop()
    {
        T temp = _q.front();
        _q.pop();
        return temp;
    }

运行

        我们先开两个线程去试验下

linux线程 (2)——互斥、同步、基于Blockqueue的生产者消费者模型_第10张图片

结果:

linux线程 (2)——互斥、同步、基于Blockqueue的生产者消费者模型_第11张图片

         

        大家有没有发现代码还有一个地方有BUG,就在此处。       

linux线程 (2)——互斥、同步、基于Blockqueue的生产者消费者模型_第12张图片

         ConWaitPro函数中是去调用pthread+cond_wait()这个函数,既然是函数那就有调用失败的时候,那调用失败了,当前线程就不会挂起,从而继续向下执行,但是当前的队列是空的,从空的队列拿数据当然会出现问题。

         还有如果我们不小心在后续的敲代码的不小心将线程唤醒,或者线程在条件不满足的情况下被伪唤醒,这时不就又出现了BUG。同样生产者这样的问题也会出现。

        肿么办?

        其实将条件稍微改变一下就可以解决问题。

        while (isFull())
        {
            // 等待消费者将数据消费出来
            ProWaitCon();
        }        
        while (isEmpty())
        {
            // 等待生产者将数据生产出来
            ConWaitPro();
        }

        一旦你被唤醒别着急往下走,先判断是否满足条件再继续往下走。

4.再认识生产者消费者模型

        我们都说这个生产者和消费者并发运行,但是我们看到的只是生产者和消费者互斥的去访问临界资源,究竟线程并发体现在什么地方。

        我们再实现一个案例来看出生产者消费者模型的优点 解耦 支持并发 支持忙闲不均。

        既然我们设置的Blockqueue还有模板,我们可以自己定义一个类,传入Blockqueue里,让生产者生产这个类中的元素,由消费者消费。而并发就体现在生产者制造数据(不是将数据塞入临界资源的过程)的同时消费者将数据取出并处理数据的过程,此时它们就是并发的,而在临界区中它们又是串行的。且看代码。

class Task
{
public:
    Task(int one = 0, int two = 0, char opr = '0')
        : _elem1(one), _elem2(two), _operator(opr)
    {
    }
    int operator()()
    {
        return calculate();
    }
    int calculate()
    {
        int result = 0;
        switch (_operator)
        {
        case '+':
            result = _elem1 + _elem2;
            break;
        case '-':
            result = _elem1 - _elem2;
            break;
        case '*':
            result = _elem1 * _elem2;
            break;
        case '/':
        {
            if (_elem2 == 0)
            {
                cout << "除零错误" << endl;
                result = INT32_MAX;
            }
            else
            {
                result = _elem1 / _elem2;
            }
        }
        break;
        case '%':
        {
            if (_elem2 == 0)
            {
                cout << "模零错误" << endl;
                result = INT32_MAX;
            }
            else
            {
                result = _elem1 % _elem2;
            }
        }
        break;
        default:
            cout << "错误输入,opertor : " << _operator << endl;
            break;
        }
        return result;
    }
    void getelem(int *one, int *two, char *opr)
    {
        *one = _elem1;
        *two = _elem2;
        *opr = _operator;
    }

private:
    int _elem1;
    int _elem2;
    char _operator;
};
const std::string opera = "+-*/%";

void *concumer(void *queue)
{
    Blockqueue *bqp = static_cast *>(queue);
    while (1)
    {
        sleep(1);
        Task t = bqp->consume();
        int result = t();
        int one, two;
        char opr;
        t.getelem(&one, &two, &opr);
        cout << "Time : " << (unsigned long)time(nullptr) << ' ' << "thread : " << pthread_self()
             << ' ' << "consume data success,data is : " << one << ' ' << opr << ' ' << two << " = " << result << endl;
    }
}
void *producer(void *queue)
{
    Blockqueue *bqp = static_cast *>(queue);
    while (1)
    {
        // 制作数据
        int one = rand() % 20;
        int two = rand() % 10;
        char opr = opera[rand() % opera.size()];
        // 生产数据
        Task t(one, two, opr);
        bqp->produce(t);
        cout << "Time : " << (unsigned long)time(nullptr) << ' ' << "thread : " << pthread_self()
             << ' ' << "produce data success,data is : " << one << ' ' << opr << ' ' << two << " -> ?" << endl;
        sleep(0.5); 
    }
}

int main()
{ // 种随机数种子
    srand((unsigned int)time(nullptr) ^ getpid());

    Blockqueue bq;
    pthread_t con, prd;

    pthread_create(&con, nullptr, concumer, &bq);
    pthread_create(&prd, nullptr, producer, &bq);

    pthread_join(con, nullptr);
    pthread_join(prd, nullptr);

    return 0;
}

结果:

linux线程 (2)——互斥、同步、基于Blockqueue的生产者消费者模型_第13张图片

        限于篇幅,我们下一篇再见,下一篇文章会讲到基于环形队列的生产消费模型。

        感谢观看,再见。

你可能感兴趣的:(Linux,c++,linux)