• 2022-09-14 C++并发编程(二十二)



    老林的C语言新课, 想快速入门点此 <C 语言编程核心突破>



    前言

    当我们把原子操作的内存次序带入程序中,会出现程序运行逻辑的改变,这是相当费脑筋的活儿。

    我们原本的逻辑中,先写入变量的如果在后写入变量的之后读取,那如果后写入的变量被成功读取更新值,则先写入的变量被读取时,也必然是更新值。

    但涉及多线程时,以上逻辑不一定成立。

    做个类比:

    高考,小明和小丽是邻居,爸妈都相互认识,二人在同一考场,他们的父母都在外等候。

    小明先于小丽交卷,随后小丽也交卷。然后小明去了个洗手间,小丽直接出了考场。

    当小丽的父母见到小丽时,如果小丽不告诉小明的父母,小明先自己交了考卷,那小明的父母怎么知道小明是否已经交卷?

    但如果小丽见到了小明的父母说小明先于我交卷,已经出来了,没见到是因为去了厕所,则小明的父母自然知道小明已经交卷。

    单线程就是考场老师,必然知道小明先于小丽交了卷子。

    多线程就是场外的父母,无法知道小明是否先于小丽交了卷子,除非小丽告诉他们,否则谁知道小明是没交卷还是交完卷子上厕所。


    一、先后一致次序

    最严格的次序,就是最简单的次序,所有操作都服从先后顺序,包括多个线程之间也是如此。

    如下示例:x, y, 分别在不同的线程中进行写入操作,如果线程 readXThenY() 中,x 读取了更新值,则 y 在此线程中可能读取了更新值,也可能没有读取更新值。

    如果 y 没有读取更新值,在线程 readYThenX() 中,会阻塞在第一步 while (!y.load(std::memory_order_seq_cst)),当 y 读取更新值后,x 必然读取更新值,因为前一个进程 readXThenY() 中 x 读取更新值是先于 y 的。

    反过来也是同样道理。

    #include 
    #include 
    #include 
    #include 
    
    std::atomic<bool> x, y;
    std::atomic<int> z;
    
    void writeX()
    {
        x.store(true, std::memory_order_seq_cst);
    }
    
    void writeY()
    {
        y.store(true, std::memory_order_seq_cst);
    }
    
    void readXThenY()
    {
        while (!x.load(std::memory_order_seq_cst))
        {
        }
    
        if (y.load(std::memory_order_seq_cst))
        {
            ++z;
        }
    }
    
    void readYThenX()
    {
        while (!y.load(std::memory_order_seq_cst))
        {
        }
    
        if (x.load(std::memory_order_seq_cst))
        {
            ++z;
        }
    }
    
    auto main() -> int
    {
        x = false;
        y = false;
        z = 0;
    
        std::thread a(writeX);
        std::thread b(writeY);
        std::thread c(readXThenY);
        std::thread d(readYThenX);
    
        a.join();
        b.join();
        c.join();
        d.join();
        assert(z.load() != 0);
    
        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
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    二、宽松次序

    宽松次序,字面上就是虽然在一个线程内遵循先行关系,但在对应的读写线程中,不保证同步。

    以下示例中,虽然在 writeXThenY() 线程中,x 先于 y 进行原子写入,但在 readYThenX() 线程中,当 y 读取了更新值,却没有发出同步指令,以至于 x 是否读取了更新值,成了一个迷。

    于是 assert(z.load(std::memory_order_relaxed) != 0); 断言有可能被出发。

    #include 
    #include 
    #include 
    #include 
    
    std::atomic<bool> x, y;
    std::atomic<int> z;
    
    void writeXThenY()
    {
        x.store(true, std::memory_order_relaxed);
        y.store(true, std::memory_order_relaxed);
        std::cout << "x, y, yes\n";
    }
    
    void readYThenX()
    {
        while (!y.load(std::memory_order_relaxed))
        {
            std::cout << "y, not yes\n";
        }
    
        if (x.load(std::memory_order_relaxed))
        {
            ++z;
            std::cout << "x, yes\n";
        }
    }
    
    auto main() -> int
    {
        x = false;
        y = false;
        z = 0;
    
        std::thread a(writeXThenY);
        std::thread b(readYThenX);
        a.join();
        b.join();
        assert(z.load(std::memory_order_relaxed) != 0);
    
        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

    在多个线程上进行内存宽松次序的读写操作,最终只能导致完全的乱序。

    以下示例中,x, y, z 的原子读写完全时宽松次序的,也就是说,只有在一个线程内部,一个原子变量的读取和写入操作是可以预期的,其他只进行读操作的原子变量,无法保证读取的是更新值。

    当然,由于多线程本身的乱序逻辑,就算用最严格的先后一致次序,结果也是相似的。

    输出的结果,如(0,0,0),会保证其中一个位置的值每次递增 1,如(1,0,0) (2,0,0) (3,0,0),其余的两个位置,递增与否,每次递增值是多少,则完全是个谜。

    如果要在高速运行的计算机上看到上述结果,需要调整 const unsigned loopCount = 1000; 否则你看到的就是貌似非常有规律的递增。

    #include 
    #include 
    #include 
    
    std::atomic<int> x(0);
    std::atomic<int> y(0);
    std::atomic<int> z(0);
    
    std::atomic<bool> go(false);
    
    const unsigned loopCount = 10;
    
    struct readValues
    {
        int x;
        int y;
        int z;
    };
    
    readValues values1[loopCount];
    readValues values2[loopCount];
    readValues values3[loopCount];
    readValues values4[loopCount];
    readValues values5[loopCount];
    
    void increment(std::atomic<int> *varToInc, readValues *values)
    {
        while (!go)
        {
            // CPU时间片让渡,如有其他线程争夺此CPU时间,让其先执行
            std::this_thread::yield();
        }
    
        for (unsigned i = 0; i < loopCount; ++i)
        {
            values[i].x = x.load(std::memory_order_relaxed);
            values[i].y = y.load(std::memory_order_relaxed);
            values[i].z = z.load(std::memory_order_relaxed);
            varToInc->store(static_cast<int>(i) + 1, std::memory_order_relaxed);
            //    std::this_thread::yield();
        }
    }
    
    void readVals(readValues *values)
    {
        while (!go)
        {
            std::this_thread::yield();
        }
    
        for (unsigned i = 0; i < loopCount; ++i)
        {
            values[i].x = x.load(std::memory_order_relaxed);
            values[i].y = y.load(std::memory_order_relaxed);
            values[i].z = z.load(std::memory_order_relaxed);
            //    std::this_thread::yield();
        }
    }
    
    void print(readValues *v)
    {
        for (unsigned i = 0; i != loopCount; ++i)
        {
            if (i != 0U)
            {
                std::cout << ",";
            }
            std::cout << "(" << v[i].x << "," << v[i].y << "," << v[i].z << ")";
        }
        std::cout << std::endl;
    }
    
    auto main() -> int
    {
        std::thread t1(increment, &x, values1);
        std::thread t2(increment, &y, values2);
        std::thread t3(increment, &z, values3);
        std::thread t4(readVals, values4);
        std::thread t5(readVals, values5);
    
        go = true;
    
        t5.join();
        t4.join();
        t3.join();
        t2.join();
        t1.join();
    
        print(values1);
        print(values2);
        print(values3);
        print(values4);
        print(values5);
    
        return 0;
    }
    
    //(0,0,0),(1,0,0),(2,0,0),(3,0,0),(4,0,0),(5,0,0),(6,0,0),(7,0,0),(8,0,0),(9,0,0)
    //(10,0,10),(10,1,10),(10,2,10),(10,3,10),(10,4,10),(10,5,10),(10,6,10),(10,7,10),(10,8,10),(10,9,10)
    //(10,0,0),(10,0,1),(10,0,2),(10,0,3),(10,0,4),(10,0,5),(10,0,6),(10,0,7),(10,0,8),(10,0,9)
    //(10,0,10),(10,0,10),(10,0,10),(10,0,10),(10,0,10),(10,0,10),(10,0,10),(10,0,10),(10,0,10),(10,0,10)
    //(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10)
    
    • 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
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102

    三、获取-释放次序

    获取-释放操作不会构成单一的全局总操作序列,

    如果没有明确的先行关系,则原子操作只能保证本身的读写同步,以下示例中,在原子写入时 x,y 之间没有先行关系,所以会出现多种结果:

    线程 readXThenY() 和 readYThenX() 中 x 读取了更新的值,y 也读取了更新的值。 不会触发断言 assert(z.load() != 0);

    线程 readXThenY() 和 readYThenX() 中,一个线程 x 读取了更新的值,y 也读取了更新的值,但另一个线程中,只有一个原子变量读取了更新值。 不会触发断言 assert(z.load() != 0);

    线程 readXThenY() 和 readYThenX() 中, x,y 没有同时读取更新的值。 触发断言 assert(z.load() != 0); 本例中可能性极小,但确实是有可能出现此种情况的。

    #include 
    #include 
    #include 
    #include 
    
    std::atomic<bool> x;
    std::atomic<bool> y;
    
    std::atomic<int> z;
    
    void writeX()
    {
        //以std::memory_order_release 内存次序保证配对线程中 x 的读写同步
        x.store(true, std::memory_order_release);
    }
    
    void writeY()
    {
        //以std::memory_order_release 内存次序保证配对线程中 y 的读写同步
        y.store(true, std::memory_order_release);
    }
    
    void readXThenY()
    {
        //由于 x,y 没有明确的先行关系或同步关系,只能保证 x 的同步
        while (!x.load(std::memory_order_acquire))
        {
        }
        if (y.load(std::memory_order_acquire))
        {
            ++z;
        }
    }
    
    void readYThenX()
    {
        //由于 x,y 没有明确的先行关系或同步关系,只能保证 y 的同步
        while (!y.load(std::memory_order_acquire))
        {
        }
        if (x.load(std::memory_order_acquire))
        {
            ++z;
        }
    }
    
    auto main() -> int
    {
        x = false;
        y = false;
        z = 0;
        //
        std::thread a(writeX);
        std::thread b(writeY);
        std::thread c(readXThenY);
        std::thread d(readYThenX);
        
        a.join();
        b.join();
        c.join();
        d.join();
        
        assert(z.load() != 0);
        
        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
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    通过线程内的先行关系和线程间的同步关系,进行线程间同步传递:

    以下示例演示了如何传递同步:

    y 在同线程内保证 x 完成写入操作后才完成写入,在另一配对线程,y 保证在 x 完成读取操作前读取原子写入操作后的值。

    逻辑上保证了,如果 y 成功读取了原子写入操作的值,那么 x 读取的,必然是原子写入操作的值。

    #include 
    #include 
    #include 
    #include 
    
    std::atomic<bool> x;
    std::atomic<bool> y;
    
    std::atomic<int> z;
    
    void writeXThenY()
    {
        x.store(true, std::memory_order_relaxed);
        //通过 std::memory_order_release 内存次序保证上面的写操作完成
        y.store(true, std::memory_order_release);
    }
    
    void readYThenX()
    {
        //当以std::memory_order_acquire 内存次序同步读取 y 值,保证了 x 读取的同步
        while (!y.load(std::memory_order_acquire))
        {
        }
        if (x.load(std::memory_order_relaxed))
        {
            ++z;
        }
    }
    
    auto main() -> int
    {
        x = false;
        y = false;
        z = 0;
    
        std::thread a(writeXThenY);
        std::thread b(readYThenX);
    
        a.join();
        b.join();
    
        assert(z.load() != 0);
    
        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

    四、通过获取-释放次序传递同步

    因在一个线程内部遵循先行关系,将线程内的最后一个原子写操作辅以 std::memory_order_release 内存次序,保证其之前所有的原子写操作完成。

    通过中间线程,读取最后写操作的值,并辅以 std::memory_order_acquire 内存次序,同步所有写操作。

    并以 std::memory_order_release 内存次序在同一线程同步读操作之后,写入另一个值。

    并通过其他线程的 std::memory_order_acquire 内存次序读操作同步上述写操作,将第一个线程的所有写操作同步给本线程,完成同步操作的传递。

    #include 
    #include 
    #include 
    #include 
    
    std::atomic<int> data[5];
    
    std::atomic<bool> sync1(false);
    std::atomic<bool> sync2(false);
    
    void thread1()
    {
        data[0].store(42, std::memory_order_relaxed);
        data[1].store(97, std::memory_order_relaxed);
        data[2].store(17, std::memory_order_relaxed);
        data[3].store(-141, std::memory_order_relaxed);
        data[4].store(2003, std::memory_order_relaxed);
        //内存次序为release:
        //本线程中,所有之前的写操作完成后才能执行本条原子操作
        //保证此条原子语句完成时上面所有的写操作均已完成
        sync1.store(true, std::memory_order_release);
    }
    
    void thread2()
    {
        //内存次序为acquire:
        //本线程中,所有后续的读操作必须在本条原子操作完成后执行
        //保证此条原子语句完成时,thread1 中所有的写操作对应的读操作均已同步
        while (!sync1.load(std::memory_order_acquire))
        {
        }
        //内存次序为release:
        //本线程中,所有之前的写操作完成后才能执行本条原子操作
        //用以传递同步,将 thread1 的写操作同步给 thread3 中的读操作
        sync2.store(true, std::memory_order_release);
    }
    
    void thread3()
    {
        //内存次序为acquire:
        //本线程中,所有后续的读操作必须在本条原子操作完成后执行
        //保证此条原子语句完成时,thread2 中所有的写操作对应的读操作均已同步
        //将 thread1 的同步操作传递给 thread3
        while (!sync2.load(std::memory_order_acquire))
        {
        }
        assert(data[0].load(std::memory_order_relaxed) == 42);
        assert(data[1].load(std::memory_order_relaxed) == 97);
        assert(data[2].load(std::memory_order_relaxed) == 17);
        assert(data[3].load(std::memory_order_relaxed) == -141);
        assert(data[4].load(std::memory_order_relaxed) == 2003);
    }
    
    auto main() -> int
    {
        std::thread t1(thread1);
        std::thread t2(thread2);
        std::thread t3(thread3);
    
        t1.join();
        t2.join();
        t3.join();
    
        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
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    总结

    辅以内存次序的原子操作,会有不同的执行逻辑,较单线程的顺序逻辑,复杂了不是一点半点。

    所以在程序设计时,一旦涉及此种情况,逻辑一定要清晰,能简单的务必不要复杂化,否则很容易把自己绕进去。


    老林的C语言新课, 想快速入门点此 <C 语言编程核心突破>


  • 相关阅读:
    接口与外设数据传送方式(笔记)
    h5开发网站-css实现页面的背景固定定位
    Java设计模式之策略模式
    heic格式图片怎么转换jpg?
    我如何编码8个小时而不会感到疲倦。
    Feign实现各个服务之间的远程调用问题
    5款免费的项目管理软件(推荐收藏)
    Linux ssh协议
    平衡搜索树——B-树小记
    深入理解数据库事务:确保数据完整性与一致性
  • 原文地址:https://blog.csdn.net/m0_54206076/article/details/126845714