C#并行场景使用ILogger时日志未完全刷出如何处理?
控制台日志提前终止输出问题及解决方案
问题复现
如下代码中,程序会在日志完成全部刷出操作前就终止运行:
class Program { static void Main(string[] args) { var count = -1; var tasks = new ConcurrentBag<Task>(); var services = new ServiceCollection(); services.AddLogging(configure => configure.AddConsole()); var serviceProvider = services.BuildServiceProvider(); var logger = serviceProvider.GetService<ILogger<Program>>(); if (logger is null) throw new NullReferenceException(); Parallel.ForEach(Enumerable.Range(0, 1000), i => { tasks.Add(Task.Run(() => logger.LogInformation(Interlocked.Increment(ref count).ToString()))); //tasks.Add(Task.Run(() => Console.WriteLine(Interlocked.Increment(ref count).ToString()))); }); Task.WhenAll(tasks).Wait(); } }
按理应当打印1000条日志语句,但实际仅刷出了少量内容,运行输出示例如下:
info: Net5.Program[0] 6 info: Net5.Program[0] 2 info: Net5.Program[0] 5 info: Net5.Program[0] 1 info: Net5.Program[0] 7 info: Net5.Program[0] 4 info: Net5.Program[0] 3 info: Net5.Program[0] 0 info: Net5.Program[0] 8 info: Net5.Program[0] 10 C:\Users\MyName\source\repos\ConsoleApp\Net5\bin\Debug\net5.0\Net5.exe (process 28048) exited with code 0. Press any key to close this window . . .
但如果使用代码中被注释掉的Console.WriteLine,所有行都会被正常打印,由此推测控制台日志器的底层为异步实现。
提问:不借助手动延迟的前提下,如何确保所有日志语句都被正确等待执行完成?
解决方案
提问者最终采用的实现代码如下,可完美解决该问题:
class Program { static void Main(string[] args) { var count = -1; var services = new ServiceCollection(); services.AddLogging(configure => configure.AddConsole()); using var serviceProvider = services.BuildServiceProvider(); var logger = serviceProvider.GetRequiredService<ILogger<Program>>(); var tasks = Enumerable.Range(0, 50000) .AsParallel() .Select(_ => Task.Run(() => logger.LogInformation(Interlocked.Increment(ref count).ToString()))); Task.WhenAll(tasks).Wait(); } }
核心优化点
- 给ServiceProvider添加了
using声明,会在程序退出前自动释放ServiceProvider - 使用
GetRequiredService获取ILogger实例,无需做空值检查 - 使用
Enumerable.Range(...).AsParallel.Select(...)替代Parallel.ForEach,全程基于Task实现并行逻辑
原理解释
.NET内置的Console日志提供程序采用了异步队列设计:调用Log*系列方法时,日志内容只是被提交到内部的待处理队列,不会同步阻塞等待写入控制台。默认情况下程序退出时不会等待队列处理完成,就会直接终止进程,导致日志丢失。
而ServiceProvider被释放时,会触发所有托管服务的清理逻辑,其中就包含Console日志提供程序的刷出逻辑,会等待队列中所有未处理的日志全部写入控制台后,释放流程才会结束,因此可以保证所有日志都正常输出。
内容的提问来源于stack exchange,提问作者Kristoffer Lerbæk Pedersen
相关产品推荐
相关产品推荐

