用.NET展現多核威力(3) – 佛心TPL之Parallel.For好威

前一篇文章裡,我們驗證了為每個CPU Core開一條獨立Thread並事先分攤好計算工作,可以讓巨量Log10計算程式飆出最高效能! 但是,仔細看看程式碼:

int WORKER_COUNT = 2;
Thread[] workers = new Thread[WORKER_COUNT];
int jobsCountPerWorker = MAX_COUNT / WORKER_COUNT;
for (int i = 0; i < WORKER_COUNT; i++)
{
    int st = jobsCountPerWorker * i;
    int ed = jobsCountPerWorker * (i + 1);
    if (ed > MAX_COUNT) ed = MAX_COUNT;
    workers[i] = new Thread(() =>
    {
        for (int j = st; j < ed; j++)
        {
            double d = Math.Log10(Convert.ToDouble(j));
        }
    });
    workers[i].Start();
}
for (int i = 0; i < WORKER_COUNT; i++)
    workers[i].Join();

我們寫了近20行的程式碼,而且還得花腦筋寫邏輯分割工作給多條Thread,要曉得如何用Thread.Join同步完成時間。說實在話,沒有三兩三,恐怕沒膽玩。

如果我說有一種很簡單的新寫法可以實現類似的效果:

Parallel.For(0, MAX_COUNT, j =>
{
    double d = Math.Log10(Convert.ToDouble(j));
});

看到這裡,大家會不會有想起立鼔掌的衝動?

這是.NET Framework 4.0裡內建的Task Parallel Library(TPL),一組幫助程式新手的佛心API。把原本複雜的多執行緒運算程式簡化成一行打死,寫程式的人就算對Thread.Join、lock()、ManualResetEvent一無所悉,照樣可以寫出漂亮的平行運算程式。(這下子,程式老鳥又有一項優勢被剝奪了,被菜鳥幹掉的日子愈來愈近了,我好怕...)

依我個人的理解,TPL強調的是平行運算的能力,目的在搾乾每一滴CPU運算能力。它在概念上介於ThreadPool與自行管理數條Thread之間,最精彩的地方是會依CPU的負載狀況自動調節Thread數,直到所有CPU使用率都飆上100%,系統運算能力完全被榨乾為止。換句話說,一開始迴圈只啟動一條Thread,接著會在資源允許的前題下增加平行處理的Thread數(the loop starts with a degree of 1, and may work its way up to any maximum that’s specified as resources become available,參考)。

接著,我們就讓原來寫法跟Parallel.For車拼一下(測試平台為E6400雙核CPU): (註: 要體驗.NET 4.0,請先下載VS2010 Beta回家玩)

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
 
namespace MultiCore
{
    class TestParalleFor
    {
        static void Main(string[] args)
        {
            int MAX_COUNT = 5000 * 10000;
            Stopwatch sw = new Stopwatch();
            for (int round = 0; round < 3; round++)
            {
                sw.Reset();
                sw.Start();
                int WORKER_COUNT = 2;
                Thread[] workers = new Thread[WORKER_COUNT];
                int jobsCountPerWorker = MAX_COUNT / WORKER_COUNT;
                for (int i = 0; i < WORKER_COUNT; i++)
                {
                    int st = jobsCountPerWorker * i;
                    int ed = jobsCountPerWorker * (i + 1);
                    if (ed > MAX_COUNT) ed = MAX_COUNT;
                    workers[i] = new Thread(() =>
                    {
                        for (int j = st; j < ed; j++)
                        {
                            double d = Math.Log10(Convert.ToDouble(j));
                        }
                    });
                    workers[i].Start();
                }
                for (int i = 0; i < WORKER_COUNT; i++)
                    workers[i].Join();
                sw.Stop();
                Console.WriteLine("Multi-Thread[{1}] = {0:N0}ms", 
                    sw.ElapsedMilliseconds, WORKER_COUNT);
 
                sw.Reset();
                sw.Start();
                Parallel.For(0, MAX_COUNT, j =>
                {
                    double d = Math.Log10(Convert.ToDouble(j));
                });
                sw.Stop();
                Console.WriteLine("Parallel.For = {0:N0}ms",
                    sw.ElapsedMilliseconds);
            }
            Console.Read();
        }
    }
}

Multi-Thread[2] = 2,003ms
Parallel.For = 2,119ms
Multi-Thread[2] = 2,098ms
Parallel.For = 2,283ms
Multi-Thread[2] = 1,966ms
Parallel.For = 2,113ms

就數據而言,二者相近,但在這個例子中,大部分的時間Parallel.For還是略輸一籌。理由是Parallel.For強調的是動態調節,由一條Thread開始,再逐步增加,自然會比事先規劃好全程用兩條Thread衝刺慢一些。不過別沮喪,我們動個手腳,馬上就能還Parallel.For一個公道。

我們將:

double d = Math.Log10(Convert.ToDouble(j));

改成每5000次Delay 10ms: (假裝在等待某項非CPU資源)

double d = Math.Log10(Convert.ToDouble(j));
if (j % 5000 == 0) Thread.Sleep(10);

並把執行次數改為100萬次縮短總執行時間。

Multi-Thread[2] = 1,046ms
Parallel.For = 652ms
Multi-Thread[2] = 1,009ms
Parallel.For = 502ms
Multi-Thread[2] = 1,007ms
Parallel.For = 550ms

怎樣? 前一個測試二者結果相近,加入Thread.Sleep後比原來的一核一緒寫法快了近一倍,程式簡潔N倍,在這兩回合比試中,我認定Parallel.For明顯勝出!!

