Linux_多线程与锁

17 篇文章 1 订阅

  • 在前面的博客【Linux_深究多线程—>link】中已经讲了有关多线程操作的有关操作和锁的基本概念。这章将接着上一章的内容对线程与锁的有关概念和操作继续深究。

1 .常见锁的概念

1.1 死锁

  • 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

1.2 死锁四个必要条件

  1. 互斥条件:一个资源每次只能被一个执行流使用
  2. 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  4. 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

注意:要形成死锁,四个条件缺一不可。所以解决死锁的方法就是破坏四个必要条件中的一个以上,常见有银行家算法/死锁检测算法等。

1.3 避免死锁

  1. 破坏死锁的四个必要条件
  2. 加锁顺序一致
  3. 避免锁未释放的场景
  4. 资源一次性分配

问题

  • 一个线程一把锁有没有可能形成死锁呢
  • 答:有,下面代码演示:
  • 在这里插入图片描述
  • 原因是同一把锁被申请两次,且该锁还没有被解锁,还被占着,自己和自己杠上。即出现死锁。

2. Linux线程同步

2.1 条件变量

  • 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
  • 同步存在是为了多线程协同高效完成某些事情
  • 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中,这种情况就需要用到条件变量。

2.2 同步概念与竞态条件

  • 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
  • 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解。

2.3 编码实现方式

  1. 如果条件不满足,等待,释放锁。
  2. 通知机制。

2.4 相关接口函数

2.4.1 初始化条件变量

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

参数

  • cond:要初始化的条件变量
  • attr:NULL

2.4.2 销毁条件变量

int pthread_cond_destroy(pthread_cond_t *cond)

2.4.3 等待条件满足

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

参数

  • cond:要在这个条件变量上等待
  • mutex:互斥量。

2.4.4 唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

2.5 代码示例

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
pthread_mutex_t lock;//创建锁
pthread_cond_t cond;//创建环境变量

void *run1(void *arg)//负责等待线程二发来信号开始执行
{
  const char *name=(char*)arg;
  while(1)
  {
    pthread_cond_wait(&cond,&lock);//等待
    printf("%s get singal\n",name);
  }
}
void *run2(void *arg)//负责给线程一发送信号
{
  const char *name=(char*)arg;
  while(1)
  {
    sleep(2);
    pthread_cond_signal(&cond);//唤醒等待
    printf("%s signal done!\n",name);
  }
}
int main()
{
  pthread_mutex_init(&lock,NULL);//初始化锁
  pthread_cond_init(&cond,NULL);//初始化环境变量
  pthread_t t1;
  pthread_t t2;

  pthread_create(&t1,NULL,run1,"thread1");//创造两个新线程
  pthread_create(&t2,NULL,run2,"thread2");

  pthread_join(t1,NULL);//等待两个线程退出
  pthread_join(t2,NULL);
  
  pthread_mutex_destroy(&lock);//销毁锁
  pthread_cond_destroy(&cond);//销毁环境变量

  return 0;
}

运行结果
在这里插入图片描述
问题

  • 为什么pthread_cond_wait()需要互斥量呢
  • 答:
  1. 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
  2. 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
  3. 简单来说,pthread_cond_wait()中还要带上锁是因为,如果该线程在等待时,此时应该将临界区进行解锁,让另外的线程在该线程等待时,可以进入到临界区,要不然它在等待时还一直加锁状态,就没意义。当别的进程给该进程发信号,唤醒它,这个时候从新加锁,访问临界区,保证其原子性。

3. 生产者消费者模型

问题

  • 为何要使用生产者消费者模型 ?
  • 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

生产者消费者模型优点

  • 解耦
  • 支持并发
  • 支持忙闲不均

3.1 图示详解

在这里插入图片描述

  • 基于BlockingQueue的生产者消费者模型 ,BlockingQueue 在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。
  • 其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)。
  • 生产者消费者“321”
  • 3->3种关系:生产者&消费者(互斥/同步),生产者&生产者(互斥),消费者&消费者(互斥)
  • 2->2种角色:生产者,消费者
  • 3->一个交易场所

3.2 代码示例

ps.h

