概述
多线程程序的调试比单线程复杂得多。本文介绍常见的调试技巧、诊断工具和最佳实践。
Visual Studio 调试技巧
线程窗口
- 调试 → 窗口 → 线程:查看所有线程的调用堆栈
- 可以冻结/恢复线程(右键 → 冻结)
- 标记线程以便跟踪
并行任务窗口
- 调试 → 窗口 → 并行任务:查看所有 Task 的状态
- 显示任务 ID、状态、位置、父任务
- 支持分组和筛选
并行堆栈窗口
- 调试 → 窗口 → 并行堆栈
- 显示所有线程的调用堆栈关系
- 线程视图:按线程分组
- 任务视图:按任务分组
设置线程名称
1 2 3 4
| Thread.CurrentThread.Name = $"Worker-{taskId}";
|
常见问题诊断
死锁检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| object lockA = new object(); object lockB = new object();
lock (lockA) { Thread.Sleep(100); lock (lockB) { } }
lock (lockB) { Thread.Sleep(100); lock (lockA) { } }
|
死锁预防策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
if (Monitor.TryEnter(lockA, TimeSpan.FromSeconds(1))) { try { if (Monitor.TryEnter(lockB, TimeSpan.FromSeconds(1))) { try { } finally { Monitor.Exit(lockB); } } } finally { Monitor.Exit(lockA); } }
if (await asyncLock.LockAsync(TimeSpan.FromSeconds(1))) { using (release) { } }
|
线程饥饿诊断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
int worker, io; ThreadPool.GetAvailableThreads(out worker, out io); Console.WriteLine($"可用线程: {worker}, 可用 IO: {io}");
if (worker < 10) { Console.WriteLine("警告:线程池接近耗尽"); }
|
使用诊断工具
性能计数器
1 2 3 4 5 6 7 8 9 10
| using System.Diagnostics;
var threadCount = new PerformanceCounter( "Process", "Thread Count", Process.GetCurrentProcess().ProcessName);
Console.WriteLine($"当前线程数: {threadCount.NextValue()}");
|
ETW (Event Tracing for Windows)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| [EventSource(Name = "MyApp")] public class MyEventSource : EventSource { public static MyEventSource Log = new MyEventSource(); [Event(1, Level = EventLevel.Informational)] public void ThreadStart(int threadId, string name) { WriteEvent(1, threadId, name); } [Event(2, Level = EventLevel.Warning)] public void LockContention(int waitMs) { WriteEvent(2, waitMs); } }
MyEventSource.Log.ThreadStart(Thread.CurrentThread.ManagedThreadId, name);
|
使用 Concurrency Visualizer
Visual Studio Enterprise 中的 Concurrency Visualizer:
- 工具 → 获取工具和功能 → 安装”并发可视化工具”
- 分析 → 并发可视化工具 启动
- 可查看线程执行时间、阻塞原因、CPU 使用率
日志记录
线程安全的日志
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| using System.Collections.Concurrent;
public class ThreadSafeLogger { private readonly ConcurrentQueue<string> _logQueue = new ConcurrentQueue<string>(); private readonly string _logFile; public ThreadSafeLogger(string logFile) { _logFile = logFile; } public void Log(string message) { string timestamp = DateTime.Now.ToString("HH:mm:ss.fff"); string threadId = Thread.CurrentThread.ManagedThreadId.ToString(); string logEntry = $"[{timestamp}] [Thread {threadId}] {message}"; _logQueue.Enqueue(logEntry); Task.Run(() => Flush()); } private void Flush() { while (_logQueue.TryDequeue(out string entry)) { File.AppendAllText(_logFile, entry + Environment.NewLine); } } }
var logger = new ThreadSafeLogger("app.log"); logger.Log("开始处理");
|
使用 ILogger(Microsoft.Extensions.Logging)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| using Microsoft.Extensions.Logging;
public class Worker { private readonly ILogger<Worker> _logger; public Worker(ILogger<Worker> logger) { _logger = logger; } public async Task RunAsync(CancellationToken token) { _logger.LogInformation("线程 {ThreadId} 开始工作", Thread.CurrentThread.ManagedThreadId); while (!token.IsCancellationRequested) { try { await DoWorkAsync(); } catch (Exception ex) { _logger.LogError(ex, "工作线程异常"); } } } }
|
最佳实践总结
1. 优先使用 Task 而非 Thread
1 2 3 4 5 6
| Thread t = new Thread(() => DoWork()); t.Start();
await Task.Run(() => DoWork());
|
2. 使用 CancellationToken 支持取消
1 2 3 4 5 6
| bool _isCancelled; while (!_isCancelled) { }
while (!token.IsCancellationRequested) { }
|
3. 避免共享可变状态
1 2 3 4 5 6 7 8 9 10
| int sharedCounter = 0; Parallel.For(0, 1000, i => sharedCounter++);
int total = 0; Parallel.For(0, 1000, () => 0, (i, state, local) => local + 1, local => Interlocked.Add(ref total, local));
|
4. 选择合适的同步原语
| 场景 |
推荐 |
| 简单互斥 |
lock |
| 异步互斥 |
SemaphoreSlim(1,1) |
| 限制并发 |
SemaphoreSlim |
| 读多写少 |
ReaderWriterLockSlim |
| 原子操作 |
Interlocked |
5. 避免 async void
1 2 3 4 5 6 7 8 9 10 11
| public async void DoWorkAsync() { }
public async Task DoWorkAsync() { }
private async void Button_Click(object sender, EventArgs e) { await LoadDataAsync(); }
|
6. 处理异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Task.Run(() => { MightThrowException(); });
Task.Run(() => { try { MightThrowException(); } catch (Exception ex) { Logger.LogError(ex, "任务异常"); } });
try { await task; } catch (Exception ex) { Handle(ex); }
|
7. 避免 Thread.Sleep
1 2 3 4 5
| Thread.Sleep(1000);
await Task.Delay(1000);
|
1 2 3 4 5 6
| await DoWorkAsync().ConfigureAwait(false);
var data = await FetchDataAsync(); this.TextBox.Text = data;
|
调试检查清单
参考资源