繁体   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