.NET中並行開發優化


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

簡單的循環方法

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優化過的差不多,有些時候更快

避免在循環中使用Task.Run

您可以在要執行並發活動時使用任務,如果您需要高度的並行性,任務永遠不是一個好的選擇,始終建議避免在ASP.Net中使用線程池線程。因此,您應該避免在ASP.Net中使用Task.Run或Task.factory.StartNew。

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更有效,因為它將通過在系統中的多個核心之間分配工作負載來優化執行。

結論和總結

並行化優化肯定可以提高性能,但是這取決於很多因素,每個案例都應該進行測量和檢查。
當各種線程需要通過某種鎖定機制相互依賴時,性能會顯着降低。

50萬數據運行結果

 

其他的多線程文章

1. C#中await/async閑說

2. .NET中並行開發優化

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

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM