国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學(xué)院 > 開發(fā)設(shè)計(jì) > 正文

多線程的Python教程--“貪吃蛇”

2019-11-14 17:28:54
字體:
供稿:網(wǎng)友

  本指南的里代碼可以在這里下載:  threadworms.py ,或者從  GitHub。代碼需要  Python 3 或 Python 2 ,同時(shí)也需要安裝  Pygame 。

點(diǎn)擊查看大版本圖片

  這是一篇為初學(xué)者準(zhǔn)備的關(guān)于  線程 和Python中的多線程編程的指南。 如果你有一些  類(class)的基礎(chǔ)知識 (什么是類,如何定義方法(method),還有方法總是將self作為他的第一個(gè)參數(shù),子類是什么以及子類如何從父類繼承一個(gè)方法,等等)這篇指南會(huì)對你有所幫助。  這里有一篇較為深入地介紹類(class)的指南。

  我們用到的例子是  “貪吃蛇” 的克隆,它有很多條在一個(gè)格子狀的區(qū)域前行的蠕蟲,每一條蟲子在一個(gè)單獨(dú)的線程里運(yùn)行。

  如果你知道線程相關(guān)的知識,那就跳過這一章節(jié),看看線程在Python中如何使用。

  當(dāng)你運(yùn)行一個(gè)普通的Python程序時(shí),這個(gè)程序從第一行開始,一行接一行的的執(zhí)行。循環(huán)和函數(shù)可能讓程序上下跳轉(zhuǎn),但是給定一行代碼的位置,你可以輕易地找到下一行從哪里執(zhí)行。你可以把一根手指指到你的.py文件中,一行一行的追蹤你的程序執(zhí)行到哪里了。這就是單線程編程(single-threaded PRogramming)。

  然而,使用多個(gè)線程就像將第二跟手指放到屏幕上。每個(gè)手指還是像之前那樣移動(dòng),但是它們現(xiàn)在是同時(shí)在移動(dòng)。

  但是事實(shí)上,它們并不是同時(shí)的。你的手指在交替著移動(dòng)。擁有多核心(multicore)的處理器可以真正意義上的同時(shí)執(zhí)行兩條指令,但是Python程序有一個(gè)叫做  GIL (全局解釋器鎖 global interpreter lock) 東西,它會(huì)限制Python程序單核執(zhí)行。

 線程是什么?為什么線程很有用?

  Python的解釋器會(huì)一會(huì)兒執(zhí)行一個(gè)線程,一會(huì)兒執(zhí)行另一個(gè)線程。但是這切換的速度如此之快,快的讓你根本無法察覺,以至于這些線程看起來像是同時(shí)執(zhí)行。

  你可以在你的Python程序中開幾十或者幾百個(gè)線程(那真是好多的手指)。這并不能讓你的程序快上幾十上百倍(事實(shí)上這些線程還是在使用同一個(gè)CPU),但是它能讓你的程序更強(qiáng)大,更高效。

  舉個(gè)例子,你要寫個(gè)函數(shù),這個(gè)函數(shù)會(huì)下載一個(gè)內(nèi)容全是名字的文件,然后將這個(gè)文件的內(nèi)容排序,然后將排序好的內(nèi)容存為另一個(gè)文件。如果這里有上百個(gè)這樣的文件,那么你可能會(huì)在一個(gè)循環(huán)中調(diào)用這個(gè)函數(shù)來處理每個(gè)文件:下載,排序,保存,下載,排序,保存,下載,排序,保存...

  這三個(gè)步驟用到了你電腦上的不同資源:下載用到了網(wǎng)絡(luò),排序用到了CPU,保存文件用到了硬盤。同時(shí),這三個(gè)操作都可能被延緩。例如,你下在文件的服務(wù)器可能很慢,或者你的帶寬很小。

  這種情況先,使用多個(gè)線程,每個(gè)線程處理一個(gè)文件是一個(gè)比較明智的選擇。這不僅能更好的利用你的帶寬,而且當(dāng)你的CPU工作的時(shí)候,網(wǎng)絡(luò)也在工作。這將更有效的利用你的電腦。

 是什么讓多線程編程那么棘手?

  當(dāng)然,在上面的例子,每個(gè)線程只做它自己獨(dú)立的事情也不需要去和其他線程通信或同步任何東西。你可以只編寫簡單的下載-排序-寫入程序的單線程版本同時(shí)獨(dú)立地運(yùn)行程序上百遍。(盡管它可能在每次打字和點(diǎn)擊來運(yùn)行每個(gè)程序來下載不同文件的時(shí)候有點(diǎn)痛苦。)

  大多數(shù)多線程程序共享訪問相同的變量,但這就是棘手的東西。

