昨天在调试c#程序的时候,遇到了个问题,怀疑是多线程竞争产生的问题。在此记录下。

问题简单来说是这样的:程序需要不断地从设备上读取数据,设备上的数据是个整数,而且是随时间递增的。但是什么时候递增是不定的。

程序需要完成的是在设备上的数据增加的时候,调用一个回调函数。我的实现是使用System.Timers.Timer定时器,每50ms会去查询一下设备的数据,如果设备的数据和我类中之前记录的数据不一致,那么就是递增了,就调用那个回调函数。

调试中发现有时会出现一个现象:设备上的数据随时间从1依次递增到了4,但是回调函数却被调用了5次,因此在回调函数中产生了逻辑错误,从而抛出了异常。

代码大概如下逻辑

class Example
{
	int oldvalue = -1;
	void timer_callback()//be called every 50ms
	{
		int data = get_data_from_device();
		if(data!=oldvalue)
		{
			oldvalue = data;
			invoke_callback_function();
		}
	}
}

我主要是通过Console.WriteLine来确定函数有没有执行以及执行顺序的,具体这个写控制台的IO操作,在多线程环境下是否能正常反应程序的顺序还不好说。

通过控制台的日志发现,有两次从设备得到的是同样的data,但是都调用了回调函数。单线程程序中显然不会有这样的问题,所以我就基本确定这个问题跟多线程有关。

c#中有3中不同的Timer。我用的这个System.Timers.Timer似乎是通过线程池实现的,通过打印Thread.CurrentThread.ManagedThreadId可以确定timer_callback是会在不同的线程中被调用的。

所以我做了个修改,将oldvalue加了个volatile属性。然而,并没有解决问题。我想起来看过一些c++方面的书,里面强调volatile不能用于多线程的同步。c#中呢?我去查了查。发现不论中文csdn或者博客园还是stackoverflows里面很多人都认为volatile可以”让对变量的修改马上被其他线程观测到“。

然而这时错误的。看ms docs,https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/volatile,里面有这样一段话:

When writing to a field marked volatile, the volatile keyword controls the order in which writes are performed. It does not guarantee that these writes are immediately visible to other threads.

特别强调了volatile并不能让对变量的写马上让其他线程看到。

c#中基本的同步机制就volatile,Interlocked,还有就是lock了。interlocked行不行我没有实验,调试中我直接在timer_callback中将if语句放在了lock(this)之中,后来实验多次确认问题确实解决了。

我后来翻effective modern cpp,中间有一节也是专门强调volatile和lock的区别。c++中volatile专用于特种内存的读写,与线程之间的同步无关。

通过这次调试,我对线程之间的同步多了一些了解。以前我认为lock都是放在由于对数据结构同时进行读写而会对数据造成破坏的地方。现在看来,c#中lock似乎还能对可见性做出保证。如果是cpp,这种要让所有线程马上观测到变化的变量,似乎用原子atomic<int>更好。但是c#似乎没有提供这种方便的原子。Interlocked系列函数在我的使用背景下似乎有些麻烦,我没把握用好,就没使用。在这方面c++更加方便一些。

在stackoverflow上查找相关问题的时候,发现一篇文章,详细讲述了c语言中的volatile和c#中volatile的区别:https://ericlippert.com/2011/06/16/atomicity-volatility-and-immutability-are-different-part-three/

另外晚上的时候,我试图自己写个小demo来复现这个多线程的bug,但是试了很久都没有成功复现。可能和运行的设备有关?多线程真的太复杂了。还有内存模型等,坑好深,慢慢学。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注