Linux系统编程--线程同步

目录

一、前言

二、线程饥饿

三、线程同步

四、条件变量

1、cond

2、条件变量的使用

五、条件变量与互斥锁


一、前言

上篇文章我们讲解了线程互斥的概念,为了防止多个线程同时访问一份临界资源而出问题,我们引入了线程互斥,线程互斥其实就是多个线程同时争抢一份资源,谁抢到了就是谁的,抢不到的只能等待着下一次抢。虽然解决了有多个线程同时访问同一资源所产生的问题,但是我们思考一下这样子合理吗?不合理,这会产生另一种问题------线程饥饿。

二、线程饥饿

那么线程饥饿是什么呢?为了便于理解,我们可以极端地考虑问题,假设在多线程情况下,存在着两类优先级不同的线程,一类线程的优先级非常高,另一类的线程的优先级非常低,他们开始同时争抢临界资源,假设高优先级的线程拿到了资源,上了锁之后,其他的线程只能等。直到该线程使用完临近资源后解锁,接着所有线程又开始争抢资源,而高优先级的线程因为其优先性会再一次争抢到资源,如循环往复,导那些低优先级的线程总是在等待中,永远拿不到或者很少次数拿到资源,这样被称为饥饿或者饿死。这种争抢临界资源的方式虽然是没有什么错误,但是总归来说是不合理的。

三、线程同步

在线程只使用互斥的方式去访问临界资源的时候,就可能会出现某些线程饥饿的情况。那么在操作系统中有没有一种机制,在某一时刻既可以只让一个线程去访问临界资源,但是又可以让所有的的线程按照一定的顺序访问资源呢?所有的线程就像排队一样一个个轮流访问资源,当某一线程访问玩临界资源的时候,他就去队尾等待。这样所有的线程的执行流都可以访问到资源,从而杜绝了线程饥饿的问题。 这样的机制叫做------同步,即线程同步,在保证临界资源安全的前提下,让执行流访问临界资源具有一定的顺序性

互斥也是同步的一种,尽管只采用互斥后执行流还是乱序的,但是互斥保证了同一时刻只能有一个线程访问临界资源。但是本篇文章在介绍同步的时候,会将两者分开,即同步不包括互斥。

四、条件变量

那么同步是怎么实现的呢?同步离不开一个东西------条件变量条件变量是一种可以实现线程同步的机制,通过条件变量,可以实现让线程有序的访问临界资源

条件变量,顾名思义它是一个执行的"条件",当线程需要访问临界资源时,如果临界资源不满足一定的条件,那就让线程进行等待,如果满足条件,则让线程继续恢复执行的机制。它是一个 pthread_cond_t 结构体类型的变量,并且在 pthread 库中也提供了一些条件变量相关的接口


1、cond

cond即 英文单词 condition 的缩写,译为条件。

pthread_cond_t是定义条件变量的类型。

条件变量的使用是和互斥锁差不多的。

  • 条件变量的初始化可以和互斥量相同有两种,一种是调用接口 pthread_cond_init() 初始化,第一个参数是条件变量的地址,第二个参数是条件变量的属性(暂时不考虑)。需要注意的是,用该接口初始化的条件变量在不需要使用的时候,需要调用 pthread_cond_destroy() 接口来销毁掉。
  • 使用宏初始化的条件变量就不用手动调用接口来销毁了。

使用条件变量等待的接口:

  • 这么多等待的接口中**pthread_cond_wait()**接口是最常用的,它是pthread库提供的使用条件变量等待的接口,线程调用此接口,线程就会立即进入等待。
  • pthread_cond_timedwait()也是pthread提供给的使用条件变量等待的接口,不过看他的名字也知道它是一种定时让线程等待的接口,即可以通过该接口设置一定的时间,在此时间内让线程等待,如果此时间内,条件满足了,线程就会被自动唤醒,继续执行代码
  • 我们可以看到这两个接口的参数中都有 互斥锁,他们是和互斥锁一起配合使用的。

上面讲到了两个通过条件变量让线程进行等待的接口,既然有等待的接口,那么自然就存在着通过条件变量去唤醒线程的接口。如下

  • pthread_cond_signal(),调用该接口可以让某个通过指定条件变量陷入等待的线程被唤醒。
  • pthread_cond_broadcast() ,调用此接口,可以让通过指定条件变量陷入等待的所有线程被唤醒

2、条件变量的使用

下面我们简单使用一下条件变量,主要看看它是怎么用的。

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using std::cin;
using std::cout;
using std::endl;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;//利用宏初始化全局互斥锁,不用销毁
pthread_cond_t cond;//定义全局条件变量

void* Callback(void* argc)
{
    pthread_detach(pthread_self());//这里让线程自动分离,我们后面不回收它
    const char* name=(const char*)argc;
    while(true)
    {
        pthread_cond_wait(&cond,&mutex);//使用条件变量让进程在这里等待
        cout<<name<<",tid::"<<pthread_self()<<",running"<<endl;
    }
    return nullptr;

}
int main()
{
    pthread_cond_init(&cond,nullptr);//初始化条件变量
    pthread_t tid1,tid2,tid3;

    pthread_create(&tid1,nullptr,Callback,(void*)"thread 1");
    pthread_create(&tid2,nullptr,Callback,(void*)"thread 2");
    pthread_create(&tid3,nullptr,Callback,(void*)"thread 3");
    
    while(true)
    {
        char c='a';
        cout<<"Please input your command:(N/Q)::";
        cin>>c;
        if(c=='N'|c=='n')
        {
            pthread_cond_signal(&cond);//唤醒单个的线程
        }
        else
            break;
        usleep(1000);//让主线程在这里等待一下防止多线程之间的打印干扰
    }
        pthread_cond_destroy(&cond);//销毁条件变量
        return 0;
    }