(來自  Brad Montgomery的圖片)

  這里是一個(gè)常用的比喻:告訴你有兩個(gè)售票機(jī)器人。它們的任務(wù)很簡單:

  1. 詢問消費(fèi)者要哪個(gè)位置。
  2. 檢查列表看下座位是不是可以用。
  3. 獲取該座位的票。
  4. 從列表上移出改座位。

  一個(gè)顧客問機(jī)器A要42號座位的票。機(jī)器A從列表中檢查和發(fā)現(xiàn)座位可以用,因此它獲取到那張票。但在機(jī)器A能從列表中刪除改座位的前,機(jī)器B被不同顧客詢問42號座位。機(jī)器B檢查列表也看到了座位仍然可以用,所以它嘗試獲取到這個(gè)座位的票。但是機(jī)器B不能找到42號座位的票。這計(jì)算不了了,同時(shí)機(jī)器B的電子大腦也爆炸了。機(jī)器A在后來把42號座位的票從列表上刪除了。

  上面的問題會(huì)發(fā)生是因?yàn)闄C(jī)關(guān)兩個(gè)機(jī)器人(或者說,兩個(gè)線程)都在獨(dú)立執(zhí)行,他們兩者都在讀寫一個(gè)共享的列表(或者說,一個(gè)變量)。你的程序可能很難去修復(fù)這種很難重現(xiàn)的bug,因?yàn)镻ython的線程執(zhí)行切換具有 非確定性,那就是,程序每次運(yùn)行都在做不同的東西。我們不習(xí)慣變量里的數(shù)據(jù)“魔術(shù)地”從一行轉(zhuǎn)到下一個(gè)僅僅是因?yàn)榫€程在他們之間執(zhí)行。

  當(dāng)從一個(gè)線程的執(zhí)行切換到另外一個(gè)線程,這就是上下文切換。

  這也存在死鎖的問題,通常用 哲學(xué)家就餐問題的比喻來解釋。五個(gè)哲學(xué)家圍坐一個(gè)桌子吃意大利面條,但需要兩個(gè)叉子。在每個(gè)哲學(xué)家之間有一個(gè)叉子(總共有5個(gè))。哲學(xué)家用這個(gè)方法來吃面條:

  1. 理性地思考一會(huì)兒。
  2. 拿起你左邊的叉子。
  3. 等待你右邊的叉子能用。
  4. 拿起右邊的叉子。
  5. 吃面條。
  6. 放下叉子。
  7. 跳到步驟1。

  從實(shí)際上他們會(huì)和旁邊的人共享叉子(我不喜歡),這方法看起來似乎能有效。但馬上或者稍后桌子上每個(gè)人最后都會(huì)拿著左邊的叉子在手擋同時(shí)等待右邊的叉子。但因?yàn)槊總€(gè)人都拿著他們旁邊的人等待的叉子同時(shí)也不會(huì)在他們吃之前放下他們,這些哲學(xué)家就在一個(gè)死鎖狀態(tài)。他們會(huì)拿著左邊的叉子在手上又永遠(yuǎn)不會(huì)拿到右邊的叉子,所以他們永遠(yuǎn)不會(huì)吃到面條也用戶不會(huì)放下他們左手上的叉子。哲學(xué)家都要餓死了(除了伏爾泰,它實(shí)際上是個(gè)機(jī)器人。沒有意大利面條,他的電子大腦會(huì)爆炸)

  還存在一種被稱為 活鎖的情況。當(dāng)這種情況發(fā)生時(shí),所有的線程都讓出資源,導(dǎo)致任務(wù)不能繼續(xù)進(jìn)行下去。就像在大廳里迎面走近的兩個(gè)人,他們都站到一邊,等待對方先過去,結(jié)果兩個(gè)人都卡住了。然后他們又同時(shí)試圖走到對面,又互相阻礙了對方。他們持續(xù)地這樣讓開-走近,直到他們都筋疲力盡。 

  在多線程編程中,還可能存在其他一些問題,比如饑餓(不是真的肚子餓的問題,只是大家都這么叫它)。這些問題在計(jì)算機(jī)科學(xué)中普遍歸類于"  

  鎖

  在多線程編程中,一個(gè)防范bug的辦法是使用鎖。在一個(gè)線程讀取或者修改該某個(gè)共享變量前,它先試圖獲得一個(gè)鎖。如果獲得了這個(gè)鎖,這個(gè)線程將繼續(xù)對這個(gè)共享變量進(jìn)行讀寫。反之,它將一直等待直到這個(gè)鎖再次可用。

  一旦完成對共享變量的操作,線程就會(huì)“釋放”這個(gè)鎖。這樣其他等待這個(gè)鎖的線程就能獲取它了。

  回到售票機(jī)器人的比喻。一個(gè)機(jī)器人把座位列表拿起來 (這個(gè)列表就是鎖),檢查后發(fā)現(xiàn)客戶要求的座位還在,于是把相應(yīng)的票取出來,把這個(gè)座位從列表中刪去。最后機(jī)器人把列表放回去的動(dòng)作,就相當(dāng)于“釋放了這個(gè)鎖“。如果另一個(gè)機(jī)器人需要查看座位列表但列表不在,它會(huì)一直等待直到座位列表再次可用。

  寫代碼時(shí),如果忘記對鎖進(jìn)行釋放,就可能引入bug。這將導(dǎo)致死鎖情況的發(fā)生,因?yàn)榱硗庖粋€(gè)等待該鎖釋放的線程會(huì)一直掛在那里無事可做。 

  Python中的線程

  OK,現(xiàn)在讓我們來寫一段python程序來說明如何使用線程和鎖。這段程序基于一個(gè)貪吃蛇游戲,是我在拙著《 Making Games with Python & Pygame》 

  這篇教程的代碼可以從此處下載: threadworms.py 或者 GitHub。這份代碼兼容 Python2和Python3, 另外運(yùn)行該代碼需要安裝 Pygame.

  這里是上述在我們的threadworms.py程序里線程相關(guān)的代碼:

1
import threading

  Python的線程庫名為threading的模塊,所以首先要導(dǎo)入這個(gè)模塊。

1
GRID_LOCK = threading.Lock()

  在threading模塊里的Lock類有acquire()和release()方法。我們會(huì)新建一個(gè)Lock對象和把它存放在名為GRID_LOCK的全局變量里。(因?yàn)轭愃凭W(wǎng)格的屏幕和被單元占據(jù)的狀態(tài)值會(huì)存儲(chǔ)在名為GRID的全局變量里。這兩種方式有點(diǎn)意外。)

