C# 多线程调试与最佳实践


概述

多线程程序的调试比单线程复杂得多。本文介绍常见的调试技巧、诊断工具和最佳实践。

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();

// 线程 1
lock (lockA)
{
Thread.Sleep(100);
lock (lockB) { } // 等待 lockB
}

// 线程 2
lock (lockB)
{
Thread.Sleep(100);
lock (lockA) { } // 等待 lockA
}

// 诊断方法:
// 1. 在 VS 中中断执行
// 2. 打开"调试 → 窗口 → 线程"
// 3. 查看每个线程的调用堆栈,找出都在等待什么锁
// 4. 使用"并行堆栈"窗口可视化

死锁预防策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 策略1:固定锁顺序
// 所有线程按相同顺序获取锁(lockA 然后 lockB)

// 策略2:使用超时
if (Monitor.TryEnter(lockA, TimeSpan.FromSeconds(1)))
{
try
{
if (Monitor.TryEnter(lockB, TimeSpan.FromSeconds(1)))
{
try { /* 临界区 */ }
finally { Monitor.Exit(lockB); }
}
}
finally { Monitor.Exit(lockA); }
}

// 策略3:使用 AsyncLock 配合超时
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
// 现象:某些线程长时间得不到 CPU
// 原因:
// 1. 高优先级线程持续占用
// 2. 锁竞争过于激烈
// 3. 线程池耗尽

// 诊断:
// 1. 检查 Thread.CurrentThread.Priority
// 2. 使用性能监视器查看线程队列长度
// 3. 使用 ThreadPool.GetAvailableThreads

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
// 使用 PerformanceCounter 监测
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
// 使用 System.Diagnostics.Tracing 记录事件
[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() { }

// 例外:UI 事件
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);

8. 配置 ConfigureAwait(库代码)

1
2
3
4
5
6
// 库代码中
await DoWorkAsync().ConfigureAwait(false);

// UI 应用中需要更新 UI 时
var data = await FetchDataAsync(); // 恢复上下文
this.TextBox.Text = data;

调试检查清单

  • 是否所有共享变量都受保护?
  • 是否存在死锁风险(锁顺序一致)?
  • 是否有线程安全集合可用?
  • 异常是否被正确处理?
  • 是否支持取消?
  • 线程池是否可能耗尽?
  • 是否存在不必要的阻塞?

参考资源