#ifndef __QUEUE_BLOCK_H__
#define __QUEUE_BLOCK_H__
#include<unistd.h>
#include<iostream>
#include<pthread.h>
#include<queue>
using namespace std;
class Task
{
public:
  int x;
  int y;
public:
  Task(int _x,int _y):x(_x),y(_y)
  {}
  Task()
  {}
  int Run()
  {
    return x+y;
  }
  ~Task()
  {}
};
class BlockQueue
{
private:
  queue<Task> q;
  size_t cap;
  pthread_mutex_t lock;
  pthread_cond_t c_cond;
  pthread_cond_t p_cond;
public:
  //判满
  bool IsFull()
  {
    return q.size()>=cap;
  }
  //判空
  bool IsEmpty()
  {
    return q.empty();
  }
  //解锁
  void UnlockQueue()
  {
    pthread_mutex_unlock(&lock);
  }
  //唤醒消费者
  void WakeUpConsumer()
  {
    cout<<"WakeUpConsumer"<<endl;
    pthread_cond_signal(&c_cond);
  }
  //唤醒生产者
  void WakeUpProductor()
  {
    cout<<"WakeUpProductor"<<endl;
    pthread_cond_signal(&p_cond);
  }
  //pthread_cond_wait传入lock参数:
  //在条件满足时,消费者or生产者持有锁进入临界区执行,当判断条件不满足时,调用对应的wait函数
  //在消费者or生产者等待时,调用对应的Wait函数,自动释放lock
  //消费者等待,必须解锁,让另一个角色持有锁,以保证线程之间友好,在
  void ConsumerWait()
  {
    cout<<"ConsumerWait"<<endl;
    pthread_cond_wait(&c_cond,&lock);
  }
  //生产者等待
  void ProductWait()
  {
    cout<<"ProductWait"<<endl;
    pthread_cond_wait(&p_cond,&lock);
  }
public:
    BlockQueue(int _cap):cap(_cap)
    {
      pthread_mutex_init(&lock,nullptr);
      pthread_cond_init(&c_cond,nullptr);
      pthread_cond_init(&p_cond,nullptr);
    }
    ~BlockQueue()
    {
      pthread_mutex_destroy(&lock);
      pthread_cond_destroy(&p_cond);
      pthread_cond_destroy(&c_cond);
    }
    //消费者消费,拿取or执行队列中的数据以及任务并执行
    void Put(Task in)
    {
      UnlockQueue();
      if(IsFull())
      {
        WakeUpConsumer();
        ProductWait();
      }
      q.push(in);
      UnlockQueue();
    }
    //生产者生产,向队列中塞数据或者任务
    void Get(Task &out)
    {
      UnlockQueue();
      if(IsEmpty())
      {
        WakeUpProductor();
        ConsumerWait();
      }
      out=q.front();
      q.pop();
      UnlockQueue();
    }
};
#endif

ps.cpp

#include "block.h"
using namespace std;

void *consumer_run(void *arg)
{
  //int num=(int)arg;
  pthread_mutex_t lock;
  BlockQueue *bq=(BlockQueue*)arg;
  while(true)                         
  {                            
    pthread_mutex_lock(&lock);
    //int data;
    Task t;
    bq->Get(t); 
    //t.Run();
    pthread_mutex_unlock(&lock);
    cout<<"consumer"<<t.x<<"+"<<t.y<<"="<<t.Run()<<endl;
    sleep(1); 
  }
  //pthread_mutex_unlock(&lock);
}
void *productor_run(void *arg)
{
  //int num=(int)arg;
  pthread_mutex_t lock;
  sleep(1);
  BlockQueue *bq=(BlockQueue*)arg;
  //int count=0;
  while(true)
  {
    int x=rand()%10+1;
    int y=rand()%100+1;
    pthread_mutex_lock(&lock);
    Task t(x,y);
    bq->Put(t);
    pthread_mutex_unlock(&lock);
    cout<<"productor "<<x<<"+"<<y<<"=?"<<endl;
  }
  //pthread_mutex_unlock(&lock);
}

int main()
{
  BlockQueue *bq = new BlockQueue(5);
  pthread_t con1,pro1;
  pthread_create(&con1,nullptr,consumer_run,(void*)bq);
  pthread_create(&pro1,nullptr,productor_run,(void*)bq);
  //pthread_create(&con2,nullptr,consumer_run,(void*)bq);
  //pthread_create(&pro2,nullptr,productor_run,(void*)bq);
  pthread_join(con1,nullptr);
  //pthread_join(con2,nullptr);
  pthread_join(pro1,nullptr);
  //pthread_join(pro2,nullptr);
  return 0;
}

运行结果
在这里插入图片描述

4. POSIX 信号量

4.1 POSIX概念

  • POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
    在这里插入图片描述

4.2 POSIX函数

4.2.1 初始化信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value)

