这周vs2010发布了,不少文章都在Show那些vs2010的新体验,这里我也凑个热闹,也来写写。
什么是TPL
TPL是Task Parallel Library的简称,也就是Framework 4.0中新加入的类库之一,这个类库里面最著名的要算是PLinq了(说到PLinq,大家一定瞬间就知道了吧)。但是PLinq只是TPL把其中最常用的内容使用Linq兼容的语法提供给大家,方便使用,所以还是有很多TPL的高级功能是无法用PLinq来实现的,这也就是学习TPL的重要原因之一。
简单的例子
要说TPL这个超级复杂的东西,还是从例子开始一点一点深入吧,否则,看完了都不知道在说啥。
现在假设要计算1-20的阶乘(正好在long的范围内,省去大数处理),并且希望在每一计算完成时写出一行“x计算完成”,最后再按照1-20的顺序一起输出计算结果。
因此,这里分两个阶段,第一阶段是计算+输出计算完成,第二阶段是按顺序输出计算结果。
同步实现
如果用同步实现的话,代码将会是类似这样:
long[] results = new long[20];
for (int i = 0; i < 20; i++)
{
long x = 1;
for (int j = 1; j <= i; j++)
{
x *= j;
}
results[i] = x;
Console.WriteLine(i + "计算完成");
}
for (int i = 0; i < 20; i++)
{
Console.WriteLine(results[i]);
}
很简单对吧,如果改用Linq的话,就会变成这样:
foreach (var result in (from i in Enumerable.Range(0, 20)
select new Func<long>(() =>
{
long x = 1;
for (int j = 1; j <= i; j++)
{
x *= j;
}
Console.WriteLine(i + "计算完成");
return x;
})()).ToList())
{
Console.WriteLine(result);
}
当然可以更进一步把Linq写的更优雅一些,不过这些不怎么好看的Linq代码并不影响接下来的试验。
来看看结果:
0计算完成
1计算完成
2计算完成
3计算完成
4计算完成
5计算完成
6计算完成
7计算完成
8计算完成
9计算完成
10计算完成
11计算完成
12计算完成
13计算完成
14计算完成
15计算完成
16计算完成
17计算完成
18计算完成
19计算完成
1
1
2
6
24
120
720
5040
40320
362880
3628800
39916800
479001600
6227020800
87178291200
1307674368000
20922789888000
355687428096000
6402373705728000
121645100408832000
一个非常顺序的执行结果,没什么需要细说的。
PLinq的实现
有了上面的Linq实现,我们可以很方便的翻译成PLinq的实现:
foreach (var result in (from i in ParallelEnumerable.Range(0, 20)
select new Func<long>(() =>
{
long x = 1;
for (int j = 1; j <= i; j++)
{
x *= j;
}
Console.WriteLine(i + "计算完成");
return x;
})()).ToList())
{
Console.WriteLine(result);
}
发现区别了吗?仅仅是把Enumerable.Range替换成了ParallelEnumerable.Range,
在来看看运行结果:
0计算完成
10计算完成
11计算完成
12计算完成
13计算完成
14计算完成
15计算完成
16计算完成
17计算完成
18计算完成
19计算完成
1计算完成
2计算完成
3计算完成
4计算完成
5计算完成
6计算完成
7计算完成
8计算完成
9计算完成
1
1
2
6
24
120
720
5040
40320
362880
3628800
39916800
479001600
6227020800
87178291200
1307674368000
20922789888000
355687428096000
6402373705728000
121645100408832000
可以发现计算过程中是乱序的(虽然也不是完全乱序),但是输出是顺序的。
还记得AsParallel这个扩展方法吗?不妨用那个来实现一个看看:
foreach (var result in (from i in Enumerable.Range(0, 20).AsParallel()
select new Func<long>(() =>
{
long x = 1;
for (int j = 1; j <= i; j++)
{
x *= j;
}
Console.WriteLine(i + "计算完成");
return x;
})()).ToList())
{
Console.WriteLine(result);
}
感觉有什么不同?看起来好像差不多,其实哪,运行了才知道:
0计算完成
2计算完成
3计算完成
4计算完成
1计算完成
6计算完成
7计算完成
8计算完成
9计算完成
10计算完成
5计算完成
12计算完成
13计算完成
14计算完成
15计算完成
16计算完成
17计算完成
18计算完成
19计算完成
11计算完成
1
720
5040
40320
362880
3628800
39916800
1
2
6
24
120
479001600
6227020800
87178291200
1307674368000
20922789888000
355687428096000
6402373705728000
121645100408832000
计算是乱序的,但是输出也是乱序的,这个有点偏离目标了,所以需要再次修正一下:
foreach (var result in (from i in Enumerable.Range(0, 20).AsParallel().AsOrdered()
select new Func<long>(() =>
{
long x = 1;
for (int j = 1; j <= i; j++)
{
x *= j;
}
Console.WriteLine(i + "计算完成");
return x;
})()).ToList())
{
Console.WriteLine(result);
}
这次在AsParallel之后再加了一个AsOrdered,我们要并发也要顺序,听起来挺绕口的,看看执行结果:
0计算完成
1计算完成
2计算完成
3计算完成
4计算完成
5计算完成
6计算完成
7计算完成
8计算完成
10计算完成
11计算完成
12计算完成
13计算完成
14计算完成
15计算完成
16计算完成
17计算完成
18计算完成
19计算完成
9计算完成
1
1
2
6
24
120
720
5040
40320
362880
3628800
39916800
479001600
6227020800
87178291200
1307674368000
20922789888000
355687428096000
6402373705728000
121645100408832000
可以看到执行过程中是有乱序的(注意9),但是总体上是趋向于顺序的,不过重要的是输出确实是顺序的了。
TPL的基本运用
文章一开始就说了,PLinq仅仅是把TPL中最常用的部分封装成了Linq的语法,但是还有不少高级的功能,其中就不缺乏例子中要求的2阶段处理。
不过,需要引入几个概念:
第一个是Task,在TPL里面Task是最核心的一个部分(要不然怎么能叫Task Parallel Library哪),Task用于包装了一段运算,使它在TPL中成为一个不可分割的单元,也就是TPL的执行器是以Task为基本单位来分配执行的。
第二个是TaskFactory,看名字就知道是干什么的了。。。
太抽象了?确实抽象了点,看看怎么用Task和TaskFactory来实现吧:
TaskFactory tf = new TaskFactory();
var t = tf.ContinueWhenAll(
(from i in Enumerable.Range(0, 20)
select tf.StartNew(() =>
{
long x = 1;
for (int j = 1; j <= i; j++)
{
x *= j;
}
Console.WriteLine(i + "计算完成");
return x;
})).ToArray(),
tasks =>
{
foreach (var task in tasks)
Console.WriteLine(task.Result);
});
t.Wait();
这里可以看到有2类Task,第一类Task是用TaskFactory的StartNew方法创建的,是用于计算的Task,第二类Task是TaskFactory用ContinueWhenAll方法创建的,用于输出结果。
如果需要的话,使用Task的Wait方法等待Task执行完成(见代码的最后一行)。
不难发现创建第一批任务的时候,直接用了一个标准的Linq,其实这里只是创建任务,所以并不会真正执行里面的计算,所以看一下输出结果:
0计算完成
1计算完成
2计算完成
3计算完成
4计算完成
6计算完成
7计算完成
8计算完成
9计算完成
10计算完成
11计算完成
12计算完成
13计算完成
14计算完成
15计算完成
16计算完成
17计算完成
18计算完成
19计算完成
5计算完成
1
1
2
6
24
120
720
5040
40320
362880
3628800
39916800
479001600
6227020800
87178291200
1307674368000
20922789888000
355687428096000
6402373705728000
121645100408832000
发现计算部分确实不是顺序的,但是有个问题,为什么和AsOrder的结果类似,总体还是趋向于顺序的哪?
这还是因为计算量太小,所以,在数据离散方面表现出一定的不足,所以将计算部分的代码更换成:
long x = 1;
for (int y = 0; y < 10000000; y++)
{
x = 1;
for (int j = 1; j <= 19 - i; j++)
{
x *= j;
}
}
Console.WriteLine(i + "计算完成");
return x;
这样,就提高了计算量,并且计算量是随着i的增加而减少的。
重新运行发现计算顺序为:
0计算完成
1计算完成
3计算完成
2计算完成
5计算完成
4计算完成
6计算完成
7计算完成
8计算完成
11计算完成
10计算完成
9计算完成
12计算完成
15计算完成
13计算完成
16计算完成
17计算完成
19计算完成
14计算完成
18计算完成
并且发现,TPL实际上是用两个线程跑的(因为本机是双核,对于纯计算工作而言,多余的线程并不能帮助我们的程序跑的更快,反而会因为来回切换线程上下文损失一些性能),所以那些任务仅仅是在排队,等待之前的任务结束(除非之前的任务出现了阻塞,TPL才会添加更多的线程来执行),这也就是整体上趋向于顺序的一个重要原因。