学习TPL(一)

    这周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才会添加更多的线程来执行),这也就是整体上趋向于顺序的一个重要原因。

你可能感兴趣的:(学习TPL(一))