你以爲輕易就可以掌握的多線程--談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()

創建多線程執行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:多線程崩潰(一)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章