【Linux】--- 线程同步

一、同步的概念

同步:线程之间按照一定顺序访问资源

线程在占用锁的时候,是不受控制的,这就有可能导致一个竞争能力强的线程,从头到尾都占用一个锁。刚释放这个锁,就又被同一个线程申请走了。

线程是通过条件变量来进行线程同步的。

二、条件变量cond

条件变量函数

条件变量cond在pthread库中,需要头文件<pthread.h>,创建与销毁方式如下:

条件变量的类型是pthread_cond_t,分为全局条件变量和局部条件变量,它们的创建方式不同。

(1)全局cond:
想要创建一个全局的条件变量很简单,直接定义即可:

pthread_cond_t xxx = PTHREAD_COND_INITIALIZER;

这样就创建了一个名为xxx的变量,类型是pthread_cond_t,即这个变量是一个条件变量,全局的条件变量必须用宏PTHREAD_COND_INITIALIZER进行初始化!

另外,全局的条件变量不需要手动销毁。

(2)局部cond:

局部的条件变量是需要通过接口来初始化与销毁的,接口如下:


pthread_cond_init:

pthread_cond_init函数用于初始化一个条件变量,函数原型如下:

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

参数:

  • restrict cond:类型为pthread_cond_t *的指针,指向一个条件变量,对其初始化
  • restrict attr:用于设定该条件变量的属性,一般不用,设为空指针即可

返回值:成功返回0;失败返回错误码


pthread_cond_destroy:

pthread_cond_destroy函数用于销毁一个条件变量,函数原型如下:

int pthread_cond_destroy(pthread_cond_t *cond);

参数:类型为pthread_cond_t *的指针,指向一个条件变量,销毁该条件变量

返回值:成功返回0;失败返回错误码

创建好条件变量后,就要使用这个条件变量,主要是两个操作:等待条件满足和唤醒线程。
如图所示:

在这里插入图片描述
现在有三个线程thread-1,thread-2,thread-3,这三个线程争夺一个临界资源。

而thread-1申请到了锁mutex,但是由于我们给这个临界资源添加了条件变量:此时thread-1不能直接访问临界资源,而是进入等待队列,并且释放持有的锁:

在这里插入图片描述
由于锁被释放,后续线程可以继续申请这个锁。

于是thread-2和thread-3也分别申请到了mutex,通过相同的方式进入了等待队列:
在这里插入图片描述
现在所有线程都在等待队列中,这些线程因为没有满足特定条件,所以不能访问临界资源
但是为什么要进等待队列呢?

因为要保证线程同步,也就是说,一开始谁先访问的临界资源,那么后续条件满足时,就让谁先来访问这个资源。

(1)pthread_cond_wait:

pthread_cond_wait函数用于让一个线程进入等待队列等待,直到被唤醒,函数原型如下:

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

参数:

  • restrict cond:类型是pthread_cond_t *的指针,即在哪一个条件变量下等待
  • restrict mutex:类型是pthread_mutex_t *的指针,因为在进入等待队列前,线程是持有锁的状态,此处传入锁的指针,就是为了帮助这个线程释放该锁,从而让其他线程也可以申请锁,进入等待队列。

返回值:成功返回0,失败返回错误码。

那么进入等待队列后,又要如何唤醒内部的线程呢?

有两种唤醒方式:唤醒一个线程唤醒所有线程

(2)pthread_cond_signal:

pthread_cond_signal用于唤醒等待队列的第一个线程,让其访问临界区的代码,函数原型如下:

int pthread_cond_signal(pthread_cond_t *cond);

参数:cond用于指明一个条件变量,说明要唤醒哪一个条件变量下等待的线程。

比如刚刚三个线程都进入了等待队列:
在这里插入图片描述
当使用pthread_cond_signal唤醒一个线程时:线程重新获得之前释放的锁mutex,随后访问临界区代码。

在这里插入图片描述
当后续条件再次满足,thread-2和thread-3也会依次再次获得锁,从而访问到临时资源。
假设现在thread-1访问完毕临界资源后,立马再次申请了锁:

在这里插入图片描述
由于条件变量的存在,therad-1不能直接访问资源,要去等待队列等待,此时thread-1进入等待队列尾部
在这里插入图片描述
这样就可以避免一个线程一直占用临界资源,从而完成线程同步了。

(3)pthread_cond_broadcast:

pthread_cond_broadcast用于唤醒等待队列中的所有线程,函数原型如下:

int pthread_cond_broadcast(pthread_cond_t *cond);

