你以为轻易就可以掌握的多线程--谈python多线程竞争资源引起程序崩溃的解决办法

在使用多线程时,通常在考虑多线程竞争系统资源的时候,一般会使用信号量控制之类的锁(Lock)来限制同一时间访问修改资源的线程。在PC桌面级程序开发当中,共享资源数据同步的问题尤其值得引起重视。下面来看一下使用信号量的方法控制多线程访问共享数据的示范。
定义共享数据counter计数器

global counter
counter=0

定义Task工作内容,执行工作时,将counter计数器+1

class Task:
	#自定义设置Task工作内容(略)
	def dojob(self):
		global counter
		counter=counter+1
		pass

设置信号量。

#同一时间只允许单个线程访问修改共享数据块
semaphore=threading.BoundedSemaphore(1)

使用信号量控制多线程访问共享数据。

class Task:
	def dojob(self):
		global counter
		counter=counter+1
		pass
		
	#通过信号量控制线程访问共享数据块
	def syncupdate(self):
		#加锁
		semaphore.acquire()
		#更新数据
		self.dojob()
		#释放锁
		semaphore.release()

设置线程入口函数

#线程入口
def schedule(tid, task:Task):
	print('Task['+str(tid)+'] started.')
	#task.dojob()	#此为未增加信号量,是抄写笔误,应为下一步
	task.syncupdate()

创建多线程执行tasks

import threading
#自定义线程池数组
threadpool=[]
#tasks定义省略
tasks=[...]
for tid in range(len(tasks)):
	#创建线程,设置线程入口及参数
	t=threading.Thread(target=schedule, args=(tid, tasks[tid]))
	threadpool.append(t)
	t.start()
for t in threadpool:
	#等待线程完成
	t.join()

在这里,counter计数器是共享资源,每次访问都需使用信号量semaphore进行获取访问的权限。

semaphore.acquire()

如果有线程正在使用共享资源,其他线程则进入等待状态。
每次完成共享资源修改后,通过信号量释放对其的控制访问权。

semaphore.release()

信号量控制多线程的使用基本完成。
以往把新手搞得一团糟的多线程,现在看来,似乎太简单–so easy!

这样想的话,你就大错特错!因为一个程序的执行,包含太多的东西在里面。一个程序首先基于一个特定的平台,不管其是否可移植性如何。程序执行需要资源包括内部的和外部的。我们将内部资源定义为软件执行体系结构,外部资源定义为操作系统及相关平台体系结构。通常情况下,在软件开发周期,考虑的是软件执行体系结构的问题,而无法设想操作系统及相关平台体系结构的关联。软件设计往往是静态的,很难顾及实际状况中实时动态出现的意外。

事实上,程序执行的每一步都涉及到系统资源的竞争,从涉及内存资源的变量定义赋值到关联操作系统资源的窗口变动。而资源的分配和释放,是需要操作系统及相关平台执行实时调度的,是涉及时间的。现实中,很多程序员包括一些“老油条”都会犯的一个错误:CPU速度是很快的,足以让程序的某一些步骤无时差地“瞬间”完成。这个在内存资源分配普通基本类型变量来说,基本上可以认为是正确的。置于涉及操作系统及相关系统软件平台中,这是极大的“错误”。因为涉及资源分配的“实时”的系统软件操作并非完全是实时的。学过操作系统的大概应当理解这句话的含义。CPU“实时”操作本质是时间管理和资源调度。只不过CPU的速度相对人类的大脑反应要快的多,才让人觉得是“实时”的。CPU本质是时间片竞争资源。在一些敏感资源的竞争中,尤为明显。当一个系统资源耗时“较长”时,仍以“实时”的角度去考虑问题时,极有可能出现难以估计的意外。

当多线程去竞争敏感系统资源时,由于请求的速度非常快,但是系统分配和释放以及使用资源的过程是相对“较慢”的。很有可能由于CPU时钟的误差而出现意外。我们知道计算机的时间计算,实际上是关联CPU时钟的。而CPU时钟尽管是非常精确的,但仍有小误差的可能。而系统平台存在bug也是可能的。这就造成系统敏感资源的分配和释放的异常–“不及时”,进而导致意外发生–程序崩溃。这是经过实践检验的。

当这类无法catch/except捕捉的exception异常–fatal error出现时,你应当考虑的是,极有可能是系统敏感资源的分配和释放出现问题。以上代码,通过单线程执行时没有异常,因为没有资源的竞争矛盾。通过多线程来执行,当涉及敏感系统资源时,极有可能引起程序崩溃。那怎么办呢?我们知道,CPU执行是CPU时间片(CPU Time)的竞争。如果请求的时间片和使用资源的时间片相差较大容易引起系统异常,可以考虑使用延时的方法。毫秒ms是系统时间的精确单位。使用任意整数毫秒(符合规则)的延迟,均可以避免CPU时钟误差和系统时间差bug引起的异常崩溃。因此,可以在关键位置使用延时操作代码进行时间误差校正。

time.sleep(1)
#time.sleep(.1)

针对上述多线程代码,可修改为

#通过信号量控制线程访问共享数据块
	def syncupdate(self):
		#加锁
		semaphore.acquire()
		#操作延迟1秒:进行时间误差校正
		time.sleep(1)
		#更新数据
		self.dojob(self)
		#释放锁
		semaphore.release()

至此关于多线程“恶意”竞争资源引起的程序崩溃完美解决。
后记:事实上,很少关于多线程“恶意竞争”的文章论及根本原因。只知道其然,不知其所以然。Java中使用synchronized来限制异步线程亦是同样的道理。对于普通基本类型应该没有问题,但部署在实际环境中又涉及系统敏感资源分配调度的就很难说了。
这类文章度娘搜索相当少,附上几篇。
附:
smking:多线程并发访问可能出现的崩溃问题
柳鲲鹏:多线程访问导致崩溃一例
sinat_41685845:多线程崩溃(一)

你可能感兴趣的:(Bug,Python,研究)