1
2
# A global variable that the Worm threads check to see if they should exit.
WORMS_RUNNING = True

  我沒的WORMS_RUNNING全局變量通常由worm線程來檢查是否應(yīng)該退出。調(diào)用sys.exit()不會(huì)停止程序,因?yàn)樗皇峭顺稣{(diào)用它的線程。只要有其他線程仍然在運(yùn)行程序還會(huì)繼續(xù)。在我們程序里的主線程(負(fù)責(zé)Pygame渲染和時(shí)間處理)會(huì)在它調(diào)用pygame.quit()和sys.exit()前設(shè)置WORMS_RUNNING為False,直到實(shí)際最后的線程退出然后程序終止,它就會(huì)退出。

1
2
3
4
class Worm(threading.Thread):
    def __ init__(self, name='Worm', maxsize=None, color=None, speed=None):
        threading.Thread.__init__(self)
        self.name = name

  線程代碼必需充Thread類的子類開始(在threading模塊里的)。我們的線程子類會(huì)名為Worm,因?yàn)樗?fù)責(zé)控制蟲子。但由于我們的Worm類會(huì)先使用我們需要調(diào)用threading.Thread的___init__()方法,所以你不需要一個(gè)__init()__函數(shù)。同樣也可以選擇重載該命名方法。我們的__init__()函數(shù)使用字符串"Worm'作為默認(rèn),但我們能夠提供每個(gè)線程一個(gè)獨(dú)立的名字。Python會(huì)在線程崩潰的時(shí)候在錯(cuò)誤信息里顯示線程的名字。 

1
2
3
GRID_LOCK.acquire()
# ...some code that reads or modifies GRID...
GRID_LOCK.release()

  在我們讀獲取修改GRID變量里的值前,線程代碼應(yīng)該去嘗試申請鎖。如果鎖不可用,方法對acquire()調(diào)用不會(huì)返回同時(shí)直到鎖能用前都會(huì)“阻塞”。線程在這種情況下會(huì)展廳。這樣,我們就知道在acquire()后的代碼的調(diào)用只會(huì)在線程申請到鎖后才發(fā)生。

  在一段代碼中獲取和釋放一個(gè)鎖,可以保證當(dāng)前線程在執(zhí)行這段代碼的時(shí)候,其他線程不會(huì)執(zhí)行這段代碼。這會(huì)讓這段代碼變成“原子的”,因?yàn)檫@段代碼總會(huì)被當(dāng)成一個(gè)整體。

  在對GRID變量的操作完成之后,使用release方法釋放這個(gè)鎖:

1
2
def run(self):
    # 這里是線程代碼

  當(dāng)Worm類(是一個(gè)threading.Thread的字類)的start()方法被調(diào)用時(shí),一個(gè)線程開始執(zhí)行。我們不需要自己實(shí)現(xiàn)start()方法,因?yàn)樗菑膖hreading.Thread中繼承的。 當(dāng)調(diào)用start()時(shí),會(huì)創(chuàng)建一個(gè)新的線程,這個(gè)新的線程會(huì)執(zhí)行run()方法中的代碼。不要直接調(diào)用run()方法,這樣不會(huì)創(chuàng)建一個(gè)新的線程。

  需要明白的東西:使用start()新開一個(gè)線程,但是這個(gè)線程會(huì)執(zhí)行run()里面的代碼。我們不需要自己實(shí)現(xiàn)start(),因?yàn)樗菑?threading.Thread中繼承的。既然線程要執(zhí)行run()中的代碼,我們要自己實(shí)現(xiàn)run()。

  當(dāng)run()的調(diào)用結(jié)束時(shí), (或者sys.exit()這個(gè)函數(shù)在這個(gè)線程中被調(diào)用),這個(gè)線程會(huì)被銷毀。在一個(gè)程序結(jié)束之前,它里面所有的線程需要被銷毀。只要這個(gè)程序中有一個(gè)還在運(yùn)行的線程,這個(gè)程序就不會(huì)結(jié)束。

  所以,當(dāng)start()被調(diào)用時(shí),就是你將一根新的手指放到run()中開始追蹤代碼的時(shí)候。你的第一根手指在執(zhí)行完start()后會(huì)繼續(xù)追蹤代碼。

 一個(gè)簡單的多線程例子

  在我們進(jìn)入Threadworm代碼前,先看下一個(gè)廢棄了的簡單多線程程序:

1
2
3
4
5
6
7
8
9
10
11
12
import threading
TOTAL = 0
class CountThread(threading.Thread):
    def run(self):
        global TOTAL
        for i in range(100):
            TOTAL = TOTAL + 1
        print('%s/n' % (TOTAL))
a = CountThread()
b = CountThread()
a.start()
b.start()

  這個(gè)程序定義一個(gè)叫做CountThread的新類。當(dāng)一個(gè)CountThread對象的start()方法被調(diào)用,一個(gè)會(huì)循環(huán)100次同時(shí)在每次循環(huán)迭代中為TOTAL全局變量的值加1的新的線程會(huì)被創(chuàng)建(在變量之間共享的)。

  一旦我們創(chuàng)建兩個(gè)CountThread對象,無論哪個(gè)完成了都會(huì)顯示200.每個(gè)線程為TOTAL增加100,并且有兩個(gè)線程。當(dāng)我們運(yùn)行這個(gè)程序的時(shí)候,我們會(huì)看到:

1
2
100
200

  因?yàn)榈谝粋€(gè)數(shù)字是100,我們能說出一個(gè)線程在上下文切換發(fā)生前在整個(gè)循環(huán)里發(fā)生了什么。

  然而,如果我們把范圍從100改到100000,我可能期待第二個(gè)數(shù)字是200000,一旦每個(gè)線程為TOTAL增加100000同時(shí)有兩個(gè)線程。但當(dāng)我們運(yùn)行程序的時(shí)候,一些像這樣的東西會(huì)出現(xiàn)(你的數(shù)字可能有點(diǎn)不同):

1
2
143294
149129

  第二個(gè)數(shù)字不是200000!它比實(shí)際數(shù)字要小。這樣會(huì)發(fā)生的原因是因?yàn)槲覀儧]有在代碼讀寫TOTAL變量周圍使用鎖,而它是在多線程中共享的。

  看這行:

