zoukankan      html  css  js  c++  java
  • C++并发编程 thread

    std::thread

      C++11在标准库中为多线程提供组件, 使用线程需要包含头文件 thread, 其命名空间为 std.

    启动新线程

    每个进程至少有一个线程: 执行main()函数的线程, 其余线程有其各自的入口函数(线程函数)。
    当线程执行完线程函数后, 线程也会退出. 如果不传入线程函数(类似这种形式std::thread t;), 线程不会运行. 线程函数不能重载, 否则不能编译.
    在为一个线程创建了一个 std::thread 对象后, 如果线程已启动(不传入线程序函数时, 线程不会启动), 必须要明确是加入(join)还是分离线程(detach).

            // 启动一个线程:
            void MyThread(const std::string& str)
            {
                PRINT_LINE_INFO();
                std::cout << str << std::endl;
            }
            //std::thread t(MyThread, "Hello C...");
            std::thread t([] {
                MyThread("Hello C...");
                MyThread("Hello C2...");
            });               
            // 对于类方法, 需要使用 std::bind.
            std::thread t(std::bind(&ThreadExample::MyThread, this, "msg"));
            ThreadGuard tg(t);

    如果 std::thread 对象销毁之前还没有调用 join 或 detach, 程序就会终止( std::thread 的析构函数会调用 std::terminate() ). 因此, 即便是有异常存在, 也需要确保线程能够正确的加入(joined)或分离(detached).
    调用 join 或 detach 之前需要调用 joinable() 判断一下线程是否运行. 如果 joinable() 返回 false, 则不需要.
    join()是简单粗暴的等待线程完成, 此时创建 std::thread 对象的线程(以下称主线程)将被阻塞. 如果在线程启动后到主线程在调用 join() 前的代码中发生了异常, 此时将会导致主线程永远没有机会执行.
    针对此问题, 需要使用 RAII 机制来解决, 如创建一个 ThreadGuard 对象, 在析构函数中保证总是可以调用到 join.

            #ifndef _THREAD_GUARD_
            #define _THREAD_GUARD_
    
            #include <thread>
    
            class ThreadGuard
            {
            public:
                ThreadGuard(std::thread& t_) : t(t_){}
    
                ~ThreadGuard()
                {
                    if (t.joinable())
                    {
                        t.join();
                    }
                }
    
                ThreadGuard(const ThreadGuard &) = delete;
                ThreadGuard& operator=(const ThreadGuard &) = delete;
    
            private:
                std::thread& t;
            };
    
            #endif // _THREAD_GUARD_

    如果是分离线程, 必须保证可访问数据的有效性, 否则会产生未定义的行为, 如同单线程中一个对象被销毁后再访问一样.
    处理这种情况的常规方法: 使线程函数的功能齐全, 将数据复制到线程中. 如果使用一个可调用的对象作为线程函数,这个对象就会复制到线程中,而后原始对象就可以销毁. 下面是错误的使用方法示例:

            class Func
            {
                int& i;
            public:
                Func(int& i_) : i(i_) {}
                void operator() ()
                {
                    for (unsigned j = 0; j < 10; ++j)
                    {
                        // 潜在访问隐患:悬空引用 i 
                        std::cout << i << " ";
                    }
                    std::cout << std::endl;
                }
            };
            { // 某个作用域内
                int* p = new int(100);
                Func f(*p);
                std::thread t(f);
                t.detach(); // 不等待线程结束
                delete p;
            } // 新线程可能还在运行

    线程函数

      线程函数可以有不同的参数, 向线程传递参数,只要在构造 std::thread 对象时,按照线程函数参数列表一一对应传入即可。线程函数有几点需要注意的地方:

    (1) 默认的参数会被拷贝到独立的线程中,即使是引用的形式, 如果需要需要传递引用, 需要使用 std::ref 显示说明(并且线程函数参数也需要声明为引用).

            void ThreadParamRef(std::string& str)
            {
                str += " --> add";
            }
    
            void ThreadParam(std::string str)
            {
                str += " --> add";
            }    
        
            std::string str("Hello C++ Thread...");
            //std::thread t(ThreadParamRef, str);
            std::thread t(ThreadParamRef, std::ref(str)); // 只有这种形式才能在线程执行完毕后输出 Hello C++ Thread... --> add 
            //std::thread t(ThreadParam, std::ref(str));
            t.join();
            std::cout << str << std::endl;

    (2)  线程参数传递时需要注意不能传入局部变量, 考虑下面的代码,buffer②是一个指针变量,指向本地变量,然后本地变量通过buffer传递到新线程中②。
      函数有很大的可能,会在字面值转化成 std::string 对象之前崩溃,从而导致线程的一些未定义行为。
      解决方案就是在传递到 std::thread 构造函数之前就将字面值转化为 std::string 对象。

            void f(int i,std::string const& s);
            void oops(int some_param)
            {
                char buffer[1024]; // 1
                sprintf(buffer, "%i",some_param);
                std::thread t(f,3,buffer); // 2
                t.detach();
            }          
            // 正确的方法
            void f(int i,std::string const& s);
            void not_oops(int some_param)
            {
                char buffer[1024];
                sprintf(buffer,"%i",some_param);
                std::thread t(f,3,std::string(buffer)); // 使用std::string,避免悬垂指针
                t.detach();
            }

      (3) 线程函数参数传递时, 可以移动, 但不能拷贝. "移动"是指: 原始对象中的数据转移给另一对象,而转移的这些数据在原始对象中不再保存.

            void ThreadParamUniquePtr(std::unique_ptr<int> up)
            {
                std::cout << (up.get() ? *up : -1) << std::endl;
            }
            std::thread t(ThreadParamUniquePtr, std::move(up));
            //std::thread t(ThreadParamUniquePtr, up);  // 不能编译
            //std::thread t(ThreadParamUniquePtr, std::ref(up)); // 要求线程函数参数也为引用才能编译
            t.join();
            std::cout << (up.get() ? *up : -1) << std::endl; // 将输出-1

    线程所有权转移

      线程是资源独占型, 但可以将所有权转移给别的对象. 如果一个 std::thread 对象与一个运行的线程关联, 此时接受一个新的线程所有权时, 其以前关联的线程将直接调用 std::terminate() 终止程序继续运行.

    std::thread t1(f);
    std::thread t2(f);
    // t1 = std::thread(f); // t1 所有权还没有转移, 不能通过赋一个新值来放弃线程
    // t1 = std::move(t2); // t1 所有权还没有转移, 不能通过赋一个新值来放弃线程
    t1.detach(); 或 t1.join();
    t1 = std::move(t2);
    std::thread t3 = std::move(t1);
    t1 = std::move(t2);

    线程对象也可以在函数中进行转移.

    std::thread f1()
    {
      return std::thread(f);
    }
    std::thread f2()
    {
      std::thread t(f);
      return t;
    }
    void f3(std::thread t);
    void f4()
    {
      f3(std::thread(f));
      std::thread t(f);
      f3(std::move(t));
    }

    由于 std::thread 是可转移的, 如果容器对移动操作支持, 则可以将 std::thread 对象放入其中.

            class Func
            {
                int i;
            public:
                Func(int i_) : i(i_) {}
                void operator() ()
                {
                    for (unsigned j = 0; j < 10; ++j)
                    {
                        std::cout << i << " ";
                    }
                    std::cout << std::endl;
                }
            };
            std::vector<std::thread> threads;
            for (int i = 1; i < 10; i++)
            {
                Func f(i);
                //std::thread t(f);
                //v.push_back(t);     // 不能采用这种方式
                //v.push_back(std::move(t));  // 需要使用移动操作才可以
                threads.push_back(std::thread(f));
            }
            std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join)); // 对每个线程调用join()

    常用函数

      std::thread::hardware_concurrency() 返回 CPU 核心线程数. 如果无法查询系统信息时, 返回0. (static 函数)
      get_id() 返回 std::thread 对象关联的线程的 id. 如果所有权已转移, 或线程函数已返回, 返回0.
      std::this_thread::get_id() 取得当前线程的 id. (static 函数)

    一个更好的ThreadGuard

    #ifndef _THREAD_GUARD_
    #define _THREAD_GUARD_
    
    template <class _Thread>
    class ThreadGuard
    {
    public:
        explicit ThreadGuard(_Thread& t_) : t(t_) {}
    
        ~ThreadGuard()
        {
            if (t.joinable())
            {
                t.join();
            }
        }
    
        ThreadGuard(const ThreadGuard &) = delete;
        ThreadGuard& operator=(const ThreadGuard &) = delete;
    
    private:
        _Thread& t;
    };
    
    #endif // _THREAD_GUARD_

     

     

     

     

     

     

     

     

     

     

  • 相关阅读:
    【软件构造】Lab1基本流程指导及重难点分析
    【软件构造】关于java中List和Set数据结构不同实现方式和不同遍历方式时间效率的探讨与分析
    程序人生-Hello’s P2P
    WinterCamp2017吃饭睡觉记
    bzoj 3144 [Hnoi2013]切糕
    bzoj 1565 [NOI2009]植物大战僵尸
    bzoj 1061 [Noi2008]志愿者招募
    序列
    Philosopher
    时机成熟之时
  • 原文地址:https://www.cnblogs.com/diysoul/p/5934622.html
Copyright © 2011-2022 走看看