• 【Linux】线程


    1.线程的函数

    线程有自己的pcb

    线程共享资源:1.文件描述符 2.当前工作目录,内存地址空间

    线程非共享资源 1.线程id,2.内核栈 3.独立的栈空间 4.errno变量 5.信号屏蔽字 6.调度优先级

    char* strerror(int errnum)  //获取错误码对应的错误消息
    
    • 1
    1.1创建一个线程pthread_create()函数
    功能:创建一个新的线程
    原型
    int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
    参数
    thread:返回线程ID
    attr:设置线程的属性,attr为NULL表示使用默认属性
    start_routine:是个函数指针,线程启动后要执行的函数
    arg:传给线程启动函数的参数
    返回值:成功返回0;失败返回错误码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    //文件名:first_pthread.c
    #include
    #include
    
    void* thr(void* arg)
    {
        int i=*(int*)arg;
        printf("agr:%d",i);
        printf("I am a thread! pid=%d\n",getpid());
        return NULL;
    }
    int main()
    {
        pthread_t tid;
        int* p=new int(4);
        pthread_create(&tid,NULL,thr,p);
        printf("I am main pthread,pid=%d\n",getpid());
        sleep(1);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项
    gcc first_pthread.c -o first_pthread -g -lpthread
    
    • 1

    image-20220803231457888

    查看线程id

    //返回当前线程id
    #include 
    pthread_t pthread_self(void);
    
    • 1
    • 2
    • 3

    改进上面的程序

    void* thr(void* arg)
    {
        printf("I am a thread! pid=%d,tid=%lu\n",getpid(),pthread_self());
        return NULL;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    image-20220803233416710

    1.2线程回收pthread_join

    在前面的进程中,我们知道创建一个子进程之后,父进程是需要等待的,否则子进程成为僵尸进程,父进程都不知道,后面也就无法处理,造成内存泄漏等问题,所以子线程同样需要等待。等待原因总结如下:

    线程等待原因:

    已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
    创建新的线程不会复用刚才退出线程的地址空间。

    功能:等待线程结束后回收(阻塞等待)
    原型
    int pthread_join(pthread_t thread, void **value_ptr);
    参数
    thread:线程ID
    value_ptr:它指向一个指针,后者指向线程的返回值
    返回值:成功返回0;失败返回错误码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

    • 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
    • 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_CANCELED。
    • 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传pthread_exit的参数。
    • 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数
    void* thr(void* args)
    {
    	int cnt=0;
    	while(cnt<3)
        {
         sleep(1);
         cout<<"thread id:"<<pthread_self()<<endl;
         sleep(1);
         cnt++;
        }   
    	cout<<"thread quit"<<endl;
        return NULL;
    }
    int main()
    {                                                              
        pthread_t main_thread;
        main_thread=pthread_self();
        pthread_t tid[2];
        int j;
        for(j=0;j<2;j++)
        {
          pthread_create(tid+j,NULL,thr,NULL);
          cout<<"tid="<<tid[j]<<endl;
        }
         int i;
         for(i=0;i<2;i++)
        {
          if(0==pthread_join(tid[i],NULL))
          {
           cout<<"thread"<< i <<" tid: "<<tid[i]<<" join sucess "<<endl;
          }
        }
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    image-20220804004851558

    1.3线程终止

    如果需要只终止某个线程而不终止整个进程,可以有三种方法:

    1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
    2. 线程可以调用pthread_ exit终止自己。
    3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
    功能:线程终止
    原型
    void pthread_exit(void *value_ptr);
    参数
    value_ptr:value_ptr不要指向一个局部变量。
    返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1.4线程取消(杀死)pthread_cancel函数
    功能:取消一个执行中的线程
    原型
    int pthread_cancel(pthread_t thread);
    参数
    thread:线程ID
    返回值:成功返回0;失败返回错误码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    using namespace std;
    void *Rountine(void* args)
    {
      int cnt = 5;
      while(cnt--)
      {
        printf("thread %d: %lu is running !\n", cnt, pthread_self());
      }
      return (void*)-1;
    }
    int main()
    {
      pthread_t tids[5];
      for(auto i = 0 ; i < 5; ++i)
      {
        pthread_create(&tids[i], nullptr, Rountine, nullptr);
      }
      sleep(3);
      for(auto i = 2; i< 4; ++i)
      {
        pthread_cancel(tids[i]);
        printf("thread %p is cancel\n", tids[i]);
      }
      void* ret=nullptr;
      for(auto i = 0; i < 5; ++i)
      {
        if(pthread_join(tids[i], &ret) == 0)
        {
          printf("thread %d: %lu join sucess!!!  |  exit code: %d\n", i, tids[i],ret);
        }
      }
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    image-20220804011601775

    对应线程函数的执行过程没有执行,就直接返回-1了,说明我们取消线程成功了。

    1.5线程分离pthread_detach()

    ​ 线程分离状态,指定该状态,线程主动与主控线程断开练习。线程结束后,其退出状态不由其他线程获取,而由自己主动释放。常用于网络,多线程服务器。

    ​ 进程若有该机制,就不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源倍释放,一部分资源残留仍然存于系统中,导致内核认为该进程仍然存在。

    • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
    • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
    int pthread_detach(pthread_t thread);
    //也可以分离自己
    pthread_detach(pthread_self());
    
    • 1
    • 2
    • 3
    • 一般情况下,线程终止后,其终止状态一直保留到其他线程调用pthread_join获取它的状态为止。但是线程可以被置为detach状态,这样线程一旦终止,内核就会立刻回收它的所有资源,而不是保留它的终止状态。
    • 不能对一个detah状态的线程调用pthread_join,也不能对一个join状态的线程调用pthread_detach。
    void* thr(void*agrs)
    {
     printf("I am a thread,self=%lu\n",pthread_self());
     sleep(4);
     printf("I am a thread,self=%lu\n",pthread_self());
     return NULL;
    }
    int main()
    {
     pthread_t tid;
     pthread_create(&tid,NULL,thr,NULL);
      //线程分离
      pthread_detach(tid);
      sleep(5);                                                                                      
      int ret=0;
      if((ret=pthread_join(tid,NULL))>0)
      {
        printf("join err %d,%s\n",ret,strerror(ret));
      }
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    image-20220804021444097

    当一个线程同时调用pthread_join和pthread_detach时,就会报非法参数的错误。

    1.6判断线程ID是否相等pthread_equal

    比较两个线程ID是否相等。

    int pthread_equal(pthread_t t1,pthread_t t2);
    
    • 1
    • 线程ID在一个进程的线程组中是唯一的
    • 在不同进程的线程组,不同线程组的线程ID可能相等。
    最优的线程数

    (cpu核数 * 2,cpu核数 * 2+2)

    1.7线程互斥
    • 临界资源:多线程执行流共享的资源就叫做临界资源

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

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

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

    1.7.1互斥量mutex(锁)
    • 部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。

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

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

    观察下面的这个程序

    //  		buy_ticets.c
    int num=5;
    int tickets=50;
    void* Route(void* agrs)
    {
        printf("begin buy tickets......\n");
        while(1)
        {
            if(tickets>0)
            {
                unsleep(2000);
                printf("0x%x: get a tickets:%d\n",(int)pthread_self(),tickts);
                tickets--;
            }
            else
            {
                break;
            }
        }
    }
    int main()
    {
        pthread_t nums[num];
        for(int i=0;i<num;i++)
        {
            pthread_create(nums+i,NULL,Route,NULL);
        }
        //回收资源
        for(int i=0;i<num;i++)
        {
            pthread_join(nums[i],NULL);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    image-20220807122125584

    在抢票的过程中,出现了两个需要注意的点

    • 票数为负数的情况,这显然和我们预想实现的结果是不相同的。
    • 票数减少的数值并不是从50逐渐减少1直到0。

    原因

    原因是tickets是一个全局变量,属于是临界资源,被所有线程共享。在多线程中,当多个线程对一个全局变量进行操作时,该操作不是原子的(安全的)。

    比如上述的tickets–操作,一共设计三条汇编指令。

    • load:将共享变量ticket从内存加载到寄存器中
    • update: 更新寄存器里面的值,执行-1操作
    • store:将新值,从寄存器写回共享变量ticket的内存地址

    image-20220807164837225

    为什么会出现负数的情况?

    • 在执行if语句的时候,判断为真,代码可以并发切换到其他线程
    • unsleep()这个过程中,可能会有很多线程进入代码段。

    要解决以上的问题,需要以下的解决方法:

    • 代码直接必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。

    • 如果多个线程同时要求执行临界区代码,并且临界区没有线程在执行,那么只能允许一个线程进入。

    • 如果线程不在临界区执行,那么该线程不能阻止其他线程进入临界区。

    就像是一把锁,锁住了临界区,linux上提供的这把锁叫做互斥量

    image-20220807170632431

    2.互斥量的接口

    初始化互斥量的方式:

    方法1,静态分配:
        	pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
    方法2,动态分配:
        int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t*restrict attr);
    参数:
    mutex:要初始化的互斥量
    attr:NULL
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    销毁互斥量 销毁互斥量需要注意的点:

    • 静态分配的互斥量不需要销毁

    • 不要销毁一个已经加锁的互斥量

    • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    
    • 1

    互斥量加锁和解锁

    int pthread_mutex_lock(pthread_mutex_t *mutex);
    int pthread_mutex_unlock(pthread_mutex_t* mutex);
    返回值:
        成功返回0,失败返沪错误码
    
    • 1
    • 2
    • 3
    • 4

    调用pthread_lock时,注意事项:

    • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回0;
    • 发起函数调用时,其他线程已经锁定了互斥量,或者存在其他线程同时申请互斥量,但是没有竞争成功,那么pthread_lock调用就会陷入阻塞(执行流被挂起),等待互斥量解锁。
    2.1对抢票程序进行改进

    对程序中的临界区加上锁

    int num=5;
    int tickets=50;
    pthread_mutex_t lock;
    void* Route(void* agrs)
    {
        printf("begin buy tickets......\n");
        while(1)
        {
            pthread_mutex_lock(&lock);
            if(tickets>0)
            {
                unsleep(2000);
                printf("0x%x: get a tickets:%d\n",(int)pthread_self(),tickts);
                tickets--;
                pthread_mutex_unlock(&lock);
            }
            else
            {
                pthread_mutex_unlock(&lock);
                break;
            }
        }
    }
    int main()
    {
        pthread_t nums[num];
        for(int i=0;i<num;i++)
        {
            pthread_create(nums+i,NULL,Route,NULL);
        }
        //回收资源
        for(int i=0;i<num;i++)
        {
            pthread_join(nums[i],NULL);
        }
        //销毁锁
        pthread_mutex_destory(&lock)l
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    image-20220807174234687

    加上锁后,票数没有再减至为负数。并且票数减少是从50逐渐减1到0。

    需要注意的知识点:

    • 加了锁,也会进行线程的切换,只不过线程切换的时候是抱着锁的。其他线程申请锁,但是处于阻塞状态。其他线程只有等该线程执行完毕后(或者释放锁),或者发生阻塞。所以锁间接的包含了原子性。
    • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
    • 互斥锁mutex,本质就是内存上的一块空间,空间里面存储的值是1。是可以被所有线程读取并访问的。
    2.2死锁

    死锁:

    • 死锁是指一组进程的各个进程均占有不会释放的资源,但因互相申请被其他线程所

    死锁的必要条件:

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

    3.线程同步

    饥饿问题: 我们在创建锁的过程中,可能会存在一个优先级高的线程,每次都是该线程优先申请到锁。该线程一直申锁,执行程序,释放锁。导致其他线程没有机会得到这把锁。

    3.1条件变量
    • 之所以不断在进行申请锁,检测,本质是我们并不知道到临界资源的情况!所以我们这里需要用到条件变量。
    • 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
    • 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

    条件变量(cond):条件变量是线程库提供的一个描述临界资源状态的对象的变量。不用频繁申请或者释放锁的方式,也能达到检测临界资源的目的。

    3.2同步和竞态条件

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

    3.3条件变量函数

    初始化

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

    销毁

    int pthread_cond_destroy(pthread_cond_t *cond)
    
    • 1

    阻塞等待条件满足

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

    等待唤醒

    //唤醒所有等待的线程
    int pthread_cond_broadcast(pthread_cond_t *cond);
    //唤醒在指定条件下等待的一个线程
    int pthread_cond_signal(pthread_cond_t *cond);
    
    • 1
    • 2
    • 3
    • 4
    3.4实例
    pthread_cond_t cond; //条件变量,用来控制同步
    pthread_mutex_t mutex; //互斥量
    void* Route1(void* argc)
    {
        while(1)
        {
            pthread_cond_wait(&cond,&mutex);
            printf("%s线程正在运行......\n",(char*)argc)
        }
        return NULL;
    }
    void* Route2(void* agrc)
    {
        while(1)
        {
            pthread_cond_broadcast(&cond);
            //pthread_cond_signal(&cond); //给在等待队列的一个线程发送信号
            sleep(1);
        }
    }
    int main()
    {
        pthread_cond_init(&cond,NULL); //初始化条件变量
        pthread_mutex_init(&mutex,NULL); //锁也进行初始化
        //使用tid4控制其他的线程
        pthread_t tid1,tid2,tid3,tid4;
        pthread_create(&tid1,NULL,Route1,(void*)"thread 1");
        pthread_create(&tid2,NULL,Route1,(void*)"thread 2");
        pthread_create(&tid3,NULL,Route1,(void*)"thread 3");
        pthread_create(&tid4,NULL,Route2,(void*)"thread 4");
        //回收资源
        pthread_join(tid1,NULL);
        pthread_join(tid2,NULL);
        pthread_join(tid3,NULL);
        pthread_join(tid4,NULL);
        //销毁条件变量和互斥量
        pthread_cond_destroy(&cond);
        pthread_mutex_destory(&mutex);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    image-20220807203252094

    为什么pthread_cond_wait需要互斥量?

    • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
    • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据 。

    image-20220807203726540

    4.生产者消费者模型

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

    image-20220807204005372

    特点:

    • 存在三种关系,生产者与生产者【互斥关系】,消费者与消费者【互斥关系】,生产者与消费者【互斥、同步】
    • 两种角色:生产者和消费者
    • 交易场所【超市】:通常指的是一段内存空间【list,vector或者其他容器】–管道通信
    4.1基于BlockingQueue的生产者消费者模型

    BlockingQueue的概念:

    ​ 在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于:

    • 当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;
    • 当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

    image-20220807204708527

    单消费者和单生产者的生产消费模型

    使用STL中的queue进行模拟

    #pragma once
    #include 
    #include 
    #include 
    #include 
    //单生产者、单消费者
    //类模板,
    template <class T>
    class BlockQueue
    {
      public:
        //构造函数初始化
        BlockQueue(int _cap):cap(_cap)
        {
          //条件变量、互斥锁初始化
          pthread_mutex_init(&lock, nullptr);
          pthread_cond_init(&have_space, nullptr);//生产者
          pthread_cond_init(&have_data, nullptr);//消费者
        } 
        ~BlockQueue()
        {
          pthread_mutex_destroy(&lock);
          pthread_cond_destroy(&have_space);
          pthread_cond_destroy(&have_data);
        }
        void put(const T& in)
        {
          pthread_mutex_lock(&lock);//加锁
          //生产者不应该在生在,而应该等待
          //为什么用while,防止伪唤醒,只有一个数据,可能唤醒了多个消费者,存在数据错误,所以用while持续判断
          // if(is_full())
          while(is_full())
          {
              //若阻塞队列已经满了,那么此时该生产者线程就需要进行等待,直到阻塞队列中有空间时再将其唤醒。
            //这里前面条件变量讲到过,可以去看看前面
            pthread_cond_wait(&have_space, &lock);//为什么要抱着锁,等待自动解锁,唤醒时自动获取锁
            //为了避免死锁问题,在调用pthread_cond_wait函数时就需要传入当前线程手中的互斥锁,此时当该线程被挂起时就会自动释放手中的互斥锁,而当该线程被唤醒时又会自动获取到该互斥锁。
          }
          bq.push(in);
          //有一半以上的数据,我们才去通知消费者进行消费,实现同步过程
          if(bq.size() >= cap / 2)
            pthread_cond_signal(&have_data);//给消费者发送信号,有数据了,需要你过来进行消费
          pthread_mutex_unlock(&lock);
        }
        void get(T* out)
        {
          pthread_mutex_lock(&lock);
          // if(is_empty())
          //若阻塞队列为空,那么此时该消费者线程就需要进行等待,直到阻塞队列中有新的数据时再将其唤醒
          while(is_empty())
          {
            pthread_cond_wait(&have_data, &lock);
          }
          *out = bq.front();
          bq.pop();
          pthread_cond_signal(&have_space);
          pthread_mutex_unlock(&lock);
        }
      private:
        bool is_empty()
        {
          return bq.size() == 0 ? true : false;
        }
        //是否满
        bool is_full()
        {
          return bq.size() == cap ? true : false;
        }
      private:
        int cap;//队列容量
        pthread_mutex_t lock;//条件变量需搭配互斥锁使用
        pthread_cond_t have_space;//生产者
        pthread_cond_t have_data;//消费者
        std::queue<T> bq;//本身就是临界资源,也需要加锁保护
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 生产者生产数据的时候,我们首先需要判断阻塞队列是否满了,若阻塞队列已经满了,那么此时该生产者线程就需要进行等待,直到阻塞队列中有空间时再将其唤醒。
    • 消费者消费数据的时候,也需要判断阻塞队列是否有数据,若阻塞队列为空,那么此时该消费者线程就需要进行等待,直到阻塞队列中有新的数据时再将其唤醒
    • 生产者与消费者在等待的过程中,因为他们都是先申请到锁在去判断条件的,在条件不满足的情况下,该线程是被挂起的。此时该线程抱着锁,我们就需要用到pthread_cond_wait这个函数,第二个参数需要传入当前线程手中的互斥锁,此时当该线程被挂起时就会自动释放手中的互斥锁,而当该线程被唤醒时又会自动获取到该互斥锁。
    • 判断阻塞队列为空或者未满的时候,用while的问题,因为我们这里是单生产者、单消费者模型,所以不存在伪唤醒的情况,但在多生产者与多消费者模型下就会存在伪唤醒的情况。
      • pthread_cond_wait函数是让当前执行流执行等待的函数,是函数就意味着有可能调用失败,调用失败后该执行流就会继续往后执行。
      • 其次,在多消费者的情况下,当生产者生产了一个数据后如果使用pthread_cond_broadcast函数唤醒消费者,就会一次性唤醒多个消费者,但待消费的数据只有一个,此时其他消费者就被伪唤醒了。

    为了避免出现上述情况,我们就要让线程被唤醒后再次进行判断,确认是否真的满足生产消费条件,因此这里必须要用while进行判断。

    #include "block_queue.hpp"
    // #include "task.hpp"
    #define NUM 10
    //消费者
    void *consumer(void *c)
    {
      // BlockQueue *bq = (BlockQueue*)c;
      BlockQueue<int> *bq = (BlockQueue<int>*)c;
      int out = 0;
      while(true)
      {
        sleep(1);
        int t;
        bq->get(&t);
        std::cout << "consumer have cost: " << t  << std::endl;
      }
      return nullptr;
    }
    void *producter(void *p)
    {
      // BlockQueue *bq = (BlockQueue *)p;
      BlockQueue<int> *bq = (BlockQueue<int> *)p;
      //int in = 1;
      while(true)
      {
        int top = rand()%5 + 1;
        bq->put(top);
        std::cout << "producer produce: " << top << std::endl;
      }
    }
    int main()
    {
      //这里处理的int这种商品
      //类型具体可以变化,这里可以安排任务给消费者,消费者去消费任务
      BlockQueue<int> *bq = new BlockQueue<int>(NUM);
      pthread_t c, p;
      //两个线程,一个生产者,一个消费者
      pthread_create(&c, nullptr, consumer, bq);
      pthread_create(&p, nullptr, producter, bq);
      pthread_join(c, nullptr);
      pthread_join(p, nullptr);
      delete bq;//在堆上开辟的记得释放
      return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    image-20220807215613120

    5.POSIX信号量

    信号量本质是一个计数器,用来描述临界资源数目的计数器。

    信号量是一种特殊的变量,它只能取自然数值并且只支持两种操作:等待(wait)和信号(signal)。但是在Linux/UNIX中,“等待”和“信号”都已经具有特殊的含义。所以我们用的称呼是P、V操作。

    • P(SV),如果SV的值大于0,就将它减1;如果SV的值为0,则挂起进程的执行。
    • V(SV),如果有其他进程因为等待SV而挂起,则唤醒之;如果没有,则将SV加1。

    最常用的、最简单的信号量是二进制信号量,它只能取0和1这两个值。

    ​ 当关键代码段可用时,二进制信号量SV的值为1,进程A和B都有机会进入关键代码段。如果此时进程A执行了P(SV)操作将SV减1,则进程B若再执行P(SV)操作就会被挂起。直到进程A离开关键代码段,并执行V(SV)操作将SV加1,关键代码段才重新变得可用。如果此时进程B因为等待SV而处于挂起状态,则它将被唤醒,并进入关键代码段。同样,这时进程A如果再执行P(SV)操作,则也只能被操作系统挂起以等待进程B退出关键代码段。

    5.1相关函数

    初始化信号量

    #include 
    int sem_init(sem_t *sem, int pshared, unsigned int value);
    参数:
    pshared:0表示线程间共享,非零表示进程间共享
    value:信号量初始值
    
    • 1
    • 2
    • 3
    • 4
    • 5

    销毁信号量

    int sem_destroy(sem_t *sem);
    
    • 1

    等待信号量

    功能:等待信号量,会将信号量的值减1
    int sem_wait(sem_t *sem);
    
    • 1
    • 2

    发布信号量

    功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1int sem_post(sem_t *sem);
    
    • 1
    • 2
  • 相关阅读:
    使用Flask开发简单接口
    [nltk_data] Error loading stopwords: <urlopen error [WinError 10054]
    小白一键重装官网下载使用方法
    漫画 | 芯片战争50年,Intel为什么干不掉AMD?
    HTX 与 Zebec Protocol 展开深度合作,并将以质押者的身份参与 ZBC Staking
    LeetCode39- 组合总和
    CDP集群搭建过程和bug处理
    SwiftUI Preview传递参数问题
    Es6 箭头函数
    模板匹配与像素统计
  • 原文地址:https://blog.csdn.net/qq_53893431/article/details/126605868