1
TOTAL = TOTAL + 1

  如果TOTAL設(shè)置為99,然后你會(huì)認(rèn)為TOTAL+1等于99+1然后就是100了,然后100作為新的值存入到TOTAL里。然后在下一次迭代,TOTAL+1會(huì)是100+1或101,它都會(huì)作為新的值存到TOTAL里。

  但在說TOTAL+1等于99+1時(shí),執(zhí)行切換到另外一個(gè)線程,它也是執(zhí)行TOTAL=TOTAL+1那一行。在TOTAL里的值還是99,所以TOTAL+1在這第二個(gè)線程會(huì)等價(jià)于99+1。

  然后,另外一個(gè)上下文切換發(fā)生的時(shí)候又回到第一個(gè)線程TOTAL=99+1的執(zhí)行到中間的地方。整數(shù)100會(huì)分配到TOTAL。現(xiàn)在執(zhí)行又切換到第二個(gè)線程。

  在第二個(gè)線程,TOTAL=99+1大概要執(zhí)行了。即使現(xiàn)在TOTAL是100,在第二個(gè)線程里的TOTAL+1這里已經(jīng)等于是99+1了。所以第二個(gè)線程最終也把整數(shù)100分配到TOTAL。即使TOTAL=TOTAL+1被執(zhí)行了兩次(每次又一個(gè)線程執(zhí)行),但TOTAL的真實(shí)值只是加了1!

  這問題是由于代碼的TOTAL=TOTAL+1這行不是原子的。上下文切換切換可以剛好在這行執(zhí)行的中間發(fā)生。我們需要在代碼周圍加上鎖讓它成為一個(gè)原子操作。

  新的代碼修復(fù)了這個(gè)問題:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import threading
TOTAL = 0
MY_LOCK = threading.Lock()
class CountThread(threading.Thread):
    def run(self):
        global TOTAL
        for i in range(100000):
            MY_LOCK.acquire()
            TOTAL = TOTAL + 1
            MY_LOCK.release()
        print('%s/n' % (TOTAL))
a = CountThread()
b = CountThread()
a.start()
b.start()

  當(dāng)我們運(yùn)行這段代碼時(shí)候,下面就是輸出結(jié)果了(你的第一個(gè)數(shù)字可能有一點(diǎn)不同):

1
2
199083
200000

  第二個(gè)數(shù)字是20000告訴了我們TOTAL=TOTAL+1這行正確在它運(yùn)行的200,000次里執(zhí)行。

 解釋Threadworms程序

  我打算用程序的 threadworms_nocomments.py版本,因?yàn)樗鼪]有冗長的注釋在里面。在每行的開頭都包含了行號(它們不是實(shí)際Python源代碼的一部分)。我跳過很多注釋部分因?yàn)樗鼈兌际且恍┳晕医忉尅T谙旅娲a不需要你真正了解Pygame。Pygame只是負(fù)責(zé)創(chuàng)建窗口和畫線和方塊。

  一件要了解的事情是Pygame用一個(gè)三個(gè)整數(shù)的元組來代表顏色。每個(gè)這些整數(shù)的范圍是從0到255并且代表了RGB(紅-綠-藍(lán))顏色的值。所以(0,0,0)是黑和(255,255,255)是白,同時(shí)(255,0,0)是紅和(255,0,255)是紫等。

1
2
3
4
5
6
7
8
9
9. import random, pygame, sys, threading
10. from pygame.locals import *
11.
12. # Setting up constants
13. NUM_WORMS = 24  # the number of worms in the grid
14. FPS = 30        # frames per second that the program runs
15. CELL_SIZE = 20  # how many pixels wide and high each "cell" in the grid is
16. CELLS_WIDE = 32 # how many cells wide the grid is
17. CELLS_HIGH = 24 # how many cells high the grid is

  代碼的最上部分導(dǎo)入一些我們程序需要的模塊同時(shí)定義一些常量值。會(huì)很容易地修改這些常量。增加或減少FPS的值不會(huì)影響到蟲子跑得有多快,它只是改變屏幕刷新頻率。如果你把這個(gè)值設(shè)置得很低,它看起來這些蟲子都會(huì)瞬間移動(dòng),因?yàn)樗鼈冊诿看纹聊凰⑿碌臅r(shí)候移動(dòng)了多個(gè)位置。

  CELL_SIZE代表屏幕上的網(wǎng)格的每個(gè)方塊有多大(像素)。如果你想改變單元的數(shù)量,就修改CELLS_WIDE和CELLS_HIGH常量。

1
2
3
20. GRID = []
21. for x in range(CELLS_WIDE):
22.     GRID.append([None] * CELLS_HIGH)

  全局變量GRID會(huì)包含網(wǎng)格狀態(tài)跟蹤數(shù)據(jù)。它是一些簡單的列表所以GRID[x][y]會(huì)指向X和Y坐標(biāo)的單元。(在編程里,(0,0)遠(yuǎn)點(diǎn)在屏幕左上方。X增加會(huì)往右移動(dòng)(就像在數(shù)學(xué)課里)但Y增加會(huì)往下移動(dòng)。)

  如果GRID[x][y]設(shè)置為None,然后單元就不會(huì)被占據(jù)。否則的話,GRID[x][y]會(huì)設(shè)置為一個(gè)RGB三元組。(這個(gè)信息對在屏幕上畫網(wǎng)格的時(shí)候有用。)

1
24. GRID_LOCK = threading.Lock() # pun was not intended

  第24行創(chuàng)建一個(gè)Lock對象,我們的線程代碼會(huì)在讀或修改GRID的時(shí)候申請和釋放這個(gè)鎖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
26. # Constants for some colors.
 27. #             R    G    B
 28. WHITE     = (255, 255, 255)
 29. BLACK     = 0,   0,   0)
 30. DARKGRAY  = ( 404040)
 31. BGCOLOR = BLACK             # color to use for the background of the grid
 32. GRID_LINES_COLOR = DARKGRAY # color to use for the lines of the grid
