如何在 Cilk Plus 中组织非线程安全资源池(每个工作人员一个资源)?
How to organize a pool of non thread-safe resources in Cilk Plus (one resource per worker)?
我有一个串行代码,我想使用 Cilk Plus 对其进行并行处理;主循环在不同的数据集上重复调用一个处理函数,所以迭代是相互独立的,除了使用非线程安全的资源,它被封装到一个class(比如,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 代码,因为这样通常可以更好地扩展。但是,在某些情况下(包括本例),为每个工作人员创建一个插槽是最佳策略。
我有一个串行代码,我想使用 Cilk Plus 对其进行并行处理;主循环在不同的数据集上重复调用一个处理函数,所以迭代是相互独立的,除了使用非线程安全的资源,它被封装到一个class(比如,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 代码,因为这样通常可以更好地扩展。但是,在某些情况下(包括本例),为每个工作人员创建一个插槽是最佳策略。