C++ 多线程进程顺序控制方案、解析与对比

7 篇文章 0 订阅
4 篇文章 0 订阅

C++ 多线程进程顺序控制

例题 (简单)leetcode 1114
主要代码参考:https://leetcode.cn/problems/print-in-order/comments/490373

atomic(原子对象) < condition_variable(条件变量) < posix semaphore(信号量) < mutex (互斥锁)< promise

atomic

atomic 汇编级别操作,主要是防止一个单元被读-修改(不同方式)-写的时候被两个进程写导致错误而出现
读-修改-写三个动作变成一个,不能中断

每个 std::atomic 模板的实例化和全特化定义一个原子类型。若一个线程写入原子对象,同时另一线程从它读取,则行为良好定义。

另外,对原子对象的访问可以建立线程间同步,并按 std::memory_order 所对非原子内存访问定序。
std::atomic 既不可复制亦不可移动。

参考:

atomic: https://zhuanlan.zhihu.com/p/463913671
https://blog.csdn.net/Cdreamfly/article/details/123122341
https://cloud.tencent.com/developer/section/1008933
https://blog.csdn.net/liuhhaiffeng/article/details/52604052

代码:
class Foo {
private:
    std::atomic<bool> firstAtomLock;
    std::atomic<bool> secondAtomLock;
public:
    Foo() {
        std::atomic_init(&this->firstAtomLock,false);
        std::atomic_init(&this->secondAtomLock,false);
    }


    void first(function<void()> printFirst) {
        // printFirst() outputs "first". Do not change or remove this line.
        printFirst();
        firstAtomLock.store(true);
    }

    void second(function<void()> printSecond) {
        while(!firstAtomLock.load()){
            std::this_thread::yield();
        }
        // printSecond() outputs "second". Do not change or remove this line.
        printSecond();
        secondAtomLock.store(true);
    }

    void third(function<void()> printThird) {
        while(!secondAtomLock.load()){
            std::this_thread::yield();
        }
        // printThird() outputs "third". Do not change or remove this line.
        printThird();
    }
};
解释:

定义于stdatomic.h

1)void atomic_init(volatile A*obj,C desired);

​ obj - 指向要初始化的原子对象的指针
​ desired - 用以初始化原子对象的值
此后firstReady 和 secibdReady就变成原子对象,只能通过 **.store(bool)修改,.load(bool)**查询

2)std::this_thread::yield();

将当前线程所抢到的CPU”时间片A”让渡给其他线程(其他线程会争抢

condition_variable(条件变量)

是允许多个线程相互交流的同步原语。
它允许一定量的线程等待(可以定时)另一线程的提醒,然后再继续。
条件变量始终关联到一个互斥。
condition_variable 和 mutex 结合使用,因此condition_variable 更多是为了通知、顺序之类的控制

参考:

https://blog.csdn.net/qq_46615150/article/details/114520411?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-114520411-blog-125340274.pc_relevant_multi_platform_whitelistv3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-114520411-blog-125340274.pc_relevant_multi_platform_whitelistv3&utm_relevant_index=1 https://blog.csdn.net/TwoTon/article/details/123752423
https://blog.csdn.net/wangxu696200/article/details/122775502
https://blog.csdn.net/ccw_922/article/details/124662275

代码:
class Foo {
public:
    Foo() {
        firstReady = false;
        secondReady = false;
    }

bool firstReady, secondReady;
mutex mtx;
condition_variable cv1,cv2;

void first(function<void()> printFirst) {
    lock_guard<mutex> l(mtx);
    // printFirst() outputs "first". Do not change or remove this line.
    printFirst();
    firstReady = true;
    cv1.notify_one();
}

void second(function<void()> printSecond) {
    unique_lock<mutex> ul(mtx);
    cv1.wait(ul, [&]{ return this->firstReady; });

    // printSecond() outputs "second". Do not change or remove this line.
    printSecond();
    secondReady = true;
	cv2.notify_one();
}

void third(function<void()> printThird) {
    unique_lock<mutex> ul(mtx);
    cv2.wait(ul, [&]{ return this->secondReady; });
    // printThird() outputs "third". Do not change or remove this line.
    printThird();
}

};
解释:
1)lock_guard l(m)