RGB tuples are kind of hard to read, so I usually set up some constants for them.
 33.
 34. # Calculate total pixels wide and high that the full window is
 35. WINDOWWIDTH = CELL_SIZE * CELLS_WIDE
 36. WINDOWHEIGHT = CELL_SIZE * CELLS_HIGH
 37.
 38. UP = 'up'
 39. DOWN = 'down'
 40. LEFT = 'left'
 41. RIGHT = 'right'

  還有更多的簡單常量。我使用如DOWN和RIGHT這些常量代替字符串“down”和“right”因?yàn)槿绻义e(cuò)誤使用使用常量(如DOWN)然后Python會(huì)直接由于一個(gè)NameError異常而崩潰。這比我用錯(cuò)誤的值如“down”更好,它不會(huì)直接程序崩潰但以后會(huì)導(dǎo)致一些bug,讓它更難去跟蹤。

1
2
43. HEAD = 0
44. BUTT = -1 # negative indexes count from the end, so -1 will always be the last index

  每個(gè)蟲子會(huì)用一個(gè)像{'x':42,'y':7}的字典列表來表示。每一個(gè)字典都代表了蟲子身體的一部分。列表前面的字典(索引為0)是頭部,而在最后的字典(索引-1,使用Python的負(fù)數(shù)索引來從后面開始計(jì)數(shù))是蟲子的屁股。

  (在計(jì)算機(jī)科學(xué)里,“頭”通常指向列表或隊(duì)列的第一個(gè)元素,而“尾”指向任何一個(gè)在頭后面的元素。所以我使用“屁股”來指向最后一個(gè)元素。我同樣有點(diǎn)笨。)

  上面的蟲子能用這樣的列表能表示:[{'x': 7, 'y': 2}, {'x': 7, 'y': 3}, {'x': 7, 'y': 4}, {'x': 8, 'y': 4}, {'x': 9, 'y': 4}, {'x': 10, 'y': 4}, {'x': 11, 'y': 4}, {'x': 11, 'y': 3}, {'x': 11, 'y': 2}]

1
2
46. # A global variable that the Worm threads check to see if they should exit.
47. WORMS_RUNNING = True

  隨著一個(gè)線程在運(yùn)行,程序會(huì)繼續(xù)地執(zhí)行。負(fù)責(zé)渲染屏幕的主線程會(huì)檢查用戶什么時(shí)候點(diǎn)擊了窗口上的關(guān)閉按鈕或者按ESC鍵,所以它需要一個(gè)方法來告訴蟲子線程退出。我們會(huì)編寫蟲子線程代碼來定期檢查WORMS_RUNNING。如果WORMS_RUNNING是False的,線程就會(huì)終止自己。

1
2
49. class Worm(threading.Thread): # "Thread" is a class in the "threading" module.
50.     def __init__(self, name='Worm', maxsize=None, color=None, speed=None):

  這里是我們的Worm類,它是threading.Thread的子類。每個(gè)蟲子都有一個(gè)名字(在蟲子碰撞的時(shí)候會(huì)顯示,幫助我們分辨出是那個(gè)線程崩潰),一個(gè)大小,顏色和速度。都提供了默認(rèn)值,但如果喜歡我們能夠指定這些屬性的值。

1
56.         threading.Thread.__init__(self) # since we are overriding the Thread class, we need to first call its __init__() method.

  一旦我們重載了__init__()方法,我們需要調(diào)用父類的__init__()方法以便于它能夠初始化所有線程內(nèi)容。(我們不需要知道它怎么工作,只要記住調(diào)用它。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
57.
58.         self.name = name
59.
60.         # Set the maxsize to the parameter, or to a random maxsize.
61.         if maxsize is None:
62.             self.maxsize = random.randint(4, 10)
63.
64.             # Have a small chance of a super long worm.
65.             if random.randint(0,4) == 0:
66.                 self.maxsize += random.randint(10, 20)
67.         else:
68.             self.maxsize = maxsize
69.
70.         # Set the color to the parameter, or to a random color.
71.         if color is None:
72.             self.color = (random.randint(60, 255), random.randint(60, 255), random.randint(60, 255))
73.         else:
74.             self.color = color
75.
76.         # Set the speed to the parameter, or to a random number.
77.         if speed is None:
78.             self.speed = random.randint(20, 500) # wait time before movements will be between 0.02 and 0.5 seconds
79.         else:
80.             self.speed = speed

  上面的代碼設(shè)置一個(gè)有隨機(jī)大小、顏色和速度的蟲子,除非參數(shù)指定了值,它都用默認(rèn)的值。

1
2
3
4
5
6
7
8
9
10
11
82.         GRID_LOCK.acquire() # block until this thread can acquire the lock
83.
84.         while True:
85.             startx = random.randint(0, CELLS_WIDE - 1)
86.             starty = random.randint(0, CELLS_HIGH - 1)
87.             if GRID[startx][starty] is None:
88.                 break # we've found an unoccupied cell in the grid
89.
90.         GRID[startx][starty] = self.color # modify the shared data structure
91.
92.         GRID_LOCK.release()

  我們需要決定一個(gè)蟲子的隨機(jī)開始的位置。要讓這個(gè)簡單點(diǎn),所有蟲子都從一個(gè)身體部分來開始同時(shí)增長直到他們到達(dá)最大值。但我們需要確保網(wǎng)格上的隨機(jī)位置還沒被占據(jù)。這取決對GRID全局變量的讀和修改,所以我們需要在做之前申請和釋放GRID_LOCK鎖。

  (備注,你可能會(huì)好奇為什么我們不用把“global GRID”這行放在方法的開始。GRID是一個(gè)全局變量同時(shí)我們要在這個(gè)方法里修改它,并且沒有一個(gè)Python要去考慮的全局的語句,只有在有一個(gè)局部變量和GRID全局變量同名的時(shí)候,它才需要。但是,如果你看仔細(xì)點(diǎn),我們只是改變GRIDlist列表里的值,但永遠(yuǎn)不是GRID本身的值。那就是說,我們像這樣“GRID[startx][starty]=self.color”的代碼永遠(yuǎn)不會(huì)“GRID=someValue”。因?yàn)槲覀儾粫?huì)實(shí)際改變GRID本身,Python會(huì)考慮在方法里用GRID的名字來指向全局變量GRID。)

  我們繼續(xù)循環(huán)知道我們找到一個(gè)沒被占據(jù)的單元,然后標(biāo)記這個(gè)單元現(xiàn)在被占據(jù)了。之后,我們完成讀和修改GRID,所以就釋放GRID_LOCK鎖。

  (另外一個(gè)備注,如果網(wǎng)格里沒有可用的單元,這個(gè)循環(huán)會(huì)一直繼續(xù)下去同時(shí)線程會(huì)“hang(掛死)”。因?yàn)槠渌€程會(huì)繼續(xù)運(yùn)行,你可能不會(huì)注意到這個(gè)問題。新的蟲子不會(huì)創(chuàng)建但是程序剩下的蟲子還會(huì)繼續(xù)正常運(yùn)行。然而,當(dāng)你嘗試退出的時(shí)候,因?yàn)閽焖赖木€程永遠(yuǎn)不會(huì)去檢查WORMS_RUNNING來知道自己應(yīng)該退出同時(shí)程序會(huì)拒絕終止。你就得通過操作系統(tǒng)來強(qiáng)制關(guān)閉程序。只要確定在沒更多空間時(shí)不創(chuàng)建更多蟲子。)

