使用Cilk™ Plus来对程序进行并行化比使用传统的Pthread方式来建立管理线程库容易得多,在一般情况下,利用关键字cilk_sync以及cilk_for可以使串行的程序更容易改写为并行的代码,尽管在一些复杂的并行情况下,使用cilk_sync以及cilk_for并不能解决程序中本身存在的数据竞态及多线程并行的协调管理问题。
值得注意的是Cilk™ Plus并不是只有关键字的方式,Cilk™ Plus库也包含一些用于解决并行程序中的竞态、锁、多线程协调等问题的功能features。本文将提到的是Cilk Reducer,它能有助于解决常见的累计型算法中存在的数据竞态及多线程间按序计算等问题。
1. 累计型算法常见于对一个变量进行多次叠加地更新值,比如以下代码:
#include <iostream> int main() { unsigned long accum = 0; for (int i = 0; i != 1000; i++) { //cilk_for accum += i*i; } std::cout << accum << "\n"; }
利用Cilk™ Plus对上文例程代码并行化的过程中常见的错误就是仅仅用cilk_for来代替for而无视对程序的正确性:例如本例中的accum += i*i;会出现多个线程同时更新此accum全局变量。
然而,如果按照传统的多线程编程的方式用加锁来对全局变量进行保护的话,又会引起很大的性能损失,Cilk reducer的引入就正好可以解决此全局变量的同时更新问题:
#include <iostream> #include <cilk/cilk.h> #include <cilk/reducer_opadd.h> int main() { cilk::reducer_opadd<unsigned long> accum(0); cilk_for (int i = 0; i != 1000; i++) { *accum += i*i; } std::cout << accum.get_value() << "\n"; }
对于原串行代码的改动只需:
- 将accum全局变量替换为“reducer” (
cilk::reducer_opadd<unsigned long>
); - 将reducer看做为指向真实的变量的指针(
*accum += a[i]
); - 当计算结束时,从reducer中返回最终的累加值(
accum.get_value()
).
2. 利用reducer来协调并行计算的顺序,在很多情况下此并行问题比上文的数据冲突的并行问题出现得更为典型:
#include <string> #include <iostream> #include <cilk/cilk.h> int main() { std::string alphabet; cilk_for(char letter = 'A'; letter <= 'Z'; ++letter) { alphabet += letter; } std::cout << alphabet << "\n"; }
上述代码中也出现了同时更新/Append同一字符串时的竞态问题,而且如果只是通过添加锁代码来期待保证按序append到string时,程序的结果同样是错误的,比如会输出KPFGLABCUHQRSVWXDMTYZMEIJO等乱序的结果。
尽管加锁可以保证每一次更新正确地发生且与其他的更新保持无关,但是锁并不能够保证全局地所有更新按正确次序发生。Cilk reducer的实现则保证了没有上述问题的出现,在并行计算时,使用Cilk reducer可以保证所有的输入值以同串行程序一致的顺序按序计算,如以下代码将全局变量改为Cilk reducer形式后:
#include <string> #include <iostream> #include <cilk/cilk.h> #include <cilk/reducer_string.h> int main() { cilk::reducer_string alphabet; cilk_for(char letter = 'A'; letter <= 'Z'; ++letter) { *alphabet += letter; } std::cout << alphabet.get_value() << "\n"; }
编译后运行可发现此并行程序的结果为ABCDEFGHIJKLMNOPQRSTUVWXYZ,与串行程序的结果保持一致。
3. Cilk reducer不仅仅适用于循环及常用的计算等例子,在下文的例子中,函数filter_tree()会被传参入一个二叉树和对应键值来被调用,此函数将在所有树的节点中找到键值相同的Key,并且返回含有对应节点的值域的链表。链表中的值将按照左子树、根节点、右子树的位置按序排列,filter_tree()函数中的filter_and_collect()用于递归遍历此二叉树从而构建此链表:
#include<cilk/cilk.h> #include <cilk/reducer_list.h> // 树的节点结构体定义 // template <typename Key, typename Value> struct TreeNode { TreeNode* left_subtree; TreeNode* right_subtreee; Key key; Value value; }; // worker函数,遍历子树且将匹配对应键的所有节点的值累加到一个reducer // template <typename Key, typename Value> void filter_and_collect(const TreeNode<Key, Value>* subtree, const Key& key, cilk::reducer_list_append<Value>& list) { if (!subtree) return; cilk_spawn filter_and_collect(subtree->left, key, list); if (subtree->key == key) { list->push_back(subtree->value); } filter_and_collect(subtree->right, key, list); } //主函数,匹配对应的输入键值Key // template <typename Key, typename Value> std::list<Value> filter_tree(const TreeNode<Key, Value>* tree, const Key& key) { cilk::reducer_list_append<Value> list; filter_and_collect(tree, key, list); return list.get_value(); }
4. 并行化程序时的要点总结:
- 将用于累计值的全局变量替换为reducer可以更高效地实现并行;
- 用cilk_for 或
cilk_spawn将循环或递归等并行化;
- 计算结束时,用reducer的get_value()方式取得累计后的共享变量的最终值,reducer的实现保证了不用考虑多线程数据竞态及多线程按序计算等问题,从而保证了与串行程序的结果一致。
关于更多Cilk™ Plus的规范或内部实现规定,可以访问https://software.intel.com/en-us/intel-cilk-plus.