说到 volatile
,它的中文含义叫 易变的
,说直白点就是不稳定,任何人使用它都要当点心,为了说明问题,先上一段代码:.
public class Worker
{
private bool _shouldStop;
public void DoWork()
{
bool work = false;
// 注意:这里会被编译器优化为 while(true)
while (!_shouldStop)
{
work = !work; // do sth.
}
Console.WriteLine("工作线程:正在终止...");
}
public void RequestStop()
{
_shouldStop = true;
}
}
public class Program
{
public static void Main()
{
var worker = new Worker();
Console.WriteLine("主线程:启动工作线程...");
var workerTask = Task.Run(worker.DoWork);
// 等待 500 毫秒以确保工作线程已在执行
Thread.Sleep(500);
Console.WriteLine("主线程:请求终止工作线程...");
worker.RequestStop();
// 待待工作线程执行结束
workerTask.Wait();
//workerThread.Join();
Console.WriteLine("主线程:工作线程已终止");
}
}
接下来我们将程序跑起来,可以看到程序无法退出,那什么原因呢?难道 _shouldStop
还没有别改为 true 吗?只能用 windbg 求证啦,首先我们反汇编下 DoWork()
方法。
0:009> !U /d 022e08db
Normal JIT generated code
ConsoleApp5.Worker.DoWork()
Begin 022e08d0, size 1c
D:\net5\ConsoleApp1\ConsoleApp5\Program.cs @ 21:
022e08d0 55 push ebp
022e08d1 8bec mov ebp,esp
D:\net5\ConsoleApp1\ConsoleApp5\Program.cs @ 23:
022e08d3 0fb64104 movzx eax,byte ptr [ecx+4]
022e08d7 85c0 test eax,eax
022e08d9 7504 jne 022e08df
>>> 022e08db 85c0 test eax,eax
022e08dd 74fc je 022e08db
D:\net5\ConsoleApp1\ConsoleApp5\Program.cs @ 27:
022e08df 8b0d50234603 mov ecx,dword ptr ds:[3462350h] ("工作线程:正在终止...")
022e08e5 e87a46e757 call mscorlib_ni!System.Console.WriteLine(System.String)$##6000B79 (5a154f64)
D:\net5\ConsoleApp1\ConsoleApp5\Program.cs @ 28:
022e08ea 5d pop ebp
022e08eb c3 ret
接下来我们重点解读下 Progrom.cs:23
行的汇编指令。
1. movzx eax,byte ptr [ecx+4]
表示将 ecx+4 位置上的值给 eax,这里要提醒下,ecx 表示当前的 this 指针,也就是 worker 实例。
0:009> !do ecx
Name: ConsoleApp5.Worker
MethodTable: 00964dd8
EEClass: 00961310
Size: 12(0xc) bytes
File: D:\net5\ConsoleApp1\ConsoleApp5\bin\Release\ConsoleApp5.exe
Fields:
MT Field Offset Type VT Attr Value Name
59cb878c 4000001 4 System.Boolean 1 instance 1 _shouldStop
所以 ecx+4 就是把 _shouldStop 值给提取出来了。
2. test eax,eax
test 指令表示将 eax 和 eax 进行逻辑与运算,将与结果
放到 ZF
标志位寄存器上,要知道的是,此时 windows 执行此指令时 _shouldStop
肯定还是 0 ,所以此时的 ZF=1
对吧。
3. jne 022e08df 和 je 022e08db
jne 表示当前的 ZF=0
时跳转到 022e08df
处,je 表示当前的 ZF=1
时跳转到 022e08db
,按照方法反汇编的代码,此时会调转到指令 022e08db 85c0 test eax, eax
处,然后又执行了一次 test eax ,eax
,结果还是要走 je 022e08db
语句,形成了一个while循环
发现问题
从我刚才的描述中,是否发现了一个问题,后续的判断都是做 test eax,eax
操作,也就是程序无法退出时 _shouldStop
已经变更为 1 ,此时的 eax
在循环做 test 操作,并没有感知到 _shouldStop
的变化,不信的话,可以看下此时的 eax 寄存器值。
0:009> r eax
eax=00000000
解决方案
有了问题,接下来的解决方案是什么呢?对,用 volatile
关键词,目的就是让 cpu 感知到内存的变化,那如何做到感知呢?可以看看最终的汇编代码就好了。
接下来我们用 windbg 来启动 exe 程序,并且在 Console.WriteLine("工作线程:正在终止...");
上下一个断点,也就是 27 行代码处。
0:000> !mbp Program.cs 27
The CLR has not yet been initialized in the process.
Breakpoint resolution will be attempted when the CLR is initialized.
0:000> g
Breakpoint: JIT notification received for method ConsoleApp5.Worker.DoWork() in AppDomain 004d2f40.
Breakpoint set at ConsoleApp5.Worker.DoWork() in AppDomain 004d2f40.
Breakpoint 2 hit
eax=023408d0 ebx=024a8240 ecx=024a2394 edx=024a48b4 esi=024a48ec edi=04eaf99c
eip=023408df esp=04eaf928 ebp=04eaf928 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
023408df 8b0d50234a03 mov ecx,dword ptr ds:[34A2350h] ds:002b:034a2350=024a8294
为了看清楚点,我们这次再把 DoWork 方法的反汇编贴一下。
0:009> !U /d 023408df
Normal JIT generated code
ConsoleApp5.Worker.DoWork()
Begin 023408d0, size 1c
D:\net5\ConsoleApp1\ConsoleApp5\Program.cs @ 21:
023408d0 55 push ebp
023408d1 8bec mov ebp,esp
D:\net5\ConsoleApp1\ConsoleApp5\Program.cs @ 23:
023408d3 80790400 cmp byte ptr [ecx+4],0
023408d7 7506 jne 023408df
023408d9 80790400 cmp byte ptr [ecx+4],0
023408dd 74fa je 023408d9
D:\net5\ConsoleApp1\ConsoleApp5\Program.cs @ 27:
>>> 023408df 8b0d50234a03 mov ecx,dword ptr ds:[34A2350h] ("工作线程:正在终止...")
023408e5 e87a46e157 call mscorlib_ni!System.Console.WriteLine(System.String)$##6000B79 (5a154f64)
D:\net5\ConsoleApp1\ConsoleApp5\Program.cs @ 28:
023408ea 5d pop ebp
023408eb c3 ret
对比之前的代码看,人家做了两点变更。
-
将 test 换成了 cmp
-
用寄存器比较 改成 取内存地址值做比较。
因为 ecx
就是 work实例地址,所以一直取它的话自然就更感知到 _shouldStop
的变更,不过话说不回来,这也是以牺牲性能为代价的,毕竟取内存还要过 地址总线,数据总线,控制总线
,哈哈。。。
原理就是这么一个原理,啰嗦了这么多,相信大家应该也明白了吧。