参数

  • pshared:0表示线程间共享,非零表示进程间共享
  • value:信号量初始值

4.2.2 销毁信号量

int sem_destroy(sem_t *sem)

4.2.3 等待信号量

int sem_wait(sem_t *sem);

功能

  • 等待信号量,会将信号量的值减1 //P()

4.2.4 发布信号量

int sem_post(sem_t *sem)//V()

功能

  • 发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1

上面生产者-消费者的例子是基于queue的,其空间可以动态分配,现在基于固定大小的环形队列重写这个程序(POSIX信号量):

4.3 基于环形队列的生产消费模型

  • 环形队列采用数组模拟,用模运算来模拟环状特性
  • 环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态
  • 但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程

在这里插入图片描述

4.3 代码示例

// RingQueue.hpp

#pragma once
#include <iostream>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <vector>

class RingQueue
{
  private:
    std::vector<int> _q;
    int _cap;
    //信号量
    sem_t sem_blank;
    sem_t sem_data;
  //资源下标
    int pro_sub;
    int con_sub;

  private:
    void P(sem_t &sem)
    {
      sem_wait(&sem);
    }

    void V(sem_t &sem)
    {
      sem_post(&sem);
    }


  public:
  RingQueue(int cap=6)
    :_cap(cap)
     ,_q(cap)
  {
    sem_init(&sem_blank,0,_cap);//格子个数一开始为容量大小
    sem_init(&sem_data,0,0);//数据一开始为0
    pro_sub=0;
    con_sub=0;
  }
  
  void Put(const int &data)
  {
    P(sem_blank);//申请格子资源,判断是否还有容量,格子--
    _q[pro_sub]=data;
    pro_sub++;
    pro_sub%=_cap;//越界回环
    V(sem_data);//数据++
  }

  void Get(int &data)
  {
    P(sem_data);//申请数据资源,判断是否还有数据,数据--
    data=_q[con_sub];
    con_sub++;
    con_sub%=_cap;
    V(sem_blank);//消耗数据,格子++
  }

  ~RingQueue()
  {
    sem_destroy(&sem_blank);
    sem_destroy(&sem_data);
    pro_sub=con_sub=0;
  }

};

//main.cpp

#include "RingQueue.hpp"

pthread_mutex_t pro_lock;//生产者组内竞争锁
pthread_mutex_t con_lock;//消费者组内竞争锁

int count=0;//生产者数据

void Lock(pthread_mutex_t &lock)
{
  pthread_mutex_lock(&lock);
}

void ULock(pthread_mutex_t &lock)
{
  pthread_mutex_unlock(&lock);
}

  void *Get(void *arg)
{
  RingQueue *q=(RingQueue*)arg;
  while(1)
  {
    usleep(1);
    int data=0;
    Lock(con_lock);//组内竞争
    q->Get(data);//获取数据
    ULock(con_lock);
    std::cout<<"consumer get data...:"<<data<<std::endl;
  }
}
void *Put(void *arg)
{
  RingQueue *q=(RingQueue*)arg;
  while(1)
  {
    sleep(1);//增加系统调用
    Lock(pro_lock);//组内竞争
    int number = (++count)%10l;
    q->Put(number);
    ULock(pro_lock);
    std::cout<<"productor put data...."<< number << std::endl;
  }

}

int main()
{
//创建交易场所
  RingQueue *q=new RingQueue(5);

  //初始化锁
  pthread_mutex_init(&con_lock,nullptr);
  pthread_mutex_init(&pro_lock,nullptr);
  
  //创建线程
  pthread_t tid1,tid2,tid3,tid4,tid5,tid6;
  pthread_create(&tid1,nullptr,Put,q);
  pthread_create(&tid2,nullptr,Put,q);
  pthread_create(&tid3,nullptr,Put,q);
  pthread_create(&tid4,nullptr,Get,q);
  pthread_create(&tid5,nullptr,Get,q);
  pthread_create(&tid6,nullptr,Get,q);


//等待线程、避免内存泄漏,不关心返回值
  pthread_join(tid1,nullptr);
  pthread_join(tid2,nullptr);
  pthread_join(tid3,nullptr);
  pthread_join(tid4,nullptr);
  pthread_join(tid5,nullptr);
  pthread_join(tid6,nullptr);

  //销毁工作
  pthread_mutex_destroy(&con_lock);
  pthread_mutex_destroy(&pro_lock);
  delete q;
  
  return 0;
}

运行结果
在这里插入图片描述

  • 14
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值