美文网首页C++多线程
2.2 向线程函数传递参数 ~ 2.3转移线程所有权

2.2 向线程函数传递参数 ~ 2.3转移线程所有权

作者: 庄阿莫 | 来源:发表于2018-11-25 01:51 被阅读111次

    一、依赖隐式转换的危险

    void f(int i,  std::string const& s);
    void oops( int some_param )
    {
      char buffer[1024];
      sprintf(buffer, "%i", some_param);
      std::thread t(f, 3, buffer);
      t.detach();
    }
    

    std::thread t 的构造函数中,想要依赖隐式转换,将字面值buffer转换为期待的 string 对象。但是 std::thread t 的构造函数只会复制buffer

    怎么解决?
    buffer传递到 std::thread t的构造函数之前,就将它显示转换为 std::string 对象

    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));//显示转换
      t.detach();
    }
    

    注意:

    • 向线程传递一个引用的变量时,在一些情况下会出现错误
    class func{
    public:
        string &a;
        explicit func(string &a_) : a(a_) {}
    };
    
    void fu(string str, func &b)
    {
        b.a = str;
    }
    
    int main()
    {
        string st("hhh");
        string str("嘿嘿");
    
        func my_func(st);
        std::thread my_thread(fu, str, my_func);//报错
        my_thread.join();
    
        cout << st << endl;
    
    
        return 0;
    }
    

    my_thread构造函数无视函数期待的参数类型,并盲目的拷贝my_func。当线程调用fu函数时,传递给函数的参数是my_func内部拷贝的引用,而非它本身的引用。

    怎么解决?
    使用 std::ref 将参数my_func转换成引用的形式:

    std::thread my_thread(fu, str, std::ref(my_func));
    

    当然,也可以使用bind的方式来解决:

    class func1{
    public:
        int &a;
        explicit func1(int &a_) : a(a_) {}
    
    
        int operator()(int c)
        {
            for(;a < c; ++a)
            {
                cout << a << endl;
            }
            cout << a << endl;
            return a;
        }
    };
    
    int main()
    {
        int st = 6;
        int c = 10;
    
        func1 f(st);
        /*
         *  f::operator ()(int)实际上是一个operator()(func1 * this, int)
         *  因为类的成员函数,并没有在类内占空间,而是利用签名在类外进行查找
         *  传一个对象引用进去,让函数是针对的是哪个实例
         * */
        std::thread my_thread (&func1::operator(), &f, c);
        my_thread.join();
    
        return 0;
    }
    

    二、一个有趣的情况:提供的参数可以移动,但不能拷贝

    "移动"是指:原始对象中的数据转移给另一对象,而转移的这些数据就不再在原始对象中保存了。
    比如:std::unique_ptr
    移动构造函数(move constructor)和移动赋值操作符(move assignment operator)允许一个对象在多个 std::unique_ptr 实现中传递。在使用"移动"转移原对象后,就会留下一个空指针(NULL)。

    当对象是一个临时变量时,自动进行移动操作,例如:string str = string("hhh");。但当对象是一个命名变量,那么转移的时候就需要使用 std::move() 进行显示移动:

    void process_big_object( std::unique_ptr<big_object> );
    
    void main()
    {
      std::unique_ptr<big_object> p_ob(new big_object);
      p_ob->prepare_data(42);
      std::thread t( process_big_object, std::move(p) ); //①
      
      t.join();
    }
    

    ①运行过程:
    对象p_ob 的所有权就被首先转移到新创建线程的的内部存储中,之后传递给process_big_object函数。

    thread 的特性:
    std::thread 实例不像 std::unique_ptr 那样能占有一个动态对象的所有权,但是它能占有其他资源:每个实例都负责管理一个执行线程。执行线程的所有权可以在多个 std::thread 实例中互相转移,这是依赖于 std::thread 实例的可移动且不可复制性
    不可复制性保证了在同一时间点,一个 std::thread 实例只能关联一个执行线程。
    可移动性使得程序员可以自己决定,哪个实例拥有实际执行线程的所有权。

    三、转移线程所有权的基础用法

    void some_function();
    void some_other_function();
    
    std::thread t01(some_function);
    std::thread t02 = std::move(t01);//t01的所有权就转移给t02
    
    /*
    *  与一个临时 std::thread 对象启动了相关联的线程
    *  由于std::thread(some_other_function)是一个临时对象,所以会隐式的调用移动操作
    * */
    t01 = std::thread(some_other_function);
    
    std::thread t03 = std::move(t02);
    t01 = std::move(t03); // ①赋值操作将使程序崩溃
    

    最后一个移动操作使程序崩溃的原因是:
    在 t01 将some_function线程的所有权就转移给t02后,t01 又启动了some_other_function线程。当 t03( some_other_function线程 ) 赋给了t01时,由于t01已经有了一个关联的线程,因此系统直接调std::terminate() 终止程序继续运行。这样做是为了保证与 std::thread 的析构函数的行为一致。

    • ( std::terminate() 是noexcept函数,不抛出异常。)
    • “不抛出异常,与 std::thread 的析构函数的行为一致”
      解析:
      线程对象被析构前,需要显式的等待线程完成,分离它;
      进行赋值时,赋一个新值给 std::thread 对象来"丢弃"一个线程。用这种方式来触发它的析构函数,这是绝对不行的。

    四、转移线程所有权的两种想法

    1,使用函数来返回新线程的所有权

    std::thread f()
    {
      void some_function();
      return std::thread(some_function);
    } 
    
    std::thread g(int a)
    {
      void some_other_function(int);
      std::thread t(some_other_function,a);
      return t;
    }
    

    2,在函数中转移线程所有权

    void func(std::thread t);
    void gunc()
    {
      void some_function();
      func( std::thread(some_function) );
    
      std::thread t(some_function);
      func( std::move(t) );
    }
    

    五、迈出线程自动化管理的第一步

    为了确保线程程序在主线程退出前完成:

    class Threa
    {
      std::thread t;
    
    public:
    
      explicit Threa(std::thread t_): 
          t( std::move(t_) )
      {
        if(!t.joinable())//检测是否为可执行线程,当线程不可加入时,抛出异常
          throw std::logic_error(“No thread”);
      } 
    
      ~Threa()
      {
        t.join();
      } 
    
      Threa( Threa const& )=delete;
      Threa& operator= (Threa const&)=delete;
    };
    
    struct func
    {
      int& i;
      func(int& i_) : i(i_) {}
      void operator() ()
      {
        std::cout << i << std::endl;
      }
    };
    
    void f()
    {
      int some_local_state;
      th t(std::thread(func(some_local_state)));
      do_something_in_current_thread();
    }
    

    当主线程到达f()函数的末尾时,scoped_thread对象开始析构,并阻塞至线程结束。

    使用vector实现初步的线程自动化管理

    void do_work(unsigned id);
    void f()
    {
      std::vector<std::thread> threads;
      for(unsigned i=0; i < 20; ++i)
    {
      threads.push_back(std::thread(do_work,i)); // 产生线程
    } 
    
      std::for_each(threads.begin(),  threads.end(),
                        std::mem_fn(&std::thread::join)); 
    //mem_fn把成员函数转为函数对象,使用对象指针或对象(引用)进行绑定
    }
    

    相关文章

      网友评论

        本文标题:2.2 向线程函数传递参数 ~ 2.3转移线程所有权

        本文链接:https://www.haomeiwen.com/subject/vqeuqqtx.html