zoukankan      html  css  js  c++  java
  • 学习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才会添加更多的线程来执行),这也就是整体上趋向于顺序的一个重要原因。

  • 相关阅读:
    [py]戏说python面向对象细节
    [py]彻底细究web框架的wsgi+逻辑处理模块
    [py]access日志入mysql-通过flask前端展示
    [sql]mysql管理手头手册,多对多sql逻辑
    [py]requests+json模块处理api数据,flask前台展示
    [py]flask从0到1-模板/增删改查
    [wx]雪落香杉树人物关系图
    [py]资源搜集
    [py]python之信用卡ATM
    【Unity技巧】开发技巧(技巧篇)
  • 原文地址:https://www.cnblogs.com/vwxyzh/p/1713622.html
Copyright © 2011-2022 走看看