因為Thread.Sleep的加入會降低CPU使用率,Parallel.For動態增加Thead的能力便可派上用場,填補了CPU空檔,也就縮短了總執行時間。在實務上,即便是以運算為主的工作,還是免不了有等待I/O、等待其他Thread就緒的同步需求,必須暫停等待其他非CPU資源後再繼續,等待期間會產生CPU使用率下降的情況。平行運算哲學中,讓CPU閒著是一種罪惡,在CPU使用率未達100%時增加Thread數充分利用閒置的運算能力,自然會比一核一緒更上一層樓。要自己寫出視CPU使用率動態調節Thread數的程式並非易事,而Parallel.For可以幫我們做到這一點,很威吧? 大家如果在.NET 4.0中開發類似的平行運算需求時,千萬不要錯過它囉~~

[2010-02-05補充] 若想在.NET 3.5中使用 Parallel.For(),可以參考這篇文章

歡迎推文分享:
Published 22 January 2010 06:06 AM 由 Jeffrey
Filed under: ,
Views: 42,255



意見

# Fillano said on 21 January, 2010 07:12 PM

Parallel.For的用法感覺有點像OpenMP...

# Lan said on 22 January, 2010 12:02 AM

印象中N年前學平行程式設計時,Parallel Pascal就有類似的語法了

不過根本不用擔心被菜鳥幹掉啊,因為老鳥才曉得這些強大工具背後其實是包裝掉哪些事,才有辦法當茶包射手,菜鳥遇到問題只會:

1.裝死

2.問人

3.Google

# mis2000lab said on 22 January, 2010 02:38 AM

謝謝您,

又長了見識,學到新東西了

# Ark said on 22 January, 2010 03:33 AM

msdn.microsoft.com/.../system.threading.barrier%28VS.100%29.aspx

Barrier Class

Thread要達到同步不同步好像在4.0用這個比較便捷

# jain said on 22 January, 2010 06:08 AM

哇,身為菜鳥的我還蠻驚訝的,

可以如此簡化,

才摸vs2008沒多久,

又要摸vs2010。

# KENCHAO said on 04 February, 2010 10:30 PM

不知道有沒有組件可以讓3.5的去參考使用呢?,這真的很方便但是不是每個客戶都用到4.0

# Jeffrey said on 05 February, 2010 04:28 AM

to KENCHAO, 關於在.NET 3.5中用TPL的方法,可以參考新文blog2.darkthread.net/.../tpl-for-3-5.aspx

# 路人假 said on 19 July, 2010 08:05 AM

請問一下  那魔老手

遇到問題時是如何解決的ㄋ???

# C.J. said on 25 August, 2010 12:59 AM

偶然从google链接进来。

作者您也好威

# Glegoo said on 14 August, 2014 08:28 AM

Parallel.For果然是好東西,竟然還能動態調節,等我重寫一下我的程式看看

# Loops said on 23 December, 2014 02:47 AM

您好,我寫了一個程式測試Parallel For,發現如果再回圈一開始如果有多一行Console.WriteLine({0},i),Parallel.For就會比一般的For慢,但如果不要這一行,Parallel就比較快。要怎麼解釋這個現象呢?

# Loops said on 23 December, 2014 02:48 AM

程式碼如下:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading;

using System.Threading.Tasks;

using System.Diagnostics;

using System.Collections.Concurrent;

namespace ParallelTest

{

   class Program

   {

       static void Main(string[] args)

       {

           int totalNum = 20000;

           bool isParallelCalculate = false;

           ConcurrentStack<float> scores = new ConcurrentStack<float>();

           //float[] scores = new float[totalNum];

           Random randomNumber = new Random();

           Stopwatch timer = new Stopwatch();

           timer.Reset();

           timer.Start();

           if (isParallelCalculate)

           {

               Parallel.For(0, totalNum,

                   (i, state) =>

                   {

                       //Console.WriteLine("{0}", i);

                       scores.Push(2 * 2);

                       float sum = scores.Sum();

                       for (int rndCount = 0; rndCount < totalNum; rndCount++)

                       {

                           sum += rndCount;

                       }

                   });

               timer.Stop();

               Console.WriteLine("Parallel.For = {0:N0}ms", timer.ElapsedMilliseconds);

           }

           else

           {

               for (int i = 0; i < totalNum; i++)

               {

                   //Console.WriteLine("{0}", i);

                   scores.Push(2 * 2);

                   float sum = scores.Sum();

                   for (int rndCount = 0; rndCount < totalNum; rndCount++)

                   {

                       sum += rndCount;

                   }

               }

               timer.Stop();

               Console.WriteLine("Sequencial For = {0:N0}ms", timer.ElapsedMilliseconds);

           }

           Console.WriteLine("Press any key...");

           Console.ReadLine();

       }

   }

}

# Jeffrey said on 23 December, 2014 09:54 AM

to Loops, Console屬共用資源,初步猜想是多個Thread爭奪協調使用權產生的Overhead拖累,我再試試是否能用實例驗證。

# Jeffrey said on 26 December, 2014 07:30 PM

to Loops, 請參考新文:blog2.darkthread.net/post-2014-12-26-concurrency-visualizer.aspx

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<January 2010>
SunMonTueWedThuFriSat
272829303112
3456789
10111213141516
17181920212223
24252627282930
31123456
 
RSS
創用 CC 授權條款
【廣告】
twMVC
最新回應

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication