簡體   English   中英

C#中的有效線程同步

[英]Effective thread Synchronization in C#

我有一個場景,我需要從許多二進制文件(使用鍵)搜索並結合結果(字符串)。 到目前為止,我一直在一個for循環的文件中執行它。

foreach (string file in FileSources.Keys)
{
    aggregatedDefinitions.Append(DefinitionLookup(txtSearchWord.Text, file));
}

由於此操作非常慢,我考慮使用線程,以便我可以並行執行IO操作。 穿線是正確的方法。 如果我使用線程,我怎樣才能確保按照我想要的順序得到結果。

到目前為止我還沒有使用線程。 如果您可以建議一些可以幫助我解決問題的材料/書籍,將會非常有幫助。

一般來說,當您在應用程序的主(通常是GUI)線程的單獨線程上執行一個 I / O操作時,建議對I / O操作使用線程。 在並行的單獨線程上拆分許多 I / O操作可能不會對您有所幫助,因為磁盤一次只能訪問一個。

考慮到其他人對單個磁盤設備嘗試並行I / O所表達的擔憂,看起來您的處理模型可能會被分解為多個部分。 您有一個Filesources.Keys輸入列表,輸出似乎只是將計算結果附加到aggregatedDefinitions。

以下是如何在多個線程上進行處理並保留當前結果的順序:

首先,確定要使用的線程數。 對於計算密集型任務,通常沒有必要啟動比擁有CPU內核更多的線程。 對於I / O綁定任務,您可以使用比CPU核心更多的線程,因為線程將花費大部分時間等待I / O完成。

假設您的DefinitionLookup是計算密集型的,而不是I / O密集型的,我們假設您在雙核CPU上運行。 在這些條件下,兩個線程將是一個不錯的選擇。

接下來,將輸入分解為較大的塊,保留輸入的順序。 對於我們的兩個線程場景,將FileSources.Keys列表的前半部分發送到第一個線程,將后半部分發送到第二個線程。

在每個線程中,像以前一樣處理輸入,但將輸出附加到本地列表對象,而不是最終(共享)aggregatedDefinitions列表。

線程完成處理后,讓主線程以正確的順序將每個線程的列表結果連接到最終的aggregatedDefinitions列表中。 (接收到輸入的前半部分的線程1產生list1,並且應該在輸出Thread2的結果之前附加到主列表中。

像這樣的東西:

    static void Mainthread()
    {
        List<string> input = new List<string>();  // fill with data

        int half = input.Count() / 2;
        ManualResetEvent event1 = new ManualResetEvent(false);
        List<string> results1 = null;

        // give the first half of the input to the first thread
        ThreadPool.QueueUserWorkItem(r => ComputeTask(input.GetRange(0, half), out results1, event1));

        ManualResetEvent event2 = new ManualResetEvent(false);
        List<string> results2 = null;

        // second half of input to the second thread
        ThreadPool.QueueUserWorkItem(r => ComputeTask(input.GetRange(half + 1, input.Count() - half), out results2, event2));

        // wait for both tasks to complete
        WaitHandle.WaitAll(new WaitHandle[] {event1, event2});

        // combine the results, preserving order.
        List<string> finalResults = new List<string>();
        finalResults.AddRange(results1);
        finalResults.AddRange(results2);
    }

    static void ComputeTask(List<string> input, out List<string> output, ManualResetEvent signal)
    {
        output = new List<string>();
        foreach (var item in input)
        {
            // do work here
            output.Add(item);
        }

        signal.Set();
    }

此外,即使所有I / O活動都在訪問一個磁盤驅動器,您也可以使用異步文件讀取獲得一些性能優勢。 我們的想法是,一旦從先前的文件讀取請求接收數據,就可以發出下一個文件讀取請求,處理先前讀取的數據,然后等待下一個文件讀取的完成。 這允許您在處理磁盤I / O請求時使用CPU進行處理,而無需自己明確使用線程。

比較這些(偽)執行時間線以讀取和處理4個數據塊。 假設文件讀取需要大約500個時間單位來完成,並且處理該數據大約需要10個時間單位。

Synchronous file I/O:  
read (500)
process data (10)
read (500)
process data (10)
read (500)
process data (10)
read (500)
process data (10)
Total time: 2040 time units

Async file I/O:
begin async read 1
async read 1 completed (500)
begin async read 2 / proces data 1 (10)
async read 2 completed (500)
begin async read 3 / proces data 2 (10)
async read 3 completed (500)
begin async read 4 / proces data 3 (10)
async read 4 completed (500)
process data 4 (10)
Total time: 2010 time units

數據1,2和3的處理發生在下一個讀取請求待處理期間,因此與第一個執行時間線相比,您可以獲得基本免費的處理時間。 最后一個數據塊的處理會增加總時間,因為它沒有與其並行運行的讀操作。

這些操作的規模(I / O為500,計算為10)是保守的。 與計算時間相比,實際I / O往往更大,比計算高出許多個數量級。 正如您所看到的,當計算操作非常快時,您無法從所有這些工作中獲得很多性能優勢。

如果您在“免費”時間內所做的事情充實,那么您可以從異步I / O的工作中獲得更大的價值。 例如,加密或圖像處理可能是一種勝利,但字符串連接可能不值得。 在異步重疊中將數據寫入另一個文件可能是值得的,但正如其他人已經注意到,如果所有I / O都在同一個物理設備上,那么好處將會減少。

我同意Dan的意見,而且Fredrik並加入其中 - 嘗試對單個磁盤進行多線程IO可能而不是改善性能會使事情變得更糟。

來自並行線程的訪問請求會增加磁盤抖動,這將使磁盤上的數據檢索速度比現在慢

如果您使用的是.NET 4.0,則可能需要查看Parallel Extensions和Parallel類。 我已經寫了一些關於如何在.NET 4.0中使用C#的例子

您可能還想查看F#中的 Parallel IO (Read Don Symes WebLog) 您需要IO Parallized的部分,您可能想用F#編寫。

檢查.Net 4.0中的內存映射文件,如果您使用C#3.5檢查該主題的pinvoke實現,它確實加快了應用程序的io操作和一般性能。 我有一個應用程序,它計算給定文件夾上的md5以查找重復項並使用內存映射文件進行文件訪問。 如果您需要示例源代碼和pinvoked內存映射庫,請與我聯系。

http://en.wikipedia.org/wiki/Memory-mapped_file或查看此處的實施http://www.pinvoke.net/default.aspx/kernel32.createfilemapping

它將真正加速您的io操作,而無需額外的線程開銷。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM