【C++ 并发与多线程】std::thread类-多线程传参

本文介绍了C++中std::thread的参数传递,包括默认的拷贝行为、引用传递以及如何处理不可拷贝对象。同时讲解了线程所有权的转移,强调了std::move在移动线程所有权中的作用。还提到了线程相关的知识点,如获取硬件并发线程数和识别线程ID。

线程传参

正常的线程传参是很简单的,但是需要记住下面一点:默认情况下,即使我们线程函数的参数是引用类型,参数会先被拷贝到线程空间,然后被线程执行体访问。上面的线程空间为线程能够访问的内部内存。我们来看下面的例子:

void f(int i,std::string const& s);
std::thread t(f,3,”hello”);

即使f的第二个参数是引用类型,字符串字面值"hello"还是被拷贝到线程t空间内,然后被转换为std::string类型。在上面这种情况下不会出错,但是在下面这种参数为指向自动变量的指针的情况下就很容易出错。

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();
}

在这种情况下,指针变量buffer将会被拷贝到线程t空间内,这个时候很可能函数oops结束了,buffer还没有被转换为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();
}

由于上面所说,进程传参时,参数都会被进行一次拷贝,所以即使我们将进程函数参数设为引用,也只是对这份拷贝的引用。我们对参数的操作并不会改变其传参之前的值。看下面例子:

void update_data_for_widget(widget_id w,widget_data& data);
void oops_again(widget_id w)
{
    widget_data data;
    std::thread t(update_data_for_widget,w,data);
    display_status();
    t.join();
    process_widget_data(data);
}

线程t执行完成之后,data的值并不会有所改变,process_widget_data(data)函数处理的就是一开始的值。我们需要显示的声明引用传参,使用std::ref包裹需要被引用传递的参数即可解决上面问题

void update_data_for_widget(widget_id w,widget_data& data);
void oops_again(widget_id w)
{
    widget_data data;
    std::thread t(update_data_for_widget,w,std::ref(data));
    display_status();
    t.join();
    process_widget_data(data);
}

对于可以移动不可拷贝的参数,譬如std::unqiue_ptr对象,如果源对象是临时的,移动操作是自动执行的;如果源对象是命名变量,必须显式调用std::move函数

void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object,std::move(p));

转移线程所有权Move

std::thread是可移动的,不可拷贝。在std::thread对象之间转移线程所有权使用sd::move函数。

void some_function();
void some_other_function();
std::thread t1(some_function);           // 1
std::thread t2=std::move(t1);            // 2
t1=std::thread(some_other_function);     // 3 临时对象会隐式调用std::move转移线程所有权
std::thread t3;                          // 4
t3=std::move(t2);                        // 5
t1=std::move(t3);                        // 6 赋值操作将使程序崩溃
t1.detach();
t1=std::move(t3);                        // 7 ok

这里需要注意的是临时对象会隐式调用std::move转移线程所有权,所以t1=std::thread(some_other_function);不需要显示调用std::move。如果需要析构thread对象,必须等待join()返回或者是detach(),同样,如果需要转移线程所有权,必须要等待接受线程对象的执行函数完成,不能通过赋一个新值给std::thread对象的方式来"丢弃"一个线程。第6点中,t1仍然和some_other_function联系再一次,所以不能直接转交t3的所有权给t1。

std::thread支持移动,就意味着线程的所有权可以在函数外进行转移。

std::thread f()
{
  void some_function();
  return std::thread(some_function);
}

std::thread g()
{
  void some_other_function(int);
  std::thread t(some_other_function,42);
  return t;
}

当所有权可以在函数内部传递,就允许std::thread实例可作为参数进行传递。

void f(std::thread t);
void g()
{
  void some_function();
  f(std::thread(some_function));
  std::thread t(some_function);
  f(std::move(t));
}

利用这个特性,我们可以实现线程对象的RAII封装。

class thread_guard
{
public:
    explicit thread_guard(std::thread &_t)
        : t(std::move(_t))
    {
        if (!t.joinable())
            throw std::logic_error("No Thread");
    }

    ~thread_guard()
    {
        if (t.joinable())
        {
            t.join();
        }
    }
    thread_guard(thread_guard const&) = delete;
    thread_guard& operator=(thread_guard const &) = delete;
private:
    std::thread t;
};
struct func;
void f() {
    int some_local_state;
    scoped_thread t(std::thread(func(some_local_state)));
    do_something_in_current_thread();
}

利用线程可以转移的特性我们可以用容器来集中管理线程,看下面代码:

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));
}

线程相关

线程数量

std::thread::hardware_concurrency()函数返回一个程序中能够同时并发的线程数量,在多核系统中,其一般是核心数量。但是这个函数仅仅是一个提示,当系统信息无法获取时,函数会返回0。

识别线程

线程标识类型是std::thread::id,可以通过两种方式进行检索。

  • 通过调用std::thread对象的成员函数get_id()来直接获取。
  • 当前线程中调用std::this_thread::get_id()也可以获得线程标识。

上面的方案和线程sleep很相似,使用上面一样的格式,get_id()函数替换成sleep()函数即可。
std::thread::id对象可以自由的拷贝和对比:

  • 如果两个对象的std::thread::id相等,那它们就是同一个线程,或者都“没有线程”。
  • 如果不等,那么就代表了两个不同线程,或者一个有线程,另一没有。

std::thread::id实例常用作检测特定线程是否需要进行一些操作,这常常用在某些线程需要执行特殊操作的场景,我们必须先要找出这些线程。


引用来源:

cppreference.com

C++并发编程1 - 让我们开始管理多线程

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值