参数:cond用于指明一个条件变量,说明要唤醒哪一个条件变量下等待的线程。

还是刚才的三个线程都处于等待队列的情况:

在这里插入图片描述
当我现在用pthread_cond_broadcast唤醒所有线程,此时thread-1,thread-2,thread-3都被唤醒了,难道他们一起访问临界资源吗?不是的此时所有被唤醒的线程再次竞争同一把锁,竞争到锁的线程才访问临界资源。当前一个线程访问完毕后,剩下的线程继续竞争,再访问临界资源。

讲解完条件变量的接口,我写一个示例帮助大家理解。

class thread
{
public:
    pthread_t _tid;
    string _name;
};

pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER;

void master(string name)
{
    while(true)
    {
        sleep(1);
        pthread_cond_signal(&g_cond);
    }
}

void* server(void* argv)
{
    thread* thd = (thread*)argv;
    
    while(true)
    {
        pthread_mutex_lock(&g_mutex);

        pthread_cond_wait(&g_cond, &g_mutex);

        cout << thd->_name << " is weakup!!!" << endl;

        pthread_mutex_unlock(&g_mutex);
    }

    return nullptr;
}

int main()
{
    vector<thread> threads(5);

    for(int i = 0; i < 5; i++)
    {
        threads[i]._name = "thread-" + to_string(i + 1);
        pthread_create(&threads[i]._tid, nullptr, server, (void*)&threads[i]);
    }

    master("master thread");

    return 0;
}

首先我简单封装了一个线程类:

class thread
{
public:
    pthread_t _tid;
    string _name;
};

其包含两个成员,_tid存储该线程的TID,_name存储该线程的名字。

随后定义了两个全局变量,分别是互斥锁g_mutex以及条件变量g_cond。

主线程执行函数master:

void master(string name)
{
    while(true)
    {
        sleep(1);
        pthread_cond_signal(&g_cond);
    }
}

也就是每秒钟通过ptherad_cond_signal唤醒一个队列中的线程。

在main函数中:

int main()
{
    vector<thread> threads(5);

    for(int i = 0; i < 5; i++)
    {
        threads[i]._name = "thread-" + to_string(i + 1);
        pthread_create(&threads[i]._tid, nullptr, server, (void*)&threads[i]);
    }

    master("master thread");

    return 0;
}

先创建了五个线程对象,随后在for循环内部通过pthread_create创建线程,以及给这些线程命名,这些线程都去执行了server函数,而主线程执行master函数。

server函数如下:

void* server(void* argv)
{
    thread* thd = (thread*)argv;
    
    while(true)
    {
        pthread_mutex_lock(&g_mutex);

        pthread_cond_wait(&g_cond, &g_mutex);

        cout << thd->_name << " is weakup!!!" << endl;

        pthread_mutex_unlock(&g_mutex);
    }

    return nullptr;
}

这也是示例中最核心的部分,线程进入了while循环后,先通过pthread_mutex_lock加锁,意图访问临界资源。

但是由于条件变量的限制,其还没有访问到临界资源,就执行了pthread_cond_wait,进入等待队列,并释放掉了自己之前申请的锁。

每秒钟主线程在master中唤醒一个线程,当线程被唤醒后,便重新拿到锁,执行临界资源cout << thd->_name << " is weakup!!!" << endl;,也就是显示器资源,最后释放自己的锁。

注意:在server中,是没有任何sleep函数的,但是由于master中限制了每秒钟唤醒一次,所以最后线程会以同步的形式,每隔一秒依次占用显示器,而不会发生某个线程一直输出。

输出结果:
在这里插入图片描述
可以看到,线程按照1 2 3 4 5的顺序依次输出了,这就是线程同步的作用。

我们再看看pthread_cond_broadcast的效果,现在把master改为如下代码:

void master(string name)
{
    while(true)
    {
        sleep(3);
        cout << "-------------" << endl;
        pthread_cond_broadcast(&g_cond);
    }
}

现在主线程每隔三秒唤醒等待队列中的所有线程,为了方便观察,我每次额外输出一条横线-----------。

输出结果:

在这里插入图片描述
由于线程被全部唤醒后,此时它们又要再次竞争锁,每个轮次情况不一样,所以输出的顺序就不一样。比如第一次输出,thread-1竞争力比较强,而第三次输出,thread-2竞争力比较强。但是可以保证的是,每个轮次中1 2 3 4 5都获取到了一次资源,因此也算一种同步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

为快乐起舞

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

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

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

打赏作者

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

抵扣说明:

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

余额充值