線程死鎖的思考

線程死鎖的思考


前言

前些天在公司這邊寫了個豌豆莢的爬蟲,用到了分區思想和自己實現的線程池,我自己覺得從這個過程中學到了很多東西,包括如何去設計接口和方便擴展以及代碼的規範化。之前用小數據量測試了發現沒什麼問題,後來拿了W級以上的問題,發現插入的數碼條目的量級和輸入量級有很大差異,就算算上失效的URL也不應出現這樣的情況,於是開始排查。反反覆覆看各個模塊的代碼,對應日誌信息查看,最後發現時死鎖問題導致的。

什麼是死鎖?

死鎖(英語:Deadlock),又譯爲死鎖,計算機科學名詞。當兩個以上的運算單元,雙方都在等待對方停止運行,以取得系統資源,但是>沒有一方提前退出時,這種狀況,就稱爲死鎖。在多任務操作系統中,操作系統爲了協調不同進程,能否取得系統資源時,爲了讓系統運
作,就必須要解決這個問題。
這裏指的是進程死鎖,是個計算機技術名詞。它是操作系統或軟件運行的一種狀態:在多任務系統下,當一個或多個進程等待系統資源,
而資源又被進程本身或其它進程佔用時,就形成了死鎖。

上面的解釋引自維基,死鎖有進程間的死鎖和線程間的死鎖,只要是併發情況,並且雙方都在佔有資源的情況下等待對方的資源,就會發生死鎖。在實際情況下,很容易不注意鎖,條件變量的時候而導致死鎖。

線程池中的死鎖情況

這次死鎖發生在什麼情況下呢?在最開始寫線程池的時候,我設計了線程是可重用的,主要是通過Event信號實現,通過在每個線程核心工作代碼執行完畢後會將自己歸還到池中,然後等待Event信號。主線程會以循環超時阻塞的方式監視一個任務隊列,當發現有任務時便會從線程池中取出一個線程,並設置它的任務和目標函數,然後去start或者resume,resume就是會設置Event信號讓線程不再阻塞,這裏,從池中取線程的方法_get和歸還線程方法returnThread都已經加鎖,_get和returnThread使用同一把互斥鎖,因爲在_get和returnThread方法裏面對線程池對象以及分區對象都有狀態修改並且有些操作有條件判斷,因此必須加鎖保證線程安全和同步。

這樣就真正線程安全了嗎?可以順利按照預期執行了嗎?看起來好像沒有問題,並且我這裏設置的分區數目是4,分區的初始容量是5,最大容量爲20,故池的總大小爲4*20=80,這樣對於小數目的測試確實發現不了死鎖問題。

考慮下面一種情況:

  • 線程池已經滿了,任務隊列裏面來了任務,觸發了_get方法,線程池中沒有多餘的線程,所以會阻塞在一個queue.get的方法上,我這裏面queue是最好的分區(可用的最多)對象的一個變量,裏面存放的是該分區擁有的線程的的一個唯一的id標示符,本來打算用ident即uid,但是只有在運行期再回分配,所以,採用了這個方法。
  • 正在運行的線程,核心功能函數運行完畢,想要歸還自己到池中,由於主線程調用_get時獲得了鎖,一直不會釋放,因爲沒有可用的線程,而想要歸還自己的線程由於不能獲得鎖所以不能歸還,就這樣會一直耗着,發生了死鎖。

死鎖的解決

我們通過上面的描述,發現死鎖的發生是因爲條件等待時沒有釋放鎖資源,仔細思考這句話,會發現其實我們也熟知的Condition就是爲了解決這個問題的。
python裏面的threading.Condition裏面會內置Lock/RLock鎖,並且可以條件等待時釋放鎖資源,這樣,將之前的淡出的互斥鎖改成condition,並且在queue.get方法時,先判斷條件是否滿足(有可用線程),如果可用則直接往後執行,否則cond.wait阻塞並且釋放鎖;另一方面,正在運行的thread通過returnThread時,也是通過cond.acquire來加鎖,然後這樣當主線程cond.wait的時候能夠有機會獲得鎖,然後執行,當returnThread快要結束,已經歸還後,cond.notify/conf.notify_all來通知在等待該條件的主線程。這樣就能夠順利執行。

部分代碼

  1. def get(self):
  2. self.cond.acquire()
  3. try:
  4. if self._shutdown:
  5. raise RuntimeError('ThreadPool already shutdown.')
  6. else:
  7. for partition in self.partitions:
  8. logger.info("parition #%d status:" % partition.get_partition_no())
  9. logger.info("current load: %.2f" % partition.get_load())
  10. logger.info("used size: %d" % partition.get_used_size())
  11. logger.info("max size: %d" % partition.get_max_size())
  12. partition = self.get_best_partition()
  13. logger.info("best partition: %d" % partition.get_partition_no())
  14. if partition.get_load() >= self.config.get_partition_upper_load_factor():
  15. self.expand_pool_for_partition(partition, self.config.get_partition_increase_step())
  16. logger.debug("partition avail size: %d" % partition.get_avail_size())
  17. if partition.get_avail_size() == 0:
  18. self.cond.wait()
  19. tid = partition.take()
  20. thread = self.object_pool[tid]
  21. del self.object_pool[tid]
  22. #update access time
  23. thread.set_atime(time.time())
  24. partition.increase_active_thread_count()
  25. logger.debug("active thread count after get: %d" % self.get_active_thread_count())
  26. return thread
  27. finally:
  28. self.cond.release()
  1. def _return(self,thread):
  2. self.cond.acquire()
  3. logger.info("return back...")
  4. try:
  5. if (time.time() - thread.get_atime()) > self.config.get_timeout():
  6. logger.info("destroy thread #%d" % thread.ident)
  7. self.factory.destroy(thread)
  8. else:
  9. self.object_pool[thread.tid] = thread
  10. self.partitions[thread.get_partition()].put(thread.tid)
  11. self.partitions[thread.get_partition()].decrease_active_thread_count()
  12. logger.info("return thread #%d back to pool on partition #%d" % (thread.ident,thread.get_partition()))
  13. for partition in self.partitions:
  14. logger.info("partition #%d status:" % partition.get_partition_no())
  15. logger.info("current load: %.2f" % partition.get_load())
  16. logger.info("used size: %d" % partition.get_used_size())
  17. logger.info("max size: %d" % partition.get_max_size())
  18. logger.debug("active thread count after return: %d" % self.get_active_thread_count())
  19. self.cond.notify()
  20. except Exception,e:
  21. print e
  22. #if return error, we should kill this thread
  23. self.factory.destroy(thread)
  24. finally:
  25. self.cond.release()

補充和修訂

今天后面測試的時候發現在抓了8000條左右後出現了阻塞,仍然是在池中的線程用完的時候,大家不知道從上面的代碼中發現問題沒有?
原因在於上面的代碼雖然有wait和notify,但是如果return的線程並不是best partition中的線程,那麼partition.take依然會阻塞,這就是問題所在!??確實很容易看花眼增加一個get_avail_parition函數,查找pool中第一個可用的parition,然後從這個分區取

  1. ​def get_avail_partition(self):
  2. for partition in self.partitions:
  3. if partition.get_avail_size()>0:
  4. return partition
  1. if partition.get_avail_size()==0:
  2. self.cond.wait()
  3. ** partition = self.get_avail_partition()**
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章