当前线程ID用Thread.CurrentThread.ManagedThreadId(唯一整数,适合日志和调试),名称用Thread.CurrentThread.Name(需手动赋值且仅一次,否则为null)。

怎么拿到当前线程的 ID 和名称
线程 ID 是 Thread.CurrentThread.ManagedThreadId,它返回一个整数,稳定且可用于日志标记;线程名称是 Thread.CurrentThread.Name,但注意:它默认为 null,必须手动赋值才有意义。
常见错误是直接打印 Thread.CurrentThread.Name 却没设过值,结果日志里全是 null,误以为“没取到”。
- 给线程命名只对当前线程有效,且只能设一次 —— 第二次赋值会抛
InvalidOperationException - 新起的
Task或线程池线程(如ThreadPool.QueueUserWorkItem)默认无名,也不建议强行命名,容易混淆上下文 - 调试时可在入口处(如
Main方法或async void事件处理中)加一句:Thread.CurrentThread.Name ??= "Main";
为什么线程 ID 比名称更适合死锁排查
线程名称是人工设定的字符串,容易重复、遗漏或写错(比如多个线程都叫 "Worker");而 ManagedThreadId 是 CLR 分配的唯一整数,在 Visual Studio 的“线程”窗口、内存转储(dump)分析、甚至 !threads WinDbg 命令里都能直接对应上。
尤其在死锁现场,你看到两个线程互相等待对方持有的锁,这时比对它们的 ManagedThreadId 能快速定位是哪两条执行路径卡住了。
- 同一进程内,
ManagedThreadId在线程生命周期内不变,即使线程被挂起/恢复 - 不要用
Thread.CurrentThread.Id—— 它是 Win32 线程 ID,类型为IntPtr,跨平台不一致,且 .NET 调试器通常不显示它 - 日志中建议格式统一:
[TID:{Thread.CurrentThread.ManagedThreadId}] DoSomething()
在 lock 语句前后打日志的关键姿势
单纯加日志不能解决死锁,但能帮你确认“哪个线程在等谁、等了多久、从哪开始等”。重点不是记录“进了 lock”,而是记录“准备进”和“终于进了”两个时间点。
错误做法是只在 lock(obj) 后面打日志,此时死锁已发生,日志根本打不出来。
- 在
lock前记录:Log.Debug($"[TID:{tid}] Trying to acquire lock on {obj.GetHashCode():X8}"); - 在
lock内第一行记录:Log.Debug($"[TID:{tid}] Acquired lock on {obj.GetHashCode():X8}"); - 避免对同一对象反复
GetHashCode()—— 可提前存为局部变量,防止锁对象在等待中被 GC 或替换 - 如果对象是
static readonly object,可直接用类名+字段名代替哈希码,更易读
用 Visual Studio 实时观察线程状态的实操要点
调试多线程问题时,“并行堆栈”窗口(Debug → Windows → Parallel Stacks)比“线程”窗口更直观,但它依赖符号和调试信息。若看不到完整调用链,大概率是因为优化开启或 PDB 不匹配。
- 确保项目编译模式为
Debug,且未勾选 “Enable Just My Code”(调试 → 选项 → 调试 → 常规) - 在“线程”窗口中右键某线程 → “切换到线程”,VS 会尝试暂停并高亮该线程当前执行位置 —— 这步常失败,因为线程可能正阻塞在内核态(如
Monitor.Enter),此时需结合“并行堆栈”看等待链 - 若死锁已发生,别急着点“继续”,先导出 dump:
调试 → 转储调试 → 将转储保存为...,后续可用dotnet-dump analyze离线分析
线程 ID 看似简单,但真正有用的前提是:它得出现在你所有关键日志里,并且和调试器里的 ID 对得上。漏掉任意一环,排查就变成猜谜。


















