多线程 C++

chapter2 管理线程

Posted on 2021-10-03,6 min read

管理线程

Recall

传入thread的参数?临时对象有什么问题?

线程detach引用了主线程的资源?

异常情况下何时调用join?

std::thread 传入 引用?

std::bind? std::decay?

std::ref?

转移线程所有权

thread 是可以被move的,但是不能被copy

比thread_guard更好的scoped_thread?

好在哪里

运行时选择线程数量?

Notes

传入thread的参数?临时对象有什么问题?

  • 可以是一个重载了operator()的struct
class background_task{
public:
	void operator() const{
		do_something();
	}
};
background_task f = background_task();
std::thread t(f);

//使用临时对象,容易报错
std::thread my_thread(background_task()); // 会解析为函数
//正确用法
std::thread my_thread((background_task()));
std::thread my_thread{background_task()};

线程detach引用了主线程的资源?

当线程被detach后,与主线程的生命周期分离,但是如果线程还在引用主线程里的变量,要小心主线程已经结束,释放了那些对象。

处理这种情况的通用做法是让线程函数自包含(译注:也就是函数本身什么都有,不需要依赖外部数据),将数据拷贝到线程中,而非共享数据。

异常情况下何时调用join?

主线程调用子线程的join,要在主线程抛出异常前的对方调用。也需要在异常处理过程中调用join(),从而避免意外的生命周期问题。

  • 使用try catch

    class background_task;
    void f(){
    	int local_var = 0;
    	std::thread t({background_task()});
    	try{
    		do_something_in_current_thread();
    	}catch(...){
    		t.join();
    		throw;
    	}
    	t.join();
    }
    
    
  • 使用RAII

    #include <iostream>
    #include <thread>
    
    struct func;
    
    class thread_guard{
    
    private:    
    	  std::thread &t;
    
    public:
        explicit thread_guard(std::thread& t): t(t){
    
        }
        ~thread_guard(){
            if(t.joinable()){
                t.join();
            }
        }
        thread_guard(const thread_guard& t) = delete;
        thread_guard& operator=(const thread_guard&) = delete;
    
    };
    
    struct functor;
    void f(){
        int local_state = 0;
        struct functor myfunc();
        std::thread t(myfunc);
        thread_guard g(t);
        do_some_thing_in_current_thread();
    }
    

std::thread 传入 引用?

class widget{

};
void update_data(widget& data);
void f2(){
    widget data;
    std::thread t(update_data, data); 
// 编译不通过,尽管update_data的第一个参数期待传入一个引用,
// 但是std::thread的构造函数并不知晓;                  
//构造函数无视函数期待的参数类型,并盲目地拷贝已提供的变量。
}

// 正确用法;类似std::bind
std::thread t(update_data_for_widget,w,std::ref(data));
/*
std::bind always copies its arguments, but callers can achieve the 
effect of having an argument stored by  reference by applying std::ref to it.
*/

Simply speaking, std::decay can remove references from type. For example, std::decay<int&>::type is the same as just int. For std::thread and std::bind, when they store the parameters you passed in, std::decay::type is used as the type for each of the parameters. Therefore, when a reference is passed in, a copy is created instead of simply storing a reference.

#include <iostream>
#include <type_traits>

using namespace std;

int main()
{
        int a = 0;
        int& a_ref = a;
        decay<int&>::type a_copy = a_ref;

        a_ref++;
        cout << a << endl;
        cout << a_copy << endl;
}
//output
/*
0
1
*/

std::ref 可以让引用变得 copyable

To overcome what std::decay does to references, we must have a copiable object which stores a reference internally. This is exactly what [std::reference_wrapper](https://en.cppreference.com/w/cpp/utility/functional/reference_wrapper) does.

std::reference_wrapper is a class template that wraps a reference in a copyable, assignable object. It is frequently used as a mechanism to store references inside standard containers (like std::vector) which cannot normally hold references.

[std::ref](https://en.cppreference.com/w/cpp/utility/functional/ref) is a handy function in the standard library to create a std::reference_wrapper object. With std::ref, we can finally pass references as parameters correctly as shown below (continuing the example above).

void inc(int& x)
{
        x++;
        cout << "Inside inc, x is now " << x << endl;
}
int main()
{
        int a = 0;
        int b = 0;

        auto func1 = bind(inc, a);
        func1();
        cout << "After func1, value of a is " << a << endl;

        auto func2 = bind(inc, ref(b));
        func2();
        cout << "After func2, value of b is " << b << endl;
}

/*output

Inside inc, x is now 1
After func1, value of a is 0
Inside inc, x is now 1
After func2, value of b is 1
*/

转移线程所有权

#include <iostream>
#include <thread>

void func(){
    std::cout << "func" << std::endl;
}

int main(){
    std::thread t(func);
    std::thread& g = t; //right
		// std::thread g = t; //wrong
    g.join();
    std::cout << "main" << std::endl;
}

比thread_guard更好的scoped_thread?

class scoped_thread{
private:
    std::thread t_;
public:
    scoped_thread(std::thread t): t_(std::move(t)){
        if(!t_.joinable()){
            throw std::logic_error("No thread");
        }
    }
    ~scoped_thread(){
        t_.join();
    }
    scoped_thread(const scoped_thread&) = delete;
    scoped_thread& operator=(const scoped_thread&) = delete;
};
struct func;
void f(){
    int some_local_state = 0;
    scoped_thread s(std::thread(func(some_local_state)));
    do_some_thing_in_current_thread();
}
  • 避免了当thread_guard的生命周期长于它所引用的线程时引起不愉快的后果?
  • 还意味着,一旦所有权转移到这个对象以后,没有谁能连接或者分离这个线程(因为thread_guard是传入一个引用,在main thread里仍然可以对那个thread进行操作,而scoped_thread是直接把thread move进scoped_thread对象里了)。

运行时选择线程数量?

std::thread::hardware_concurrency()

SUMMARY: 管理c++的线程,要注意thread不能copy,而且要考虑何时join(thread_guard, scoped_thread)


下一篇: 华为MindSpore数据集加载算子开发→