优化了对共享资源访问的同步



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

问题是Logger不是线程安全的。使用lockMonitor来同步Parallel.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();
}
}
}

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

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

关于您的问题:

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

异步和等待模式将在完成IO绑定工作时将线程返回线程池。任务调度程序将使用这些线程进行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数据流管道的东西,进行计算,发布,然后批量处理写入的结果,这可能会减少分配,并具有处理同步和异步工作负载的优势。

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

最新更新