.NET中並行開發優化

讓咱們考慮一個簡單的編程挑戰:對大數組中的全部元素求和。如今能夠經過使用並行性來輕鬆優化這一點,特別是對於具備數千或數百萬個元素的巨大陣列,還有理由認爲,並行處理時間應該與常規時間除以CPU核心數同樣多。事實證實,這一壯舉並不容易實現。我將向您展現幾種並行執行此操做的方法,它們如何改善或下降性能以及以某種方式影響性能的全部細節。html

簡單的循環方法

private const int ITEMS = 500000;
private int[] arr = null;

public ArrayC()
{
    arr = new int[ITEMS];
    var rnd = new Random();
    for (int i = 0; i < ITEMS; i++)
    {
        arr[i] = rnd.Next(1000);
    }
}

public long ForLocalArr()
{
    long total = 0;
    for (int i = 0; i < ITEMS; i++)
    {
        total += int.Parse(arr[i].ToString());
    }

    return total;
}

public long ForeachLocalArr()
{
    long total = 0;
    foreach (var item in arr)
    {
        total += int.Parse(item.ToString());
    }

    return total;
}

只須要迭代循環就能夠計算出結果,超級簡單,這裏沒有用直接相加求出結果,緣由是直接求出結果,發現每次基本的運行都比並行快,可是實際上,並行處理沒有那麼簡單,因此這裏的加法就簡單的處理下total += int.Parse(arr[i].ToString())。如今,讓咱們嘗試用並行性來戰勝數組迭代吧。編程

首次嘗試

private object _lock = new object();

public long ThreadPoolWithLock()
{
    long total = 0;
    int threads = 8;
    var partSize = ITEMS / threads;
    Task[] tasks = new Task[threads];
    for (int iThread = 0; iThread < threads; iThread++)
    {
        var localThread = iThread;
        tasks[localThread] = Task.Run(() =>
        {
            for (int j = localThread * partSize; j < (localThread + 1) * partSize; j++)
            {
                lock (_lock)
                {
                    total += arr[j];
                }
            }
        });
    }

    Task.WaitAll(tasks);
    return total;
}

請注意,您必須使用localThread變量來「保存」該iThread時間點的值。不然,它將是一個隨着for循環前進而變化的捕獲變量。當數據最後打的時候並行已經比普通的快了,可是發現快的很少,說明還能夠優化數組

再次優化

public long ThreadPoolWithLock2()
{
    long total = 0;
    int threads = 8;
    var partSize = ITEMS / threads;
    Task[] tasks = new Task[threads];
    for (int iThread = 0; iThread < threads; iThread++)
    {
        var localThread = iThread;
        tasks[localThread] = Task.Run(() =>
        {
            long temp = 0;
            for (int j = localThread * partSize; j < (localThread + 1) * partSize; j++)
            {
                temp += int.Parse(arr[j].ToString());
            }

            lock (_lock)
            {
                total += temp;
            }
        });
    }

    Task.WaitAll(tasks);
    return total;
}

增長設置臨時變量,減小lock次數,發現運行效果已經有質的提升,提升了幾倍。突然想起,有個Parallel.For的方法,研究性能是否能夠更快。多線程

Parallel.For優化

public long ParallelForWithLock()
{
    long total = 0;
    int parts = 8;
    int partSize = ITEMS / parts;
    var parallel = Parallel.For(0, parts, new ParallelOptions(), (iter) =>
    {
        long temp = 0;
        for (int j = iter * partSize; j < (iter + 1) * partSize; j++)
        {
            temp += int.Parse(arr[j].ToString());
        }

        lock (_lock)
        {
            total += temp;
        }
    });
    return total;
}

運行結果比普通迭代快,可是沒有ThreadPool快,可是以爲Parallel.For還能夠繼續優化,也許能夠更快併發

Parallel.For繼續優化

public long ParallelForWithLock2()
{
    long total = 0;
    int parts = 8;
    int partSize = ITEMS / parts;
    var parallel = Parallel.For(0, parts,
        localInit: () => 0L, // Initializes the "localTotal"
        body: (iter, state, localTotal) =>
        {
            for (int j = iter * partSize; j < (iter + 1) * partSize; j++)
            {
                localTotal += int.Parse(arr[j].ToString());
            }

            return localTotal;
        },
        localFinally: (localTotal) => { total += localTotal; });
    return total;
}

運行效果已經很快,和ThreadPool優化過的差很少,有些時候更快dom

避免在循環中使用Task.Run

您能夠在要執行併發活動時使用任務,若是您須要高度的並行性,任務永遠不是一個好的選擇,始終建議避免在ASP.Net中使用線程池線程。所以,您應該避免在ASP.Net中使用Task.Run或Task.factory.StartNew。async

Task.Run應始終用於CPU綁定代碼。Task.Run在ASP.Net應用程序或利用ASP.Net運行時的應用程序中不是一個好選擇,由於它只是將工做卸載到ThreadPool線程。若是您使用的是ASP.Net Web API,則該請求已經使用了ThreadPool線程。所以,若是在ASP.Net Web API應用程序中使用Task.Run,​​則只是經過將工做卸載到另外一個工做線程來限制可伸縮性。性能

請注意,在循環中使用Task.Run存在缺點。若是在循環中使用Task.Run方法,則會建立多個任務 - 每一個工做單元或迭代一個任務。可是,若是使用Parallel.ForEach代替在循環中使用Task.Run,​​則會建立分區程序以免建立更多任務來執行活動而不是須要它。這可能會顯着提升性能,由於您能夠避免過多的上下文切換,而且仍然能夠利用系統中的多個內核。優化

應該注意的是,Parallel.ForEach在內部使用Partitioner <T>,以便將集合分發到工做項中。順便說一句,這種分發不會發生在項目列表中的每一個任務中,而是做爲批處理髮生。這下降了所涉及的開銷,從而提升了性能。換句話說,若是在循環中使用Task.Run或Task.Factory.StartNew,它們將爲循環中的每次迭代顯式建立新任務。Parallel.ForEach更有效,由於它將經過在系統中的多個核心之間分配工做負載來優化執行。spa

結論和總結

並行化優化確定能夠提升性能,可是這取決於不少因素,每一個案例都應該進行測量和檢查。
當各類線程須要經過某種鎖定機制相互依賴時,性能會顯着下降。

50萬數據運行結果

 

其餘的多線程文章

1. C#中await/async閒說

2. .NET中並行開發優化

3. C# Task.Run 和 Task.Factory.StartNew 區別

4. C#中多線程的並行處理

5. C#中多線程中變量研究

相關文章
相關標籤/搜索