C++ 并发编程笔记(二)线程所有权的转移、数量选择与标识

线程所有权的转移、最优数量与标识

所有权的转移

赋值的形式

每一个 std::thread 对象都对应一个线程实体,并且对象是无法进行拷贝的,但是可以使用 std::move() 语义将 std::thread 对象以右值的形式转移到另一个对象进行覆盖,而被覆盖的对象的析构函数会被调用,其结果是它所对应的线程(如果有)将被终止运行。

C++ 标准库中很多资源占有 (resource-owning) 的类型,都是可移动但不可拷贝的。

void func1();
void func2();

std::thread t1(func1);
std::thread t2 = std::move(t1);
t1 = std::thread(func2);
std::thread t3;
t3 = std::move(t2);
t1 = std::move(t3);   // 这会造成运行在 func2 上的线程终止运行

函数的形式

  • 返回线程

    std::thread create_thread1() {
        void func1();
        return std::thread(func1);
    }
    
    std::thread create_thread2() {
        void func2();
        std::thread t(func2);
        return t;
    }
    
  • 将线程作为参数传递

    void foo(std::thread t);
    void bar() {
        void func();
        foo(std::thread(func));
        std::thread t(func);
        foo(std::move(t));
    }
    

可移动的好处

构造 thread_guard

可移动的 std::thread 使得我们可以避免一些引发线程安全问题的因素。比如,有了可移动的机制,我们就可以构建前一篇文章中提到的 thread_guard 类,以确保对象退出作用域时,线程不会因为对象的析构而在中途被迫退出。

比如如下所示的 scoped_thread 类。

class scoped_thread : boost::noncopyable {
    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();
    }
};

struct func; // 某个仿函数(为了简便就不给出定义了,其实前篇文章有类似的定义)

void f() {
    int local;
    scoped_thread t(std::thread(func(local)));
    do_something();
}

当新线程传递给 scoped_thread 时,会将该 std::thread 对象移动至 scoped_thread 对象内部,当 scoped_thread 对象析构时,就会调用 join() 连接至 std::thread 对象,以等待线程结束。

利用支持移动感知的容器

如果容器(比如 std::vector<>)是移动感知的(即容器本身知道如何妥善处理只可移动的对象),那么就可以像如下这样进行编码,以创建线程组方便统一管理。

void foo(size_t id);
void f() {
    std::vector<std::thread> threads;
    for (unsigned i = 0; i < 20; ++i)
        threads.emplace_back(foo, i); // 创建20个线程存于 vector 中
    
    for (auto & entry : threads) {
        entry.join(); // 对每个线程调用 join
    }
    // 当 vector 析构时,每个 thread 也会被析构
}

运行时选择线程数量

C++ 标准库提供了 std::thread::hardware_concurrency() 函数,它返回程序真正可以并发的线程数,一般是 CPU 的核数(或者硬件线程数),当系统信息无法获取时,这个函数会返回 0。

选择线程数量执行任务的示例,如下所示,一个并行版本的 std::accumulate 累加器。

template <typename Iterator, typename T>
struct accumulate_block {
    void operator()(Iterator first, Iterator last, T & result) {
        result = std::accumulate(first, last, result);
    }
};

template<typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init) {
    const size_t length = std::distance(first, last);
    if (!length)
        return init;
    const size_t min_per_thread = 25;
    const size_t max_period = (length + min_per_thread - 1) / min_per_thread;
    const size_t hardware_threads = std::thread::hardware_concurrency();
    const size_t num_threads = std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
    
    const size_t block_size = length / num_threads;
    
    std::vector<T> results(num_threads);
    std::vector<std::thread> threads(num_threads - 1);
    
    Iterator block_start = first;
    for (size_t i = 0; i < (num_threads - 1); ++i) {
        Iterator block_end = block_start;
        std::advance(block_end, block_size);
        threads[i] = std::thread(accumulate_block<Iterator, T>(), block_start, block_end, std::ref(results[i]));
        block_start = block_end;
    }
    accumulate_block<Iterator, T>()(block_start, last, results[num_threads - 1]);
    
    for (auto & entry : threads)
        entry.join();
    
    return std::accumulate(results.begin(), results.end(), init);
}


启动线程数要比 num_threads 少一个,因为主线程也算一个线程。

并行算法需要注意的问题:

  • 迭代器的兼容性区别(前向迭代器与输入迭代器)
  • 结合方式问题(float 和 double)

线程标识

线程标识的类型为 std::thread::id ,可以使用两种方式获取到。

  • std::thread 的成员函数 get_id() :如果该对象没有与任何线程实体相关联,那么该函数将返回默认构造的 std::thread::id 对象,即 “not any thread”。
  • 静态函数 std::this_thread::get_id() :直接获得当前线程的标识。

C++ 标准库保证线程 ID 比较的结果相等时,它们是同一个线程。这种标识可以用于一些复杂数据结构用于存储线程的键值或者索引,比如哈希表这种关联容器。通过记录线程的标识,也可以方便在线程中传递信息或者实现其他复杂的业务逻辑。

📅 更新时间:2021/10/30 Saturday 00:00

🖊️ 本文由 Alone Café 创作,如果您觉得本文让您有所收获,请随意赞赏 🥺
⚖️ 本文以 CC BY-NC-SA 4.0,即《署名-非商业性使用-相同方式共享 4.0 国际许可协议》进行许可
👨‍⚖️ 本站所发表的文章除注明转载或出处外,均为本站作者原创或翻译,转载前请务必署名并遵守上述协议
🔗 原文链接:https://alone.cafe/2021/10/cpp并发编程-2
📅 最后更新:2021年10月30日 Saturday 00:00

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×