1
2
96.         self.body = [{'x': startx, 'y': starty}]
97.         self.direction = random.choice((UP, DOWN, LEFT, RIGHT))

  開始的身體部分會(huì)加入到body成員變量。body成員變量是一個(gè)所有身體部分的位置的列表。蟲子頭部的方向存放在direction成員變量里。

  技術(shù)上來說,因?yàn)楝F(xiàn)在蟲子只有身體的一部分,它的列表的第一項(xiàng)既是最后一項(xiàng),蟲子的頭和屁股是一樣的。

1
2
3
4
100.     def run(self):
101.         while True:
102.             if not WORMS_RUNNING:
103.                 return # A thread terminates when run() returns.

  run()方法在蟲子的start()方法被調(diào)用的時(shí)候就會(huì)被調(diào)用。在run()里的代碼在一個(gè)全新的線程里運(yùn)行。我們會(huì)用一個(gè)無限循環(huán)來讓蟲子持續(xù)在網(wǎng)格上移動(dòng)。在每個(gè)循環(huán)迭代上我們要做的第一件事是檢查是否WORMS_RUNNING被設(shè)置為False,如果是這樣的話,我們應(yīng)該從這個(gè)方法返回。

  如果我們從線程里調(diào)用sys.exit()或則當(dāng)run()方法返回,線程就會(huì)終止自己。

1
2
3
105.             # Randomly decide to change direction
106.             if random.randint(0, 100) < 20: # 20% to change direction
107.                 self.direction = random.choice((UP, DOWN, LEFT, RIGHT))

  在移動(dòng)的每一步上,有20%的機(jī)會(huì)蟲子會(huì)隨機(jī)地改變方向。(盡管新的方向和當(dāng)前方向相同。但我快速地編寫這段代碼。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
109.             GRID_LOCK.acquire() # don't return (that is, block) until this thread can acquire the lock
110.
111.             nextx, nexty = self.getNextPosition()
112.             if nextx in (-1, CELLS_WIDE) or nexty in (-1, CELLS_HIGH) or GRID[nextx][nexty] is not None:
113.                 # The space the worm is heading towards is taken, so find a new direction.
114.                 self.direction = self.getNewDirection()
115.
116.                 if self.direction is None:
117.                     # No places to move, so try reversing our worm.
118.                     self.body.reverse() # Now the head is the butt and the butt is the head. Magic!
119.                     self.direction = self.getNewDirection()
120.
121.                 if self.direction is not None:
122.                     # It is possible to move in some direction, so reask for the next postion.
123.                     nextx, nexty = self.getNextPosition()
124.
125.             if self.direction is not None:
126.                 # Space on the grid is free, so move there.
127.                 GRID[nextx][nexty] = self.color # update the GRID state
128.                 self.body.insert(0, {'x': nextx, 'y': nexty}) # update this worm's own state
129.
130.                 # Check if we've grown too long, and cut off tail if we have.
131.                 # This gives the illusion of the worm moving.
132.                 if len(self.body) > self.maxsize:
133.                     GRID[self.body[BUTT]['x']][self.body[BUTT]['y']] = None # update the GRID state
134.                     del self.body[BUTT] # update this worm's own state (heh heh, worm butt)
135.             else:
136.                 self.direction = random.choice((UP, DOWN, LEFT, RIGHT)) # can't move, so just do nothing for now but set a new random direction
137.
138.             GRID_LOCK.release()

  上面的代碼處理蟲子移動(dòng)一個(gè)位置。一旦這涉及到讀和修改GRID,我們需要申請GRID_LOCK的鎖。本來,蟲子會(huì)嘗試移動(dòng)到一個(gè)它的方向成員變量描述的方向上的一個(gè)位置。如果這個(gè)單元是網(wǎng)格的便捷或者已經(jīng)被占據(jù),然后蟲子會(huì)改變它的方向。如果蟲子在各個(gè)方向都被阻擋了,它就會(huì)反轉(zhuǎn)自己來讓屁股成為頭和頭又變?yōu)槠ü伞H绻x子還是不能在任何方向上移動(dòng),然后它會(huì)停止原地不動(dòng)。 

1
140.            pygame.time.wait(self.speed)

  在蟲子移動(dòng)一個(gè)位置之后(或至少去嘗試),我們會(huì)讓線程睡眠。Pygame有個(gè)叫做wait()的函數(shù)做的和time.sleep()一樣的東西,除了wait()的參數(shù)用毫秒的整數(shù)來代替秒。

  Pygame的pygame.time.wait()和Python標(biāo)準(zhǔn)庫的time.time()函數(shù)(以及pygame的tick()方法)都足夠聰明地去告訴操作系統(tǒng)讓該線程睡眠一段時(shí)間同時(shí)去運(yùn)行其他替代的線程。當(dāng)然,當(dāng)操作系統(tǒng)能夠在任何使用中斷我們的線程來處理另外的線程,調(diào)用wait()或sleep()是顯式地說,“去吧,不要運(yùn)行這個(gè)線程X毫秒。”

  這樣編寫“wait”代碼不能出現(xiàn):

