【问题标题】:Optimized synchronization of access to a shared resource共享资源访问的优化同步
【发布时间】:2021-02-05 04:14:04
【问题描述】:

对于我的控制台应用程序,我实现了一个简单的文件记录器。记录器使用StringBuilder 来保持追加日志条目并将LogText 数据写入最后的LogFile。这导致只有一个文件 I/O 操作。应用程序的执行必须非常快,因此,我已经实现了Parallel.ForEach 以及async-await,并尽可能减少了 I/O 操作。

问题是Logger 不是线程安全的。使用lockMonitorParallel.ForEach 循环内同步共享资源logger 会降低性能。有没有什么对执行速度影响不大的共享资源同步的最佳方式?

我愿意接受其他方法或建议。

Logger.cs

public class Logger
{
    private readonly string LogFile;
    private readonly StringBuilder LogText;

    public Logger(string logFile)
    {
        LogFile = logFile;
        LogText = new StringBuilder();
    }

    public void Write(string message)
    {
        LogText.AppendLine(message);
    }
    
    public void WriteToFile()
    {
        File.AppendAllText(LogFile, LogText.ToString());
    }
}

程序.cs

public class Program
{
    public static void Main(string[] args)
    {
        string logFile = args[0];
        string workingDirectory = args[1];
        Logger logger = new Logger(logFile);
        logger.Write($"INFO | Execution Started");

        try
        {
            List<string> files = Directory.EnumerateFiles(workingDirectory, "*", SearchOption.AllDirectories).ToList();
            Parallel.ForEach(files, async file =>
            {
                List<string> results = await PerformCPUBoundComputationAsync();
                foreach(string result in results)
                {
                    logger.Write($"INFO | Item: {result}");
                }
                string response = await MakePostRequestAsync(results);
                logger.Write($"INFO | Response: {response}");
            });
        }
        catch (Exception ex)
        {
            logger.Write($"ERROR | {ex.Message}");
        }
        finally
        {
            logger.Write($"INFO | Execution Ended");
            logger.WriteToFile();
        }
    }
}

【问题讨论】:

  • “我又做了一个东西,好吗?”问题应该去codereviewlock-free同步还有几十个问题,请先研究一下。
  • Hrm,为什么不直接使用一个已经解决了所有这些问题的日志框架,并且您将拥有更多呢?但是这段代码有一些非常可疑的部分,Parallel.ForEach(files, async file 您正在使用 Parallel.ForEach 来启动一堆异步 void,PerformCPUBoundComputation 是异步的,看起来有点奇怪。显然这根本不是线程安全的,但你知道
  • 日志是否需要按运行时间顺序排列?日志有点乱有关系吗?
  • MakePostRequest IO 绑定工作吗?你为什么不等这个?
  • Api 调用意味着 IO 绑定,即它通过您的网卡输出

标签: c# .net multithreading concurrency parallel-processing


【解决方案1】:

这不是使用您的 Logger 类,我只是想向您展示另一种方法

首先我们不将Parallel.ForEach 用于IO 绑定工作,它不适合,而且我们绝对不给它一个async lambda(这是一个未观察到的async void),它表示Parallel.ForEach 将在所有任务完成之前完成。

至于你的问题:

  1. 要解决已完成任务的问题,让我们使用WhenAll
  2. 为了解决线程安全问题,让我们为每个任务创建一个单独的字符串生成器。这有点分配,但它是无锁的。
  3. 让我们把所有的日志写在最后

async 和 await 模式会在完成 IO-bound 工作时将线程返回到线程池。任务调度程序会将这些线程用于 CPU 密集型工作。

var tasks = Directory
   .EnumerateFiles(workingDirectory, "*", SearchOption.AllDirectories)
   .Select(async x =>
   {
      var sb = new StringBuilder();
      List<string> results = // do cpu bound work, no need for fake async, 
      // thats to say no need to offload to another thread. 
      foreach (string result in results)
         sb.AppendLine($"INFO | Item: {result}");
      string response = await MakePostRequestAsync(results);
      sb.AppendLine($"INFO | Response: {response}");
      return sb;
   });

// await all your work to finish
var logs = await Task.WhenAll(tasks);

// write the results to the file
using var sw = new StreamWriter("FileName");
sw.WriteLine($"INFO | Execution Started");

foreach (var log in logs)
   sw.WriteLine(log);

sw.WriteLine($"INFO | Execution Ended");

注意:这可能会导致分配和内存压力,具体取决于日志的大小。在这种情况下,您可能需要返回到同步原语,并承担锁定的代价。

另一种有效的方法是使用诸如 Tpl Dataflow 管道之类的东西,进行计算,发布帖子,然后将结果批处理以进行写入,这可能会减少分配并具有处理同步和异步的优势工作量。

Processing (parallel)
       v
Posting (parallel)
       v
Batched Log Writes (Singular)

【讨论】:

  • @RonakThakkar Select(async x =&gt; 表示您正在启动新的热任务(当调度器认为合适时,线程池线程)以异步方式进行处理
  • @RonakThakkar 是的,那也很好,但是如果您对lock 的性能有问题,那么任何并发收集都会有性能问题
  • @RonakThakkar 计算哈希并不是特别占用 CPU 资源。该程序花费更多时间等待来自磁盘的数据,而不是处理这些数据。您应该能够通过在程序运行时观察 CPU 利用率来遵守这一点。我的 4 核 PC 显示不到 10% 的 CPU 负载,同时散列一个大文件。您应该避免多次读取每个文件,每种类型的哈希一次。可以形成CryptoStreams的管道,每个文件读取一次。
  • TheGeneral 我对 TPL DataFlow 进行了一些实验,通过将哈希计算卸载到与读取文件的线程不同的线程,我看到了一些边际性能改进 (~10%)。这些微薄的收益是以复杂性为代价的(处理ICryptoTransform 接口的方法,使用ArrayPool 以避免过多的缓冲区分配等),所以我认为这不值得。跨度>
  • @TheodorZoulias 啊,好吧,是的,很有趣,干得好。是的,我想这会有点混乱。虽然我只是在一种方法中阅读和散列,但也知道还有其他异步工作要做。无论如何,再一次,实证结果是好的结果。如果你想回答我一定要投票。
猜你喜欢
  • 1970-01-01
  • 2021-05-01
  • 1970-01-01
  • 2023-03-10
  • 2015-06-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多