运行结果:

可以看到pthread_cond_signal()对线程的唤醒是以一定顺序来进行的。当然我们也可以使用pthread_cond_broadcast()来广播唤醒所有的在等待中的线程。


上面演示的是cond变量的简单使用,我们在函数中直接让它进行等待,事实上在实际的使用中,当有条件变量不满足时,才会使用条件变量让线程等待。

我们可以设置一个退出条件 quit,为真时即为满足,否则不满足。不满足条件时,就让线程等待,满足条件就唤醒线程。

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using std::cin;
using std::cout;
using std::cerr;
using std::endl;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond;
volatile bool quit=false;
void* Callback(void* argc)
{
    pthread_detach(pthread_self());
    const char* name=(const char*)argc;
    while(!quit)
    {
        pthread_cond_wait(&cond,&mutex);
        cout<<name<<",tid::"<<pthread_self()<<",running"<<endl;
    }
//下面释放锁的操作是因为pthread_cond_wait()接口在等待时会释放锁资源,然后被唤醒的时候又会竞争锁资源,如果线程退出条件满足了,在退出的时候,仍然是对临界资源上了锁,所以在退出之前需要先解锁,不然会导致死锁(如果不提前进行分离)
    pthread_mutex_unlock(&mutex);
    cout<<name<<",tid::"<<pthread_self()<<",end"<<endl;
    return nullptr;

}
int main()
{
    pthread_cond_init(&cond,nullptr);
    pthread_t tid1,tid2,tid3;

    pthread_create(&tid1,nullptr,Callback,(void*)"thread 1");
    pthread_create(&tid2,nullptr,Callback,(void*)"thread 2");
    pthread_create(&tid3,nullptr,Callback,(void*)"thread 3");
    
    while(true)
    {
        char c='a';
        cout<<"Please input your command:(N/Q)::";
        cin>>c;
        if(c=='N'|c=='n')
        {
            pthread_cond_broadcast(&cond);
        }
        else{
            quit=true;
            pthread_cond_broadcast(&cond);
            break;
        }
        usleep(1000);
    }
        pthread_cond_destroy(&cond);
        return 0;
    }

这里比之前的简单应用主要多了一个解锁操作。且在 输入非N或n时,唤醒线程,再让线程判断一下条件是否满足。

可以看到 使用条件变量可以让多线程的执行具有一定的顺序性,即可以实现同步。同步与互斥是互补的关系。

五、条件变量与互斥锁

在我们上面所举的例子当中,让线程根据条件变量进行等待的接口都是需要同时用到条件变量和互斥锁,使用到条件变量这是无可厚非的,但是为什么需要用到互斥锁呢?

首先,条件等待是使用条件变量实现同步等待的一种方式,如果只存在一个线程的话,当条件不满足时,线程就会一直等待下去,因为唯一的线程在等待,并没有其他的线程修改条件,所以在线程等待的时候,条件也不可能满足。

所以这里需要的是一个使得条件变得满足,然后再唤醒等待的线程。这里的条件实际上就是指 线程对应的需要访问的临界资源的状态,就像我们在介绍互斥时的抢票动作,需要保证只有在票数大于0时,才能抢票。

而条件是不可能无缘无故在没有变化的情况下就自己满足的,所以条件满足势必会存在着临界资源数据的变化,所以需要用互斥锁来保护临界资源。

所以线程在判断条件满足之前需要先上锁,然后再判断条件是否满足,如果不满足则条件等待并解锁,接着让其他可以让条件满足的线程获取锁,条件满足之后,再唤醒刚才等待的线程并解锁。让刚被唤醒的线程再次取到锁,判断条件是否满足,满足就去执行,否则再次陷入等待。整个过程的重点就是谁需要访问临界资源就上锁,谁不需要就解锁,即保证在整个的过程当中临界资源始终是被保护着的。

整个的过程当中,除了第一次对临界资源上锁和最后一次对临界资源解锁,中间所有的上锁和解锁操作都是由pthread_cond_wait()操作完成的,在线程需要等待的时候调用pthread_cond_wait()解锁并等待,在线程被唤醒时,会自动再去竞争锁,解锁和上锁操作都是在pthread_cond_wait()内部进行的。这就是为什么我们在上面的例子中在多线程退出时,需要在条件满足时先释放锁,然后再让线程退出。

pthread_cond_wait()接口需要执行释放锁和竞争锁的操作,所以需要先看到锁这也是为什么该接口需要和互斥锁一起使用。

相关推荐
政安晨36 分钟前
Ubuntu 服务器无法 ping 通网站域名的问题解决备忘 ——通常与网络配置有关(DNS解析)
linux·运维·服务器·ubuntu·ping·esp32编译服务器·dns域名解析
路溪非溪2 小时前
嵌入式Linux驱动开发杂项总结
linux·运维·驱动开发
Neolock3 小时前
Linux应急响应一般思路(三)
linux·web安全·应急响应
被遗忘的旋律.4 小时前
Linux驱动开发笔记(七)——并发与竞争(上)——原子操作
linux·驱动开发·笔记
轻松Ai享生活4 小时前
minidump vs core dump
linux
轻松Ai享生活5 小时前
详细的 Linux 常用文件系统介绍
linux
张童瑶5 小时前
Linux 离线安装lrzsz(rz、sz上传下载小插件)
linux·运维·centos
十五年专注C++开发6 小时前
通信中间件 Fast DDS(二) :详细介绍
linux·c++·windows·中间件·fastdds
YC运维7 小时前
Linux服务测试题(DNS,NFS,DHCP,HTTP)
linux·网络
zhanghongyi_cpp7 小时前
linux的conda配置与应用阶段的简单指令备注
linux·python·conda