1
2
3
startOfWait = time.time()
while time.time() - 5 > startOfWait:
    pass # do nothing for 5 seconds

  上面的代碼也實(shí)現(xiàn)了“在等待”,但是對于操作系統(tǒng)來說,你的線程看起來還在執(zhí)行代碼(幾時(shí)你的代碼沒有做什么只是在循環(huán)等待5秒過去)。這不是有效的因?yàn)闀r(shí)間耗費(fèi)在執(zhí)行上面的無意義的循環(huán)上,這些時(shí)間其實(shí)可以花在其他線程上的。

  當(dāng)然,如果所有的蟲子的線程都在睡眠,計(jì)算機(jī)會(huì)知道它能夠使用CPU執(zhí)行除我們的Python Threadworms腳本外的其他程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
143.     def getNextPosition(self):
144.         # Figure out the x and y of where the worm's head would be next, based
145.         # on the current position of its "head" and direction member.
146.
147.         if self.direction == UP:
148.             nextx = self.body[HEAD]['x']
149.             nexty = self.body[HEAD]['y'] - 1
150.         elif self.direction == DOWN:
151.             nextx = self.body[HEAD]['x']
152.             nexty = self.body[HEAD]['y'] + 1
153.         elif self.direction == LEFT:
154.             nextx = self.body[HEAD]['x'] - 1
155.             nexty = self.body[HEAD]['y']
156.         elif self.direction == RIGHT:
157.             nextx = self.body[HEAD]['x'] + 1
158.             nexty = self.body[HEAD]['y']
159.         else:
160.             assert False, 'Bad value for self.direction: %s' % self.direction
161.
162.         return nextx, nexty

  getNextPosition()指出了蟲子下次會(huì)去哪里,接著給出它的頭的位置和它要走向的方向。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
165.     def getNewDirection(self):
166.         x = self.body[HEAD]['x'] # syntactic sugar, makes the code below more readable
167.         y = self.body[HEAD]['y']
168.
169.         # Compile a list of possible directions the worm can move.
170.         newDirection = []
171.         if y - 1 not in (-1, CELLS_HIGH) and GRID[x][y - 1] is None:
172.             newDirection.append(UP)
173.         if y + 1 not in (-1, CELLS_HIGH) and GRID[x][y + 1] is None:
174.             newDirection.append(DOWN)
175.         if x - 1 not in (-1, CELLS_WIDE) and GRID[x - 1][y] is None:
176.             newDirection.append(LEFT)
177.         if x + 1 not in (-1, CELLS_WIDE) and GRID[x + 1][y] is None:
178.             newDirection.append(RIGHT)
179.
180.         if newDirection == []:
181.             return None # None is returned when there are no possible ways for the worm to move.
182.
183.         return random.choice(newDirection)

  getNewDirection()方法返回一個(gè)方向(UP,DOWN,LEFT或RIGHT的字符串中一個(gè))表示在網(wǎng)格里還沒被占據(jù)的一個(gè)單元。如果沒有允許的單元讓(蟲子)的頭移過去,方法就會(huì)返回None。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
185. def main():
186.     global FPSCLOCK, DISPLAYSURF
187.
188.     # Draw some walls on the grid
189.     squares = """
190. ...........................
191. ...........................
192. ...........................
193. .H..H..EEE..L....L.....OO..
194. .H..H..E....L....L....O..O.
195. .HHHH..EE...L....L....O..O.
196. .H..H..E....L....L....O..O.
197. .H..H..EEE..LLL..LLL...OO..
198. ...........................
199. .W.....W...OO...RRR..MM.MM.
200. .W.....W..O..O..R.R..M.M.M.
201. .W..W..W..O..O..RR...M.M.M.
202. .W..W..W..O..O..R.R..M...M.
203. ..WW.WW....OO...R.R..M...M.
204. ...........................
205. ...........................
206. """
207.     #setGridSquares(squares)

  setGridSquares()函數(shù)能夠用在網(wǎng)格上通過傳入一個(gè)多行的字符串來畫靜態(tài)區(qū)域。前一個(gè)字符代碼沒有改變,一個(gè)空的字符意味著“設(shè)置它為沒有被占據(jù)”然后其他字符會(huì)代表一個(gè)放置在網(wǎng)格上的靜態(tài)區(qū)域。如果你想看到“Hello worm”文本在區(qū)域里寫出來,你可以取消207行的備注。

1
2
3
4
5
209.     # Pygame window set up.
210.     pygame.init()
211.     FPSCLOCK = pygame.time.Clock()
212.     DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
213.     pygame.display.set_caption('Threadworms')

  這是一個(gè)Pygame標(biāo)準(zhǔn)的設(shè)置,用來為我們的程序創(chuàng)建一個(gè)窗口。

1
2
3
4
5
215.     # Create the worm objects.
216.     worms = [] # a list that contains all the worm objects
217.     for i in range(NUM_WORMS):
218.         worms.append(Worm())
219.         worms[-1].start() # Start the worm code in its own thread.

  這段代碼創(chuàng)建Worm對象然后通過調(diào)用start()方法創(chuàng)建它們的線程。在每個(gè)蟲子的run()方法里的代碼會(huì)在這時(shí)候開始運(yùn)行在一個(gè)隔離的線程里。

1
2
3
4
5
6
221.     while True: # main game loop
222.         handleEvents()
223.         drawGrid()
224.
225.         pygame.display.update()
226.         FPSCLOCK.tick(FPS)

  主要的游戲循環(huán)十分簡單。handleEvents()函數(shù)會(huì)檢查用戶是否終止了程序和drawGrid()函數(shù)會(huì)畫出網(wǎng)格線和單元到屏幕上。pygame.display.update()函數(shù)告訴窗口更新屏幕,在這之后tick()方法會(huì)暫停一定時(shí)間以達(dá)到在FPS里指定的幀率。