约等于ul就变成个函数结束自动释放的零时锁,且拥塞时自动释放,约等于升级版锁
m为mutex对象地址
使用mutex时,如果加锁后忘记解锁,或者在使用的过程中出现异常,而没有在异常处理中解锁,那么锁会一直处于加锁状态,而无法解锁。
lock_guard类通过在对象构造的时候对mutux进行加锁,当对象离开作用域时自动解锁,从而避免加锁后没有解锁的问题:
lock_guard不能在中途解锁,只能通过析构时解锁。
lock_guard对象不能被拷贝和移动。

2)condition_variable.wait ()

当前线程调用wait()后将被阻塞,直到另外某个线程调用notify_*唤醒当前线程;当线程被阻塞时,该函数会自动调用std::mutex的unlock()释放锁,使得其它被阻塞在锁竞争上的线程得以继续执行。一旦当前线程获得通知(notify,通常是另外某个线程调用notify_*唤醒了当前线程),wait()函数也是自动调用std::mutex的lock()。wait分为无条件被阻塞和带条件的被阻塞两种。
分为待条件和不带条件的,不带条件就说进程卡死了主动unlock被锁的元素,待条件就第二个参数为true是才释放下一语句

3)cv2.notify_one();

notify_one()因为只唤醒一个线程,(这个被唤醒线程一般是队列的第一个线程,这个结论是我自己做实现后感觉的,如果不对,请大家指出来,我更改哈)不存在锁争用,所以能够立即获得锁。其余的线程不会被唤醒,需要等待再次调用notify_one()或者notify_all()。
notify_all()会唤醒所有阻塞的线程,存在锁争用,只有一个线程能够获得锁。那其余未获取锁的线程接着会怎么样?会阻塞?还是继续尝试获得锁?答案是会继续尝试获得锁(类似于轮询),而不会再次阻塞。当持有锁的线程释放锁时,这些线程中的一个会获得锁。而其余的会接着尝试获得锁。

4)unique_lock ul(mtx);

unique_lock中的unique表示独占所有权。
unique_lock独占的是mutex对象,就是对mutex锁的独占。
用法:
(1)新建一个unique_lock 对象
(2)给对象传入一个std::mutex 对象作为参数;
std::mutex mymutex; unique_lock lock(mymutex); 因此加锁时新建一个对象lock; unique_lock lock(mymutex);而这个对象生命周期结束后自动解锁。

5) unique_lock与lock_guard区别

unique_lock和lock_guard都不能复制
lock_guard不能移动,但是unique_lock可以移动。

// unique_lock 可以移动,不能复制
std::unique_lock<std::mutex> guard1(_mu);
std::unique_lock<std::mutex> guard2 = guard1;  // error
std::unique_lock<std::mutex> guard2 = std::move(guard1); // ok

// lock_guard 不能移动,不能复制
std::lock_guard<std::mutex> guard1(_mu);
std::lock_guard<std::mutex> guard2 = guard1;  // error
std::lock_guard<std::mutex> guard2 = std::move(guard1); // error

其实更主要的是cv.wait第一个参数必须是unique_lock,不支持lock_guard

————————————————

semaphore

信号量 (semaphore) 是一种轻量的同步原件,用于制约对共享资源的并发访问。在可以使用两者时,信号量能比条件变量更有效率。

condition_variable 必须和 mutex 配对使用,semaphore 和 mutex 是可以独立使用的

semaphore 对 acquire 和 release 操作没有限制,可以在不同线程操作;可以仅在线程 A 里面acquire,仅在线程 B 里面 release。mutex 的 lock 和 unlock 必须在同一个线程配对使用;也就是说线程 A 内 mutex 如果 lock了,必须在线程 A 内 unlock,线程 B 内 lock 了,也必须在线程 B 内 unlock。

参考:

https://blog.csdn.net/qq_46615150/article/details/114520411
https://blog.csdn.net/weixin_43340455/article/details/125435841

代码:
#include<semaphore.h>
class Foo {
protected:
    sem_t firstLock;
    sem_t secondLock;
public:
    Foo() {
        sem_init(&firstLock, 0, 0);
        sem_init(&secondLock, 0, 0);    
    }

    void first(function<void()> printFirst) {
        
        // printFirst() outputs "first". Do not change or remove this line.
        printFirst();
        sem_post(&firstLock);
    }

    void second(function<void()> printSecond) {
        sem_wait(&firstLock);
        // printSecond() outputs "second". Do not change or remove this line.
        printSecond();
        sem_post(&secondLock);
    }

    void third(function<void()> printThird) {
        sem_wait(&secondLock);
        // printThird() outputs "third". Do not change or remove this line.
        printThird();
    }
};
解释:
sem_init

extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));

__sem为指向信号量结构的一个指针;

__pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;

__value给出了信号量的初始值。

函数sem_post( sem_t *sem )用来增加信号量的值当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不再阻塞,选择机制同样是由线程的调度策略决定的。

函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。

函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。

函数sem_destroy(sem_t *sem)用来释放信号量sem。

mutex

单纯的开关锁,关了不继续运行,开了自动竞争,这里写成两个锁,就没有竞争问题了

参考:
代码:
#include<mutex>
class Foo {
protected:
    mutex m1,m2;
public:
    Foo() {
        m1.lock();
        m2.lock();
    }

    void first(function<void()> printFirst) {
        
        // printFirst() outputs "first". Do not change or remove this line.
        printFirst();
        m1.unlock();
    }

    void second(function<void()> printSecond) {
        m1.lock();
        // printSecond() outputs "second". Do not change or remove this line.
        printSecond();
        m2.unlock();
    }

    void third(function<void()> printThird) {
        m2.lock();
        // printThird() outputs "third". Do not change or remove this line.
        printThird();
    }
};
解释:

mutex.lock() mutex.unlock() 一看就懂,不懂自己查

注意初始化为锁定

promise

std::promise是一个类模板,能够在某个线程中给它赋值,然后我们可以再其它线程中把这个值取出来。

stf::future和 promise绑定,用于获取线程返回值,实现线程之间数值交换

初始化: std::promise aaa;

int为保存值的类型指定

future通过.get获取具体值

参考:

https://blog.csdn.net/FairLikeSnow/article/details/117905194

代码:
class Foo {
public:
    Foo() {

    }

    void first(function<void()> printFirst) {
        // printFirst() outputs "first". Do not change or remove this line.
        printFirst();
        p1.set_value();
    }

    void second(function<void()> printSecond) {
        // printSecond() outputs "second". Do not change or remove this line.
        p1.get_future().wait();
        printSecond();
        p2.set_value();
    }

    void third(function<void()> printThird) {
        // printThird() outputs "third". Do not change or remove this line.
        p2.get_future().wait();
        printThird();
    }

private:
    std::promise<void> p1;
    std::promise<void> p2;
};
解释:

promise::set_value()

一个原子函数,用于更改promise对象存储值,且只能修改一次!否则会报错

promise.get_future().wait()

获取promise的future,只有在promise被set后才能获取,进而放行。当作锁用

可以理解为等待资源全收集齐了,才自动开始线程

总结:

(测了一下,时间都很短,运行时间波动很大,写的时间是实测最短,所以时间就算参考了

promise(60ms):主要用于线程间数据通信,其“数据准备”可以当线程控制的锁,不知道为啥公认效率最高。。。

mutex(64ms):最简单的锁,一个类中多个线程函数抢一个公用锁,没啥好说的

semaphore(56ms):信号量,个人最喜欢的一种线程控制,效率挺高,比起锁多几个支持的线程

condition_variable(72ms):情况变量,个人理解约等于锁加上自动开关

atomic(160s):底层控制原子锁,看起来是汇编级的,可能因此锁的比较多,速度是最慢的

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值