PDC 2010 Hejlsberg的演讲中我们看到了VB.NET、C#新的简化异步编程的方式(可以下载新的Async CTP体验)。之前的TPL(Task Parallel Library)简化了并行编程。工业语言的飞速发展大大改进、简化了开发人员的编程方式。不仅是微软平台的托管语言,非托管语言也始终保持着一定程度的进化,我想通过两三篇文章来讲述Visual C++ 2010并行编程相关的内容。在介绍Visual C++ 2010的ppl之前,先简单介绍两个比较重要的新语法特性:
1、auto关键字
Visual C++ 2010中的auto关键字不再是以前简单的自动变量的概念,它被用于简化变量原型编译时的自动化推导。
1 vector numVec; 2 3 for (vector::const_iterator iter = numVec.begin(); iter != numVec.end(); ++iter) { 4 5 }
vector的遍历器写法稍微有些长了,代码过于烦琐。现在使用auto关键字编写:
1 vector numVec; 2 3 for (auto iter = numVec.begin(); iter != numVec.end(); ++iter) { 4 5 }
auto关键字的用法和C# 3.0新增的var关键字相同,用于简化那些编译时可以确定类型的声明。
1 Int32[] numArray = { 1, 2, 3, 4, 5 }; 2 IOrderedEnumerable iter = numArray.Where(p => p > 0).OrderBy(p => p); 3 var iter2 = iter;
2、Lambda表达式
微软平台的托管语言都支持匿名函数,Lambda是为实现匿名函数提供了函数式的语法。
1 Int32[] numArray = { 1, 2, 3, 4, 5 }; 2 var iter = numArray.Where(p => p > 0).Max();
旧有的Visual C++对于编写STL sort函数自定义排序需要这么编写:
1 bool less2(const int& lhs, const int& rhs) { 2 return lhs < rhs; 3 } 4 5 vector numVec; 6 7 sort(numVec.begin(), numVec.end(), less2); 8 sort(numVec.begin(), numVec.end(), less<int>());
Visual C++ 2010:
1 vector numVec; 2 3 sort(numVec.begin(), numVec.end(), [&](int& lhs, int& rhs) { return lhs < rhs; });
对于一些不超过4、5行以上的代码,我个人更愿意使用匿名函数。Visual C++ 2010的ppl的定义被包含在ppl.h中。下面先看一段简单的代码:
1 #include <iostream> 2 3 using namespace std; 4 5 int sum = 0; 6 7 for (int i = 1; i < 101; ++i) 8 sum += i; 9 10 wcout << “Result=” << sum << endl;
这组操作如果基于ppl该如何编写:
1 #include <iostream> 2 #include <ppl.h> 3 4 using namespace std; 5 using namespace Concurrency; 6 7 int sum = 0; 8 9 parallel_for(1, 101, 1, [&](int n) { 10 sum += n; 11 }); 12 13 wcout << “Result=” << sum << endl;
ppl提供了parallel_for,它的处理方式和for循环语句一样,下面是parallel_for在ppl.h中的定义:
1 template 2 void parallel_for(_Index_type _First, _Index_type _Last, _Index_type _Step, const _Function& _Func)
_First为起始索引、_Last为结束索引、_Step为步长、_Func为被调用函数指针。我以前的文章提到过Visual C++ 2008开始就提供了for each的支持,对于遍历vector之类的STL容器可以用for each实现(Visual C++的for each在for和each之间是有空格的,和Visual C#不同):
1 vector numVec; 2 3 numVec.push_back(1); 4 5 for each (int i in numVec) { 6 wcout << i << endl; 7 }
同样的ppl也提供了for each版本的并行函数parallel_for_each:
1 #include <algorithm> 2 #include <iostream> 3 #include <ppl.h> 4 5 using namespace std; 6 using namespace Concurrency; 7 8 int sum = 0; 9 const int NUMBER_ARRAY_SIZE = 10; 10 int numArray[NUMBER_ARRAY_SIZE]; 11 12 generate(numArray, numArray + NUMBER_ARRAY_SIZE, rand); 13 14 parallel_for_each(numArray, numArray + NUMBER_ARRAY_SIZE, [&](int n) { 15 sum += n; 16 }); 17 18 wcout << “Result=” << sum << endl;
上面提到的parallel_for和parallel_for_each都是基于容器的遍历,ppl提供了parallel_invoke用于并行执行多个函数体。
1 #include “stdafx.h” 2 #include <algorithm> 3 #include <iostream> 4 #include <ppl.h> 5 6 using namespace std; 7 using namespace Concurrency; 8 9 void Func1() { 10 int sum = 0; 11 const int NUMBER_ARRAY_SIZE = 10; 12 int numArray[NUMBER_ARRAY_SIZE]; 13 14 generate(numArray, numArray + NUMBER_ARRAY_SIZE, rand); 15 16 for each (int n in numArray) { 17 sum += n; 18 } 19 20 wcout << “Result=” << sum << endl; 21 } 22 23 parallel_invoke(&Func1, &Func1);
parallel_invoke最多支持10个function的函数重载。超过10以上的function完全可以用parallel_for和parallel_for_each来实现调用。
ppl提供了一些容器类型和对象用于线程安全的操作。先简单介绍两个容器类型concurrent_vector和concurrent_queue。这两个容器的使用方式和普通的STL的vector和queue相同,但有一个非常重要的区别在于它们的存储空间不是连续的,比如原来你操作vector时可以通过指针加减操作进行数据访问:
1 vector<int> vecArray; 2 3 int val = *(&vecArray[0] + 5);
但是对于concurrent_vector、concurrent_queue使用这种访问方式它的结果是未定义的,可能访问成功,可能是已经分配使用的内存空间或者访问违例。
1、concurrent_vector(使用方式和STL的vector相同)
1 #include <iostream> 2 #include <ppl.h> 3 #include <concurrent_vector.h> 4 5 concurrent_vector<int> vecArray; 6 7 parallel_invoke( 8 [&vecArray] { 9 for (int i = 0; i < 50; ++i) 10 vecArray.push_back(rand()); 11 }, 12 [&vecArray] { 13 for (int i = 0; i < 100; ++i) 14 vecArray.push_back(rand()); 15 }); 16 17 for each (int n in vecArray) 18 wcout << n << endl;
2、concurrent_queue(STL的queue在访问元素时调用pop,而concurrent_queue则是try_pop)
1 #include <iostream> 2 #include <ppl.h> 3 #include <concurrent_queue.h> 4 5 concurrent_queue<int> queueArray; 6 7 parallel_invoke( 8 [&queueArray] { 9 for (int i = 0; i < 50; ++i) 10 queueArray.push(rand()); 11 }, 12 [&queueArray] { 13 for (int i = 0; i < 100; ++i) 14 queueArray.push(rand()); 15 }); 16 17 int val = 0; 18 19 while (!queueArray.empty() && queueArray.try_pop(val)) 20 wcout << val << endl;
ppl提供了一种可以合并操作的类型用于将不同线程间的本地存储值进行一定的操作,以往我们编写一个多线程或者并行操作计算总和的时候需要通过各种手段来确保操作值的更新,看一个最简单的例子:
1 volatile long sum2 = 0; 2 3 parallel_for(1, 101, [&sum2](int n) { 4 InterlockedExchangeAdd(&sum2, n); 5 }); 6 7 wcout << sum2 << endl;
再来看使用ppl新增的combinable类型完成同样的操作:
1 #include <iostream> 2 #include <ppl.h> 3 4 combinable sum3; 5 6 parallel_for(1, 101, [&sum3](int n) { 7 int& local = sum3.local(); 8 9 local += n; 10 }); 11 12 wcout << sum3.combine(plus()) << endl;
combinable的combine是一个模板函数用来定义函数指针,所以完全可以通过简单的lambda表达式定义自己的操作。默认提供了诸如:plus、multiplies、divides、minus操作。上面的例子最后一步的combine也可以这样写:
wcout << sum3.combine([&](int n1, int n2) { return n1 + n2; }) << endl;