2.3转移线程的所有权

转移线程的所有权

假设你想要编写一个函数,它创建一个在后台运行的线程,但是向调用函数回传新线程的所有权,而非等待其完成,又或者你想要反过来做,创建一个线程,并将所有权传递给要等待它完成的函数。在任意一种情况下,你都需要将所有权从一个地方转移到另一个地方。

这里就是std::thread支持移动的由来。正如在上一节所描述的,在C++标准库里许多拥有资源的类型,如std::ifstream 和 std::unique_ptr是可移动的(movable),而非可复制的(copyable),并且std::thread就是其中之一。这意味着一个特定执行线程的所有权可以在std::thread实例之间移动,如同接下来的例子。该示例展示了创建两个执行线程,以及在三个std::thread实例t1、t2和t3之间对那些线程的所有权进行转移。

void some_function();
void some_other_function();
std::thread t1(some_function); //❶
std::thread t2 = std::move(t1); //❷
t1 = std::thread(some_other_function); //❸
std::thread t3; //❹
t3 = std::move(t2); //❺
t1 = std::move(t3); //❻ 此赋值将终结程序

首先,启动一个新线程❶并与t1相关联。然后当t2构建完成时所有权被转移给t2,通过调用std::move()来显式地转移所有权❷。此刻,t1不再拥有相关联的执行线程,运行some_function的线程现在与t2相关联。

然后,启动一个新的线程并与一个临时的std::thread对象相关联❸。接下来将所有权转移到t1中,是不需要调用std::move()来显式移动所有权的,因为此处所有者是一个临时对象——从临时对象中进行移动是自动和隐式的。

t3是默认构造的❹,这意味着它的创建没有任何相关联的执行线程。当前与t2相关联的线程的所有权转移到t3❺,再次通过显式调用std::move(),因为t2是一个命名对象。在所有这些移动之后,t1与运行some_other_function 的线程相关联,t2没有相关联的线程,t3与运行some_function的线程相关联。

最后一次移动❻将运行 some_function的线程的所有权转回给t1。但是在这种情况下t1已经有了一个相关联的线程(运行着some_other_function),所以会调用std::terminate()来终止程序。这样做是为了与std::thread的析构函数保持一致。你必须在析构前显式地等待线程完成或是分离,这同样适用于赋值:你不能仅仅通过向管理一个线程的std::thread对象赋值一个新的值来“舍弃”一个线程。

std::thread支持移动意味着所有权可以很容易地从一个函数中被转移出,如清单2.5所示。

//清单2.5 从函数中返回std::thread
#include <thread>

void some_function()
{
    
    }

void some_other_function(int)
{
    
    }

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

int main()
{
    
    
    std::thread t1 = f();
    t1.join();
    std::thread t2 = g();
    t2.join();
}

同样地,如果要把所有权转移到函数中,它只能以值的形式接受std::thread的实例作为其中一个参数,如下所示。

void f(std::thread t);

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

std::thread支持移动的好处之一,就是你可以建立在清单2.3中thread_guard 类的基础上,同时使它实际上获得线程的所有权。这可以避免thread_guard 对象在引用它的线程结束后继续存在所造成的不良影响,同时也意味着一旦所有权转移到了该对象,那么其他对象都不可以结合或分离该线程。因为这主要是为了确保在退出一个作用城之前线程都已完成,我把这个类称为scoped_thread。其实现如清单2.6所示,同时附带一个简单的示例。

#include <thread>
#include <utility>

class scoped_thread
{
    
    
    std::thread t;
public:
    explicit scoped_thread(std::thread t_): //❶
        t(std::move(t_))
    {
    
    
        if(!t.joinable()) //❷
            throw std::logic_error("No thread");
    }
    ~scoped_thread()
    {
    
    
        t.join(); //❸
    }
    scoped_thread(scoped_thread const&)=delete;
    scoped_thread& operator=(scoped_thread const&)=delete;
};

void do_something(int& i)
{
    
    
    ++i;
}

struct func
{
    
    
    int& i;

    func(int& i_):i(i_){
    
    }

    void operator()()
    {
    
    
        for(unsigned j=0;j<1000000;++j)
        {
    
    
            do_something(i);
        }
    }
};

void do_something_in_current_thread()
{
    
    }

void f()
{
    
    
    int some_local_state;
    scoped_thread t(std::thread(func(some_local_state))); //❹
        
    do_something_in_current_thread();
} //❺

int main()
{
    
    
    f();
}

这个例子与清单2.3类似,但是新线程被直接传递到scoped_thread❹,而不是为它创建一个单独的命名变量。当初始线程到达 f❺ 的结尾时,scoped_thread对象被销毁,然后结合❸提供给构造函数❶的线程。使用清单2.3中的thread_guard类,析构函数必须检查线程是不是仍然可结合,你可以在构造函数中❷来做,如果不是则引发异常。

std::thread对移动的支持同样考虑了std::thread对象的容器,如果那些容器是移动感知的(如更新后的std::vector<>)。这意味着你可以编写像清单2.7中的代码,生成一批线程,然后等待它们完成。

#include <vector>
#include <thread>
#include <algorithm>
#include <functional>

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)); //轮流在每个线程上调用join()
}

int main()
{
    
    
    f();
}

如果线程是被用来细分某种算法的工作,这往往正是所需的。在返回调用者之前,所有线程必须全都完成。当然,清单2.7的简单结构意味着由线程所做的工作是自包含的,同时它们操作的结果纯粹是共享数据的副作用。如果f()向调用者返回一个依赖于这些线程的操作结果的值,那么正如所写的这样,该返回值就得通过检查线程终止后的共享数据来决定。

将std::thread对象放到std::vector中是线程迈向自动管理的一步。与其为那些线程创建独立的变量并直接与之结合,不如将它们视为群组。你可以进一步创建在运行时确定的动态数量的线程,更进一步地利用这一步,而不是如清单2.7中的那样创建固定的数量。

猜你喜欢

转载自blog.csdn.net/qq_36314864/article/details/132076648
今日推荐