如何在Cilk Plus中组织非线程安全资源池(每个worker一个资源)

How to organize a pool of non thread-safe resources in Cilk Plus (one resource per worker)?

本文关键字:每个 资源池 worker 资源 一个 安全 线程 Cilk Plus      更新时间:2023-10-16

我有一个串行代码,我想使用Cilk Plus并行化;主循环在不同的数据集上重复调用处理函数,因此迭代是相互独立的,除了使用非线程安全的资源,该资源被封装到由外部库提供的类(例如nts)中,该类接受文件名并对其进行I/O操作。

如果我使用OpenMP,我会创建一个资源池,其中包含与我拥有的线程一样多的资源,并根据线程ID访问这些资源:

std::vector<nts> nts_pool;
for (std::size_t i{0}; i < omp_get_num_threads(); ++i)
    nts_pool.push_back(nts{});
nts_pool[omp_get_thread_num()].do_stuff();  // from inside the task

使用Cilk Plus,我可以使用__cilkrts_get_nworkers()__cilkrts_get_worker_number() api,但从英特尔论坛上的多个帖子中,我收集到这被认为是解决问题的错误解决方案,正确的解决方案是使用holder超对象。

现在,持有人解决方案看起来确实不错,除了我真的只希望创建和工作线程一样多的视图。也就是说,对于3个工作线程,我希望有3个对象,而不是更多。理由是,正如我所说,该资源是由第三方库提供的,构建起来非常昂贵,并且我将不得不在之后处理结果文件,所以越少越好。

不幸的是,我已经发现,而不是每个工人做一个视图,并保持它直到同步,持有人以某种方式创建和销毁视图根据逻辑,我不理解,似乎没有办法影响这种行为。

是否有可能使持有人按照我想要的方式行事,如果不能,什么是习惯的Cilk Plus解决方案来解决我的问题?

这是我用来调查持有者的程序,注意它在一次运行期间在我的测试机器上创建了多达50个视图,这些视图似乎是随机分配和销毁的:

#include <iostream>
#include <atomic>
#include <cilk/cilk.h>
#include <cilk/holder.h>
#include <cilk/reducer_ostream.h>
#include <cilk/cilk_api.h>
cilk::reducer_ostream *hyper_cout;
class nts {
public:
    nts() : tag_{std::to_string(++id_)} {
        *hyper_cout << "NTS constructor: " << tag_ << std::endl;
    }
    ~nts() {
        *hyper_cout << "NTS destructor: " << tag_ << std::endl;
    }
    void print_tag() {
        *hyper_cout << "NTS tag: " << tag_ << std::endl;
    }
    static void is_lock_free() {
        *hyper_cout << "Atomic is lockfree: " << id_.is_lock_free() << std::endl;
    }
private:
    const std::string tag_;
    static std::atomic_size_t id_;
};
std::atomic_size_t nts::id_{0};
class nts_holder {
public:
    void print_tag() { nts_().print_tag(); }
private:
    cilk::holder<nts> nts_;
};
int main() {
    __cilkrts_set_param("nworkers", "4");
    cilk::reducer_ostream cout{std::cout};
    hyper_cout = &cout;
    *hyper_cout << "Workers: " <<  __cilkrts_get_nworkers() << std::endl;
    nts::is_lock_free();
    nts_holder ntsh;
    ntsh.print_tag();
    for (std::size_t i{0}; i < 1000; ++i) {
        cilk_spawn [&] () {
            ntsh.print_tag();
        } ();
    }
    cilk_sync;
    return 0;
}

您是正确的,持有人是解决这个特定问题的一个诱人但效率低下的解决方案。如果您的程序正确地使用每个工作线程一个槽的槽数组,那么在这种情况下使用__cilkrts_get_nworkers()__cilkrts_get_worker_number() api实际上没有任何问题。我们一般不鼓励使用它们;更倾向于编写不受工作人员数量影响的Cilk Plus代码,因为这样通常可以更好地扩展。然而,在某些情况下,包括这个例子,为每个worker创建一个插槽是最好的策略。