单核处理器单靠提高频率来提升性能变得越来越困难,多核处理器已经是大势所趋。目前绝大部分新买的电脑都已经是双核的了,今后还会有更多的核心。
    与硬件的多核形成对比的是,软件开发领域,还没有成熟的技术让我们方便的使用多核所带来的性能提升,大多数的执行过程仍然是单线程的,因为写多线程的应用很困难,除非涉及到大量的并发IO,否则一般不会使用多线程(至少我是这样,因为我..懒)。
    值得欣喜的是,微软的一批牛人也在为此努力的工作,旨在开发一个并行库,使得更加容易的编写能够自动使用多核处理器的托管代码,这就是TPL(The Task Parallel Library)。微软于12月5日发布了其CTP版本,可以从这里下面的链接下载,里面还有文档和示例。
http://www.microsoft.com/downloads/details.aspx?FamilyID=e848dc1d-5be3-4941-8705-024bc7f180ba&displaylang=en
    迫不及待的把它Down了下来,装上,然后开始试用。
    默认安装目录为C:\Program Files\Microsoft Parallel Extensions Dec07 CTP,在目录下有一个DLL文件System.Threading.dll,这就我们期待的TPL,还有两个文件夹,一个是文档,一个是示例。
    System.Threading.dll中有一个System.Threading.Parallel类,有四个用于并行计算的方法:For,For<>,ForEach<>,Do
    首先,我们设计一个计算:
.NET 并行扩展(ParallelFX) 试用(上)        private static void Normal()
        }

    我们使用Parallel.For<>,来改造这个计算。For<>方法有四个重载,最复杂的一个是:
.NET 并行扩展(ParallelFX) 试用(上)        public static void For<TLocal>(int fromInclusive, int toExclusive, int step, Func<TLocal> threadLocalSelector, Action<int, ParallelState<TLocal>> body, Action<TLocal> threadLocalCleanup);
    fromInclusive:起始值
    toExclusive:结束值
    step:步长 
    threadLocalSelector:线程初始化方法, 
    body:每一步要执行的任务,
    threadLocalCleanup:线程结束后要执行的方法。

    其他的重载可以看作是对某些参数使用默认值后对这个函数的调用,我们用的便是其默认步长为1的重载,改造后的计算:
.NET 并行扩展(ParallelFX) 试用(上)        private static void ParallelCalculate()
        }

来做个性能测试:(环境为:Vista™ Enterprise,Pentium D 双核 3.4G,1G 内存)
.NET 并行扩展(ParallelFX) 试用(上)            DateTime startTime = DateTime.Now;
.NET 并行扩展(ParallelFX) 试用(上)            Normal();
.NET 并行扩展(ParallelFX) 试用(上)            Console.WriteLine(DateTime.Now 
- startTime);
.NET 并行扩展(ParallelFX) 试用(上)
.NET 并行扩展(ParallelFX) 试用(上)            startTime 
= DateTime.Now;
.NET 并行扩展(ParallelFX) 试用(上)            ParallelCalculate();
.NET 并行扩展(ParallelFX) 试用(上)            Console.WriteLine(DateTime.Now 
- startTime);

从测试结果可以看到。性能提升还是很明显的:

    result=114917.508227442
    00:00:06.1363260
    result=114917.508226942
    00:00:03.5691075

    注意到,两次计算的结果并不完全一样,而是有一定的误差,考虑到计算两个double类型值在相加时,由于精度的限制,有可能对末位进行四舍五入,从而不同的计算顺序必然造成结果的不同,从这个角度上来说,上面的两个计算结果都正确,只是其误差已经使得他们的末尾几位数已经没有了意义。
    

    我们来做个实验,首先抛出异常
.NET 并行扩展(ParallelFX) 试用(上)        private static void ExceptionParallelCalculate()
        }

    运行结果为:

    result=122427.621705062
    computedCount=999992---99.9992%

    
    虽然在运算进行到10%左右的时候就抛出了一个异常,但是最终结果是它几乎完成了所有的运算。因为抛出异常的那个线程(就是计算数字100000的那个线程)被分配到的任务只占总任务的很少一部分(每次运行结果可能会不同),抛出异常时,它立刻终止当前任务,执行threadLocalCleanup,并抛弃任务队列里的剩余任务(被抛弃的应该是7个数字的计算任务,加上抛出异常的一个,一共8个)。其他的未分配任务会被其他的线程执行完毕。
    上例中如果把 throw new InvalidOperationException() 换成 ps.Stop() 那么所有线程都会立即终止,运行结果为:
    
    result=12594.4311232184
    computedCount=100000---10%

    
    这次我们尝试阻塞其中的一部分线程:
        }

    运算结果为:
    result=114917.508241467
    ThreadCount=15
    computedCount=1000000---100%
    
    由于某些线程被阻塞,生成了新的线程。

    如果我们在其中抛出一个异常:
.NET 并行扩展(ParallelFX) 试用(上)private static void ExceptionJoinParallelCalculate()
        }

    运行结果:
    result=76017.275196288
    ThreadCount=2
    computedCount=999992---99.9992%

    显然,这次因为之前曾有线程抛出异常,即使线程被阻塞了,也没有生成新的线程。

    来个小结:
    1。这个方法并不会对同步执行的线程,也不保证每个任务是按顺序执行的。
    2。线程任务的分配并不是一次完成的,而是一次只分配很少的任务,任务完成后会再分配其他任务,所以每个线程都有一个任务序列,而且都很短。
    3。线程数也是动态分配的,默认情况下,会生成与CPU核心相同数量的线程,但数如果某个线程被阻塞,就会产生新的线程,最终尽量保证正在执行的线程与CPU核心数量相同,每个核心都被充分利用。
    4。如果某个线程抛出了异常,在这之后无论在何种情况下,都不会再生成新的线程,但不会终止已生成的线程。
    5。抛出异常的线程仍然会执行threadLocalCleanup,但是会抛弃其任务队列里未被执行的任务序列。
    5。它会保存任一个线程抛出的异常,并在所有的线程都终止的时候重新抛出,一个线程的异常并不会影响其他线程的执行。
    6。如果要,终止所有的线程,不要使用异常,使用ParallelState的Stop()方法。

相关文章:

  • 2021-07-16
  • 2021-12-14
  • 2022-12-23
  • 2022-12-23
  • 2021-12-19
  • 2022-01-29
  • 2021-06-16
猜你喜欢
  • 2021-09-18
  • 2021-07-22
  • 2022-12-23
  • 2021-11-29
  • 2021-12-02
  • 2022-12-23
  • 2022-02-11
相关资源
相似解决方案