【Linux-多线程】线程同步

二、线程同步

1.条件变量

引入

【故事】假设有一个VIP自习室,门口有一把钥匙,每天第一个来的就可以进自习室学习,一次允许一个人在自习室;张三早上5点来,抢到了第一个进入自习室学习的机会,张三拿着门口钥匙打开了自习室的门进入自习室学习,学了3小时后,张三要出去上厕所,张三打开门一看,外面有许多人排队,如果因为上厕所而放弃了这个自习室机会就不好了,于是张三带着钥匙去上厕所,回来了继续学习;到午饭时间了,张三要去吃午饭,准备把钥匙挂在墙上,张三后面又想,等我再次过来的时候,我就要排到最后一个队伍了,正准备将钥匙挂在墙上,后面又拿回去,再进自习室学一会,在自习室里面又饿的不行,又把钥匙放回门口,后面又立马拿回来进自习室反反复复,张三由于饥饿,来回在自习室与门口挣扎,在自习室里也没有干什么,造成了时间浪费,也让后面的人着急

这种事情没有错,但是不合理--->因为不高效

于是自习室有了新规定:每一个自习完成的同学,归还了钥匙之后不能立马申请,要二次申请必须要重新排队,这样 就合理了,这种保证线程顺序性就是同步

条件变量
  • 当一个线程互斥的访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了

  • 例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量

同步概念与竞态条件
  • 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步

  • 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解

2.条件变量接口

在Linux环境下,条件变量(Condition Variables)通常与互斥锁(Mutexes)一起使用,以实现在多线程编程中的同步机制。条件变量可以使得线程在某个条件不满足时挂起(等待),直到某个条件成立时被唤醒。

Linux下条件变量的接口主要定义在POSIX线程库(pthread)中,以下是一些常用的条件变量相关函数:

pthread_cond_init - 初始化条件变量。

 其中,cond 是指向要初始化的条件变量的指针,attr 是条件变量的属性,如果为NULL,则使用默认属性。

pthread_cond_destroy - 销毁条件变量。

当不再需要一个条件变量时,应该调用这个函数来释放它所占用的资源。

pthread_cond_wait - 等待条件变量。

 

  • cond:指向一个已经初始化的条件变量。

  • mutex:指向一个已经初始化并且被当前线程持有的互斥锁。

函数的行为如下:

  1. 调用 pthread_cond_wait 时,互斥锁 mutex 必须由当前线程持有。

  2. pthread_cond_wait 会自动释放 mutex,这样其他线程可以获取这个互斥锁。

  3. 当前线程进入等待状态,直到条件变量 cond 被信号(pthread_cond_signalpthread_cond_broadcast)。

  4. cond 被信号时,线程从等待状态中被唤醒,并且重新获取 mutex

  5. pthread_cond_wait 返回时,互斥锁 mutex 再次被当前线程持有。

pthread_cond_timedwait - 带超时的等待条件变量。 

pthread_cond_wait类似,但增加了一个超时时间,如果在指定的时间到达之前条件变量没有被信号唤醒,那么函数返回ETIMEOUT。

pthread_cond_signal - 唤醒一个等待条件变量的线程。

 pthread_cond_broadcast - 唤醒所有等待条件变量的线程。

唤醒所有等待该条件变量的线程。

在使用这些函数时,应当确保互斥锁的正确使用,以避免死锁或资源竞争。条件变量通常用于生产者-消费者问题等同步场景中。

 

为什么pthread_cond_wait需要互斥量

  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程

  • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

  • 按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码:

  • 由于解锁和等待不是原子操作。调用解锁之后,pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个 pthread_cond_wait。所以解锁和等待必须是一个原子操作。

  • int pthread_cond_wait (pthread_cond_t *cond,pthread_mutex_t * mutex); 进入该函数后,会去看条件量等于 0 不?等于,就把互斥量变成 1,直到 cond_wait 返回,把条件量改成 1,把互斥量恢复成原样。

3.认识条件变量例子

假设有一个盘子,A同学带上眼罩负责从盘子中拿苹果,B同学不能说话把苹果放在盘子里;如果没有规则,那么会造成A同学不停的拿苹果,无论在与不在,B呢想放就放,不想放就一直不放,但是A确傻傻的一直去拿,浪费时间,占用资源效率低;

为了避免效率低下,B同学拿来一个铃铛,A同学第一次拿没有拿到苹果,A同学就不拿了,B同学把苹果放在盘子里,就敲一下铃铛,告诉A,现在苹果在盘子里了,你可以拿了,于是A收到铃铛信号,A就拿到了苹果,这样就大大提高了效率

这个铃铛就是条件变量

现在C同学也带上眼罩去拿苹果,也就是有两个同学拿苹果,为了提高效率,A和C排队,B敲一下铃铛表明盘子有一个苹果,B敲2下表示盘子有两个苹果,B敲一下A拿到苹果,B等待,A重新排队在B后面;A又敲一下B也拿到苹果,B又排队在A后面;B敲两下,A拿完B拿,又有序排队等待

通过这个例子我们发现,条件变量需要一个线程队列,还需要有一个通知机制(叫醒一个,还是全部叫醒)

  • 【测试代码】

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>

const int num = 5;
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;

void *Wait(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (1)
    {
        pthread_mutex_lock(&gmutex);

        pthread_cond_wait(&gcond, &gmutex);
        usleep(10000);
        std::cout << "I am : " << name << std::endl;

        pthread_mutex_unlock(&gmutex);
    }
}
int main()
{
    pthread_t threads[num];

    for (int i = 0; i < num; i++)
    {
        char *name = new char[1024];
        snprintf(name, 1024, "thread-%d", i + 1);
        pthread_create(threads + i, nullptr, Wait, (void *)name);
    }

    sleep(1);
    // 唤醒其他线程
    while (1)
    {
        pthread_cond_signal(&gcond);
        //pthread_cond_broadcast(&gcond);
        std::cout << "唤醒一个线程...." << std::endl;
        sleep(2);
    }

    for (int i = 0; i < num; i++)
    {
        pthread_join(threads[i], nullptr);
    }

    return 0;
}

 

这里就不多说了,其中可以唤醒一个线程,也可以同时唤醒所有线程,并且唤醒顺序是与创建一致的;如果是一次性唤醒全部,唤醒打印输出可能不一致,那是由于我们没有对打印至显示器进行加锁 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值