标题:记一个诡异的问题 出处:Felix021 时间:Tue, 07 Jul 2015 23:36:56 +0000 作者:felix021 地址:https://www.felix021.com/blog/read.php?2143 内容: 今天Sandy同学在开发一个网络相关应用的时候遇到了一个奇怪的问题。 大约是这样的一个单例类Foo(以下是类python的伪代码,实际是VB.NET),当调用方法 bar('remove', key, value) 的时候,经常(而不是总是)在for循环过程中报错,错误信息是 "循环过程中_pool已经被修改" 。 class Foo(singleton): _pool = {} _mutex = Threading.Mutex() def bar(self, action, key, value=None): self._mutex.waitOne() if action == 'add': self._pool[key] = value else: #'remove' remove_keys = [] for key, value in self._pool.items(): if do(key, value): remove_keys.append(key) for key in remove_keys: del self._pool[key] self._mutex.release() _pool 作为Foo的一个私有成员并且被 _mutex 保护着,理论上是不会出现这个问题的,然而各种排查的表现都指向了线程间的竞争问题,因为只有在调用到 bar('add', key, value) 的时候,_pool才有可能被修改到。 仔细查了一下MSDN上面Threading.Mutex的说明,在备注一栏中藏着一句话:"拥有互斥体的线程可以在对 WaitOne 的重复调用中请求相同的互斥体而不会阻止其执行。",也就是说,如果是同一个线程两次调用 bar 方法的话,这个 _mutex 就相当于失效了。 换用其他的互斥锁机制(例如Syncing)并不解决这个问题(事实上Mutex已经是第三个尝试选项了)。我们甚至试着采用Threading.Senaphore,然而却导致整个进程卡住。 于是debug.print把 Threading.Thread.CurrentThread.ManagedThreadId 输出到控制台,发现在出现错误之前是有多个不同的thread id,但是出错的时候,确实是同一个线程两次调用 bar 方法,也就是说 _mutex 确实不能解决这个问题。 经过各种排查,确认是正好在 do(key, value) 方法调用中出现的,然而 Sandy 同学信誓旦旦地保证,do(key, value) 方法绝对不可能递归地调用回到 bar 函数。由于 do(key, value) 内部调用了某个阻塞的网络请求,据此我推测,.NET的网络模型底层使用了线程+纤程的模型,那只能想办法了。通过查看 bar('add', key, value) 的调用栈,发现确实这是同一个请求,但是中间夹杂了一个未知的"外部过程",也就是说空闲的线程被调度来做其他的事情了。 深坑一个,但是既然找到了原因,就可以考虑如何针对性地去解决它,初步的想法是,Semaphore理论上应该是可以解决这个问题的,可能之前没有细看MSDN、调用方式有点问题。 Sandy同学的想法是,既然 Mutex 已经过滤掉了线程间的冲突,那我们就自己模拟 Semaphore 来解决线程内的冲突,只要简单增加一个初始值为 0 的 _counter 变量 ,在 self._mutex.waitOne() 后面加上 while self._counter != 0: Threading.sleep(10) #10ms self._counter = 1 并在 self._mutex.release() 之前执行 self._counter = 0 就可以了。 想法是美好的,但是一执行就卡死在 foo('add') 调用的 while 循环里。简单分析一下就能发现,这个线程既然一直在while循环里面,就不可能被调度回到 foo('remove') 的纤程去修改 _counter=0,于是就卡住了。 没办法,再回头去仔细看MSDN,Threading.Semaphore 确实没有类似Mutex这样的同线程调用,于是把这个代码按照Example重新写了一遍,但是还是卡死了…… 然后我瞬间醒悟过来——这似乎根本就是一个因为.net底层实现导致的死锁!除非上层应用能控制线程调度的细节,否则无论是信号量还是修改过的Mutex(同一个线程不能多次获得的)都不能解决这个问题。于是暂时的结论是可能要采用自己实现的线程池来进行调度,但是改动似乎很大。 完。 UPDATE @ 20150720 后来仔细考虑了下,根本问题是在do(key, value) 内部调用的那个“阻塞”请求,在临界区内本就不该调用阻塞请求。按照.net的文档,那个请求应当是非阻塞的,但是不知道为什么在这里阻塞了。由于我对.net并不了解,我没有再继续追究了。 Generated by Bo-blog 2.1.0