• 2022-07-25 C++并发编程(一)



    前言

    C++ 11 之后,并发库比较完善,可较为简单的进行并发编程,而不用直接调用系统api。

    使得并发编程较为方便,但并发编程的逻辑已经变了,已经由单线程的顺序逻辑转换为乱序逻辑,执行顺序需要自己把控。

    同时并发编程使得debug难度飙升,顺序逻辑为基础的 gdb 受到较大限制。

    并发编程的功用也并非只是追求高速度,对于程序的逻辑梳理,方便进行任务的并行处理也是不可或缺。

    并发编程的陷阱有时需要注意,需要并发的场合可能并非那么多,需要比较单线程和多线程的投入产出比。


    一、Hello Concurrent World

    通过简单的示例,了解一下多线程:

    #include 
    #include 
    
    void hello()
    {
        std::cout << "Hello Concurrent World\n" << std::endl;
    }
    
    auto main(int /*unused*/, char * /*argv*/[]) -> int
    {
        std::thread t(hello);
        t.join();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    thread 是C++的线程库,用于开启一个新线程,对于无需控制并发逻辑的程序,直接开启线程,设置线程等待模式即可,非常简单。

    二、线程的发起等待

    1.线程发起

    线程发起是由线程对象初始化引入函数完成的,为了发起线程,需要准备相应的函数或仿函数:

    struct background_task
    {
      public:
        void operator()() const
        {
            std::cout << "q" << std::endl;
            std::cout << "r" << std::endl;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    用函数或仿函数对象初始化线程对象:

        background_task f;
        std::thread my_thread(f);
    
    • 1
    • 2

    2.线程等待方式

    线程有两种等待方式,在主线程等待其他线程结束,或不等待。

    在主线程等待其他线程完成:

        my_thread.join();
    
    • 1

    不等待线程完成,将线程分离

        my_thread.detach();
    
    • 1

    等待线程完成后退出,比较容易理解,除非程序崩溃,你会得到线程函数计算的结果或产生的副作用。

    而不等待线程完成,将线程分离,则较为晦涩,同时由于需要利用主线程的全局对象将线程函数计算结果返回,而主线程结束会导致所有对象的析构,使得多线程的函数达不到目的。所以,对于逻辑不清晰的使用者,基本不会用对。

    三、参数传递

    参数的传递并不复杂,对于一般函数或仿函数对象,只需要在thread的第二个参数顺序给出即可。

    1.传指针陷阱

    这其中有一点需注意,就是如果传入的是局部变量指针,要确保变量的生命周期长到可以在线程初始化过程中存在。

    void funcVariable(int i, const std::string &s)
    {
        std::cout << i << s << std::endl;
    }
    
    void testVariable(int someParam)
    {
        char buffer[] = "hello";
        std::thread test(funcVariable, someParam, buffer);
        test.detach();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    以上代码的问题在于char指针在线程初始化时被带入到线程中,之后才会隐式转换为string,但buffer的生命周期随着线程分离的语句结束后就结束,基本不能支持到线程初始化完成。

    有一个比较取巧的方法,是传入一个带状态的仿函数对象,但效率可能堪忧。

    struct func
    {
        explicit func(const std::string &str_)
            : str(str_)
        {}
    
        void operator()() const
        {
                std::cout << i << std::endl;
        }
    
      private:
        std::string str;
    };
    
    void f()
    {
        char someLocalState[] = "hello";
        func my_func(someLocalState);
        std::thread t(my_func);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    更实际的办法,是传参时就不用指针,直接构造std::string(buffer).

    2.传递引用

    线程传参还有个问题,线程构造的函数形参如果涉及引用,除了考虑生命周期,还要考虑如何实现,由于线程传参的过程会分成两部分,1,传给线程,2,在线程中进行拷贝,导致引用不能直接传过去,编译器不可通过,需要std::ref(引用)方法

    void funcVariable(int i, std::string &s)
    {
        std::cout << i << s << std::endl;
    }
    
    void testVariable(int someParam)
    {
        std::string buffer = "hello";
        std::thread test(funcVariable, someParam, std::ref(buffer));
        test.join();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3.单一所有权变量的转移

    C++中有一类变量是有单一所有权的,比如 std::unique_ptr 只可移动,不可拷贝,这种变量的传递,需要用 std::move(单一所有权变量)的方法。

    void funcUniquePtr(std::unique_ptr<int> someParam)
    {
        std::cout << *someParam << std::endl;
    }
    
    void testUniquePtr()
    {
        std::unique_ptr<int> uPtr(new int(10));
        std::thread thr(funcUniquePtr, std::move(uPtr));
        thr.join();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    总结

    以上内容简述了线程的发起和参数的传递,不难理解,但极易出错。

    对于多线程,比较考验编程者的逻辑,以上例子只是考虑线程传参的对象的生命周期和所有权转移,还未引入数据共享,线程间相互调用,数据竞争等问题,所以深入下去还需费些脑细胞的。

  • 相关阅读:
    https相关内容
    3.4、可靠传输
    排序算法-快速排序法(QuickSort)
    揭秘元宇宙背后最炫科技风:数字经济时代,元宇宙发展解决方案及核心技术
    玉米社:竞价推广账户展现低+点击率低+跳出率高+询盘少怎么办?优化思路
    mysql数据库root密码忘记了,这里有一个简单的方法可以解决
    在spring中使用Validated和@Valid对参数进行校验
    【巧立名目】利用IDEA工具修改Maven多模块项目标识包名全过程
    Hive工作原理
    GerbView生产高级软件,支持新旧表单
  • 原文地址:https://blog.csdn.net/m0_54206076/article/details/125967653