1
2
3
4
5
6
7
8
9
229. def handleEvents():
230.     # The only event we need to handle in this program is when it terminates.
231.     global WORMS_RUNNING
232.
233.     for event in pygame.event.get(): # event handling loop
234.         if (event.type == QUIT) or (event.type == KEYDOWN and event.key == K_ESCAPE):
235.             WORMS_RUNNING = False # Setting this to False tells the Worm threads to exit.
236.             pygame.quit()
237.             sys.exit()

  Pygame事件嫩告訴我們時(shí)候時(shí)候用戶按下了ESC鍵或者點(diǎn)擊關(guān)閉按鈕來關(guān)閉窗口。在這種情況下,我們想設(shè)置WORMS_RUNNING為False以便于線程會(huì)終止自己然后主線程關(guān)閉Pygame和退出。

1
2
3
4
5
6
7
240. def drawGrid():
241.     # Draw the grid lines.
242.     DISPLAYSURF.fill(BGCOLOR)
243.     for x in range(0, WINDOWWIDTH, CELL_SIZE): # draw vertical lines
244.         pygame.draw.line(DISPLAYSURF, GRID_LINES_COLOR, (x, 0), (x, WINDOWHEIGHT))
245.     for y in range(0, WINDOWHEIGHT, CELL_SIZE): # draw horizontal lines
246.         pygame.draw.line(DISPLAYSURF, GRID_LINES_COLOR, (0, y), (WINDOWWIDTH, y))

  這段代碼根據(jù)GRID里的值來畫出屏幕。但首先它會(huì)畫出網(wǎng)格線。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
248.     # The main thread that stays in the main loop (which calls drawGrid) also
249.     # needs to acquire the GRID_LOCK lock before modifying the GRID variable.
250.     GRID_LOCK.acquire()
251.
252.     for x in range(0, CELLS_WIDE):
253.         for y in range(0, CELLS_HIGH):
254.             if GRID[x][y] is None:
255.                 continue # No body segment at this cell to draw, so skip it
256.
257.             color = GRID[x][y] # modify the GRID data structure
258.
259.             # Draw the body segment on the screen
260.             darkerColor = (max(color[0] - 50, 0), max(color[1] - 50, 0), max(color[2] - 50, 0))
261.             pygame.draw.rect(DISPLAYSURF, darkerColor, (x * CELL_SIZE,     y * CELL_SIZE,     CELL_SIZE,     CELL_SIZE    ))
262.             pygame.draw.rect(DISPLAYSURF, color,       (x * CELL_SIZE + 4, y * CELL_SIZE + 4, CELL_SIZE - 8, CELL_SIZE - 8))
263.
264.     GRID_LOCK.release() # We're done messing with GRID, so release the lock.

  因?yàn)榇a讀GRID變量,我會(huì)首先申請GRID_LOCK鎖。如果一個(gè)單元被占據(jù)了(那就是,它會(huì)設(shè)置一個(gè)RGB元組數(shù)值到GRID變量),代碼會(huì)在該單元內(nèi)上色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
267. def setGridSquares(squares, color=(192, 192, 192)):
268.     # squares is set to a value like:
269.     # """
270.     # ......
271.     # ...XX.
272.     # ...XX.
273.     # ......
274.     # """
275.
276.     squares = squares.split('/n')
277.     if squares[0] == '':
278.         del squares[0]
279.     if squares[-1] == '':
280.         del squares[-1]
281.
282.     GRID_LOCK.acquire()
283.     for y in range(min(len(squares), CELLS_HIGH)):
284.         for x in range(min(len(squares[y]), CELLS_WIDE)):
285.             if squares[y][x] == ' ':
286.                 GRID[x][y] = None
287.             elif squares[y][x] == '.':
288.                 pass
289.             else:
290.                 GRID[x][y] = color
291.     GRID_LOCK.release()

  setGridSquares()上面解釋過了,它能夠?qū)戩o態(tài)的塊到網(wǎng)格上。

1
2
294. if __name__ == '__main__':
295.     main()

  上面是Python的一個(gè)技巧。作為把mian代碼放到全局域的代替,我們把它放到一個(gè)名為main()的函數(shù)里,它在底部被調(diào)用。這保證了所有函數(shù)在main()運(yùn)行的代碼前已定義好。__name__變量只有在自己運(yùn)行自己的時(shí)候才設(shè)置為字符串"__main__",和從其他程序作為一個(gè)模塊導(dǎo)入相反。

 總結(jié)

  它就是這樣了!多線程程序是相當(dāng)簡單地去解釋,但它很難去明白怎樣來讓你自己的多線程程序正確地工作。最好的學(xué)習(xí)方法是通過編寫你的程序來學(xué)習(xí)。

  實(shí)際上,我們建立起我們代碼的方式里,即使我們擺脫了鎖它還能完美運(yùn)行。不會(huì)崩潰,盡管有時(shí)候會(huì)出現(xiàn)兩個(gè)蟲子進(jìn)入相同單元最后都占據(jù)了這個(gè)單元的情況。然后他們就看起來在別人身上走過。使用鎖來確定一個(gè)單元在某一時(shí)刻只能被一個(gè)蟲子占據(jù)。

  祝你好運(yùn)!

  英文原文:Multithreaded Python Tutorial with the “Threadworms” Demo


發(fā)表評論 共有條評論
用戶名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 江都市| 偏关县| 交口县| 开阳县| 聂拉木县| 阿拉尔市| 佛教| 德兴市| 堆龙德庆县| 务川| 三江| 萨迦县| 商洛市| 湟中县| 罗平县| 新泰市| 山西省| 平陆县| 通海县| 南和县| 衡东县| 库车县| 上虞市| 宣威市| 什邡市| 浦北县| 赞皇县| 东阿县| 宾阳县| 丘北县| 邛崃市| 龙陵县| 广宁县| 简阳市| 米脂县| 新丰县| 融水| 辉南县| 盐津县| 金塔县| 九江县|