使用Parallel.ForEach时能否实现分层缩进的结构化美观日志?
问题原因分析
你当前的实现存在三个核心问题:
- 静态变量
_callCount的自增操作不是原子操作,多线程并行执行时会出现竞态条件,得到的计数结果不可靠 - 控制台是全局共享资源,多线程同时调用
Console.WriteLine时输出内容会随机穿插,天然无法保证顺序 - 全局计数不能代表单个并行任务的方法调用层级:每个并行迭代的调用栈是独立的,全局累加的数值和当前任务的嵌套深度没有对应关系
可行实现方案
方案1:单任务日志缓存 + 统一输出(最符合你预期的美观效果)
这个方案可以保证每个任务的所有日志是连续整块输出,完全不会和其他任务的日志穿插,层级缩进也完全符合要求。
实现思路:
每个并行迭代任务内部维护独立的日志列表和当前缩进层级,嵌套方法调用时把层级+1往下传递,所有日志先写入当前任务的私有列表,等整个任务执行完成后,加锁一次性输出该任务的所有日志。
修改后的代码示例:
namespace Parallel_ForEach_Logging { class Program { // 控制台输出锁,保证整块日志输出不被打断 private static readonly object _consoleLock = new(); static void Main(string[] args) { IEnumerable<int> data = Enumerable.Range(0, 2); Console.WriteLine("***"); ParallelOptions options = new() { MaxDegreeOfParallelism = Environment.ProcessorCount }; Parallel.ForEach(data, options, datum => { // 每个任务独立的日志缓存 var taskLogs = new List<string>(); // 初始缩进层级为0 PrintNum(datum, taskLogs, indent: 0); // 加锁一次性输出当前任务的所有日志 lock (_consoleLock) { foreach (var log in taskLogs) { Console.WriteLine(log); } } }); Console.WriteLine("***"); } public static void PrintNum(int num, List<string> taskLogs, int indent) { taskLogs.Add($"{new string(' ', indent * 2)}>>> IN PrintNum"); taskLogs.Add($"{new string(' ', (indent + 1) * 2)}{num}"); taskLogs.Add($"{new string(' ', indent * 2)}<<< OUT PrintNum"); // 嵌套调用时缩进层级+1 PrintString($"\"{num}\"", taskLogs, indent + 1); } public static void PrintString(string str, List<string> taskLogs, int indent) { taskLogs.Add($"{new string(' ', indent * 2)}>>> IN PrintString"); taskLogs.Add($"{new string(' ', (indent + 1) * 2)}{str}"); taskLogs.Add($"{new string(' ', indent * 2)}<<< OUT PrintString"); } } }
输出效果示例:
*** >>> IN PrintNum 0 <<< OUT PrintNum >>> IN PrintString "0" <<< OUT PrintString >>> IN PrintNum 1 <<< OUT PrintNum >>> IN PrintString "1" <<< OUT PrintString ***
方案2:实时输出日志(适合需要即时看到日志的场景)
如果不能等任务跑完再输出,可以用AsyncLocal存储当前上下文的缩进层级,每次输出日志时加锁保证单行日志不被打断。这个方案的输出行缩进是正确的,但不同任务的日志仍然会穿插。
核心实现代码:
// 存储当前异步上下文的缩进层级 private static readonly AsyncLocal<int> _currentIndent = new(); private static readonly object _consoleLock = new(); public static void PrintNum(int num) { var indent = _currentIndent.Value; lock (_consoleLock) { Console.WriteLine($"{new string(' ', indent * 2)}>>> IN PrintNum"); Console.WriteLine($"{new string(' ', (indent + 1) * 2)}{num}"); Console.WriteLine($"{new string(' ', indent * 2)}<<< OUT PrintNum"); } // 进入嵌套方法前增加缩进 _currentIndent.Value++; PrintString($"\"{num}\""); // 退出嵌套方法后恢复缩进 _currentIndent.Value--; } public static void PrintString(string str) { var indent = _currentIndent.Value; lock (_consoleLock) { Console.WriteLine($"{new string(' ', indent * 2)}>>> IN PrintString"); Console.WriteLine($"{new string(' ', (indent + 1) * 2)}{str}"); Console.WriteLine($"{new string(' ', indent * 2)}<<< OUT PrintString"); } }
补充说明
你之前尝试的全局计数方案本身逻辑就不成立,并行场景下每个任务的调用栈独立,必须为每个任务单独维护嵌套层级,不能用全局变量统计。你需要的分层缩进日志需求是完全可以实现的,优先推荐方案1,在兼顾并行性能的同时可以得到结构最清晰的日志。
内容的提问来源于stack exchange,提问作者user17055229
相关产品推荐
相关产品推荐

