• 2022-11-13 C++并发编程( 四十三 )



    前言

    C++11 版本已经有很长一段时间了, 它把 C++ 带入了一个新的境地, 引入了一些有用的特性, 这些特性使得编写并发代码变得容易, 需要掌握, 并且以今天这个时点, 2022年, 这些特性已经极其成熟, 并非什么新鲜事物了.


    一、移动语义与完美转发

    移动语义通俗讲, 就是将对象所持有的资源进行转移,

    类比一下, 师傅有一把祖传宝剑, 名曰巨阙, 师傅收了个徒弟, 手上没有宝剑, 二人去龙潭屠龙, 到了地方, 师傅说我老了, 该退休了, 宝剑巨阙传给你, 去屠龙, 我去喝酒聊天钓鱼了. 宝剑巨阙就是资源, 师傅和徒弟是两个对象, 资源从师傅转移给徒弟, 就是移动语义, 之后徒弟有了资源可以去屠龙( 函数调用 ), 师傅退休( 等待析构, 或重新被转移资源 ).

    有了移动语义, 我们就可以省了另铸一把巨阙剑的时间( 拷贝构造 ), 同时省了销毁原巨阙剑的时间( 析构 ), 里外里赚大发了.

    而完美转发也很神奇, 它令传入的参数形式得以保留, 右值的还是右值, 左值的还是左值.

    #include 
    
    // 移动语义
    struct moveConstruct
    {
        moveConstruct()
            : data(new int[10]())
        {}
    
        moveConstruct(const moveConstruct &rhs)
            : data(new int[10])
        {
            std::copy(rhs.data, rhs.data + 10, data);
        }
    
        // 移动构造
        // 通过移动语义转移资源
        moveConstruct(moveConstruct &&rhs) noexcept
            : data(rhs.data)
        {
            rhs.data = nullptr;
        }
    
        ~moveConstruct()
        {
            delete[] data;
        }
    
        void show()
        {
            for (int i = 0; i != 10; ++i)
            {
                std::cout << *(data + i) << std::endl;
            }
        }
    
      private:
        int *data;
    };
    
    // 参数为右值引用的函数
    void doStuff(moveConstruct &&rhs)
    {
        rhs.show();
    }
    
    template <typename T>
    void print(T & /*unused*/)
    {
        std::cout << "left value" << std::endl;
    }
    
    template <typename T>
    void print(T && /*unused*/)
    {
        std::cout << "right value" << std::endl;
    }
    
    // 完美转发
    template <typename T>
    void perfectForward(T &&rhs)
    {
        print(std::forward<T>(rhs));
    }
    
    auto main() -> int
    {
        // 右值引用
        int &&rValueReference = 42;
    
        std::cout << rValueReference << std::endl;
    
        moveConstruct mCnstr;
    
        // 移动构造时需要使用 std::move() 函数
        moveConstruct mCnstr2(std::move(mCnstr));
    
        // 函数中的右值引用
        doStuff(std::move(mCnstr2));
    
        // 完美转发, 右值引用解读为右值引用
        // void perfectForward(int &&rhs)
        perfectForward(42);
    
        int test = 42;
    
        // 完美转发, 左值引用解读为左值引用
        // void perfectForward(int &rhs)
        perfectForward(test);
    
        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
    • 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

    对于并发编程, 近乎所有的相关对象都是不可拷贝的, 大部分是可移动的, 例如 std::thread, std::unique_lock<>, std::future<>, std::promise<> 和 std::packaged_task<>, 如果不会使用移动语义, 则并发代码的限制将极大, 无法完成很多功能.

    二、删除函数

    我们设计的很多类, 其语义是不支持拷贝的, 比如并发对象, unique_ptr 智能指针, 当我们设计一些封装这些并发对象的类或容器, 就必须禁止拷贝.

    C++ 11 中引入删除函数, 可以轻松的做到这一点 ( 没有这个特性时, 是将拷贝构造和拷贝赋值放到私有函数中 )

    // 只可移动不可拷贝的类
    struct moveOnly
    {
        moveOnly()
            : noCopy(new int[10]())
        {}
    
        // 不可拷贝
        moveOnly(const moveOnly &) = delete;
    
        // 不可拷贝赋值
        auto operator=(const moveOnly &) -> moveOnly & = delete;
    
        // 可移动构造
        moveOnly(moveOnly &&rhs) noexcept
            : noCopy(rhs.noCopy)
        {
            rhs.noCopy = nullptr;
        }
    
        // 可移动赋值
        auto operator=(moveOnly &&rhs) noexcept -> moveOnly &
        {
            if (noCopy != rhs.noCopy)
            {
                delete[] noCopy;
                noCopy = rhs.noCopy;
                rhs.noCopy = nullptr;
            }
            return *this;
        }
        
        ~moveOnly()
        {
            delete[] noCopy;
        }
    
      private:
        int *noCopy = nullptr;
    };
    
    • 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

    我们做线程池, 用到装载任务的队列, 装载线程的容器, 都是不可拷贝的, 必须将相应的构造函数, 赋值函数删除, 否则会给使用者很大的困惑.

    三、lambda

    对于 C++ 并发编程, lambda 在我看来是必须掌握的知识.

    我们使用环境变量 std::condition_variable::wait( ), 需要它实现谓词, 使用 std::packaged_task< > 需要它包裹任务, 使用 std::thread 线程构造, 需要它提供可调用仿函数对象, 线程池中, 更需要它打包任务.

    lambda 在 C++ 多线程编程中几乎无处不在, 有了它, 才能有简洁而清晰的代码.

    所以, 必须掌握.

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    std::condition_variable condVar;
    bool dataReady;
    std::mutex mut;
    
    void waitForData()
    {
        std::unique_lock<std::mutex> lock(mut);
        condVar.wait(lock, [] { return dataReady; }); // 作为条件变量判断谓词
    
        condVar.wait(lock, []() -> bool {
            if (dataReady)
            {
                std::cout << "data ready" << std::endl;
                return true;
            }
    
            std::cout << "data not ready, resuming wait" << std::endl;
            return false;
        });
    }
    
    auto makeOffseter(int offset) -> std::function<int(int)>
    {
        return [=](int j) { return offset + j; };
    }
    
    struct captureNumber
    {
        void foo(std::vector<int> &vec)
        {
            // 访问成员变量, 需要显示捕获 this
            std::for_each(vec.begin(), vec.end(),
                          [this](int &i) -> void { i += numberData; });
        }
    
      private:
        int numberData = 0;
    };
    
    // C++14 广义捕获
    auto spawnAsyncTask() -> std::future<int>
    {
        // 局部 promise, 函数结束后销毁
        std::promise<int> prms;
    
        // 获取 future
        auto ftr = prms.get_future();
    
        // 开启线程, lambda 捕获移动对象, 将其转移至线程,
        // 函数结束 prms 销毁, 也不会悬空引用
        // 为了更改捕获对象, 需要加 mutable
        std::thread thrd([prms = std::move(prms)]() mutable { prms.set_value(5); });
    
        // 分离线程
        thrd.detach();
    
        // 返回 future
        return ftr;
    }
    
    auto main() -> int
    {
        []() { std::cout << 1 << std::endl; }(); // 直接调用
    
        std::vector<int> iVec{1, 2, 3};
        std::for_each(iVec.begin(), iVec.end(),
                      [](int i) { std::cout << i << std::endl; }); // 作为算法谓词
    
        std::function<int(int)> const offset42 = makeOffseter(42);
        std::function<int(int)> const offset123 = makeOffseter(123);
    
        std::cout << offset42(12) << "," << offset123(12) << std::endl;
        std::cout << offset42(12) << "," << offset123(12) << std::endl;
    
        int offset = 42;
        std::function<int(int)> offsetA = [&](int j) { return offset + j; };
    
        offset = 52;
        std::cout << offsetA(12) << std::endl; // 引用只会使用当前值 52, 输出64
    
        int value = 5;
        int refer = 10;
    
        const std::function<int()> func =
            [value, &refer]() { // 分别使用拷贝 value, 引用 refer
                return value + refer;
            };
    
        value = 0;
        refer = 20;
        std::cout << func() << std::endl; // value 使用 5, refer 使用20
    
        // C++ 14 后 lambda 可以使用 auto 推断参数, 变成通用 lambda
        auto func2 = [](auto x) { std::cout << "x = " << x << std::endl; };
        func2(42);
        func2("hello");
    
        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
    • 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
    • 103
    • 104
    • 105
    • 106

    四、thread_local 线程本地变量

    线程本地变量是每个线程独立的实例拷贝, 意味着同样的变量名称, 在每个线程可能拥有不同的值, 这在并发编程中, 不可或缺.

    我们在讲防止死锁的层级锁, 风险指针构造的无锁栈, 中断线程都用到了线程本地变量.

    对于全局和静态数据成员线程本地变量, 需要在线程使用前进行构建, 函数内部的非静态线程本地变量, 则只有在线程调用此函数时进行初始化.

    目前用 lldb 和 gdb 难以直接追踪 thread_local 变量.

    线程本地变量是可以被获取地址的, 如果其指针被传给其它线程, 和其它普通指针一样, 要注意其生存期, 不要超过其生存期进行指针解引用.

    #include 
    #include 
    #include 
    
    // 命名空间内的线程本地变量
    thread_local int thrdLcInt;
    
    struct thrdLc
    {
        // 线程本地静态成员变量
        static thread_local std::string thrdLcstr;
    };
    
    // 静态成员需类外定义
    thread_local std::string thrdLc::thrdLcstr;
    
    void foo()
    {
        // 函数内部线程本地变量
        thread_local const std::vector<int> iVec{1, 2, 3};
    
        for (const auto &i : iVec)
        {
            std::cout << i << std::endl;
        }
    }
    
    auto main() -> int
    {
        foo();
    
        thrdLc::thrdLcstr = "test";
    
        std::cout << thrdLc::thrdLcstr << std::endl;
    
        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

    总结

    今天先介绍到这里.

  • 相关阅读:
    卷积神经网络CNN基础知识
    ChatGPT的未来发展
    Vue 使用 v-bind 动态绑定 CSS 样式
    elementui el-tooltip文字提示组件弹出层内容格式换行处理
    基于Qt HTTP应用程序项目案例
    图像处理与计算机视觉--第四章-图像滤波与增强-第一部分
    想学设计模式、想搞架构设计,先学学 UML 系统建模吧
    HTML事件的种类
    NEOVIM下载安装与配置
    Xilinx IDDR与ODDR原语的使用
  • 原文地址:https://blog.csdn.net/m0_54206076/article/details/127829389