從零開始學Python(八):Python多執行緒和佇列

momoxiaomming發表於2019-03-04

很久沒有更新博文啦,在家過春節已經變懶了-_-,不過答應大家更完這個python的入門系列,偶還是會繼續努力的!另外祝願大家新年快樂,事事順心!

執行緒的概念

我們學習的很多程式語言,比如java,oc等,都會有執行緒這個概念.執行緒的用途非常的廣泛,給我們開發中帶來了很多的便利.主要用於一些序列或者並行的邏輯處理,比如點選某個按鈕的時候,我們可以通過進度條來控制執行緒的執行時間,以便於更好的用於使用者的互動.

每個獨立的執行緒都包含一個程式的執行入口,順序的執行序列和一個程式執行的出口.執行緒必須在程式中存在,而不能獨立於程式執行!

每個執行緒都有他自己的一組cpu儲存器,稱為執行緒的上下文,該上下文反應了執行緒上次執行的cpu暫存器的狀態.指令指標和堆疊指標暫存器是執行緒上下文中兩個最重要的暫存器,執行緒總是在程式得到上下文執行,這些地址都用於標誌擁有執行緒的程式地址空間中的記憶體.

Python執行緒

在Python中,主要提供了thread和threading兩個執行緒模組,thread模組提供了最基礎的,最低階的執行緒函式,和一個簡單的鎖.threading模組是thread模組的封裝進階,提供了多樣的執行緒屬性和方法.下面我們會對該兩個模組逐個解析.

thread模組(不推薦使用)

thread模組常用的函式方法:

函式名 描述
start_new_thread(function, args, kwargs=None) 產生一個新執行緒,function為執行緒要執行的函式名,args是函式引數(tuple元組型別),kwargs為可選引數
allocate_lock() 分配一個locktype型別的執行緒鎖物件
exit() 執行緒退出
_count() 返回執行緒數量,注意不包含主執行緒哦,所以在主執行緒執行該方法返回的是0
locked locktype 鎖,返回true為已鎖
release() 釋放locktype物件鎖
acquire() 鎖定

下面我們來舉個例子:

import thread,time


def loop1():
	print `執行緒個數-` + str(thread._count())
	i=0
	try:
    	while i < 100:
        	print i
        	time.sleep(1)
        	i = i + 1
	except Exception as e:
    	print e


thread.start_new_thread(loop1,())
複製程式碼

執行上面程式碼,你會發現loop1方法中的迴圈列印並沒有被呼叫,而是直接返回了一個異常:

Unhandled exception in thread started by 
sys.excepthook is missing
lost sys.stderr
複製程式碼

這時你可能會一遍又一遍的檢查程式碼,以為是程式碼錯了(沒錯,那個人就是我),其實我們程式碼本身是沒有錯誤的,是早期python的thread模組一個缺陷(這個缺陷也是導致這個模組被官方不推薦使用的主要原因):當我們在主執行緒中使用start_new_thread建立新的執行緒的時候,主執行緒無法得知執行緒何時結束,他不知道要等待多久,導致主執行緒已經執行完了,子執行緒卻還未完成,於是系統丟擲了這個異常.

解決這個異常的方法有兩種:

1.讓主執行緒休眠足夠長的時間來等待子執行緒返回結果:

import thread,time


def loop1():
	print `執行緒個數-` + str(thread._count())
	i=0
	try:
    	while i < 100:
        	print i
        	time.sleep(1)
        	i = i + 1
	except Exception as e:
    	print e


thread.start_new_thread(loop1,())
time.sleep(1000)   #讓主執行緒休眠1000秒,足夠子執行緒完成
複製程式碼

2.給執行緒加鎖(早期python執行緒使用一般處理)

import thread,time


def loop1(lock):
	print `執行緒個數-` + str(thread._count())
	i=0
	try:
    	while i < 100:
        	print i
        	time.sleep(1)
        	i = i + 1
	except Exception as e:
    	lock.release()
    	print e

	lock.release()    #執行完畢,釋放鎖


lock=thread.allocate_lock()   #獲取locktype物件
lock.acquire()   #鎖定

thread.start_new_thread(loop1,(lock,))
while lock.locked():    #等待執行緒鎖釋放
	pass
複製程式碼

以上就是thread模組的常用執行緒用法,我們可以看出,thread模組提供的執行緒操作是極其有限的,使用起來非常的不靈活,下面我們介紹他的同胞模組threading.

threading模組(推薦使用)

threading模組是thread的完善,有一套成熟的執行緒操作方法,基本能完成我們所需的所有執行緒操作

threading常用方法:

  • threading.currentThread(): 返回當前的執行緒變數。
  • threading.enumerate(): 返回一個包含正在執行的執行緒的list。 正在執行指執行緒啟動後、結束前,不包括啟動前和終止後的執行緒。
  • threading.activeCount(): 返回正在執行的執行緒數量,與len(threading.enumerate())有相同的結果。
  • run(): 用以表示執行緒活動的方法。
  • start():啟動執行緒活動。
  • join([time]): 等待至執行緒中止。 這阻塞呼叫執行緒直至執行緒的join() 方法被呼叫中止-正常退出或者丟擲未處理的異常-或者是可選的超時發生。
  • isAlive(): 返回執行緒是否活動的。
  • getName(): 返回執行緒名。
  • setName(): 設定執行緒名。

threading模組建立執行緒有兩種方式:

1.直接通過初始化thread物件建立:

#coding=utf-8
import threading,time

def test():
	t = threading.currentThread()  # 獲取當前子執行緒物件
	print t.getName()  # 列印當前子執行緒名字

	i=0
	while i<10:
    	print i
    	time.sleep(1)
    	i=i+1




m=threading.Thread(target=test,args=(),name=`迴圈子執行緒`)   #初始化一個子執行緒物件,target是執行的目標函式,args是目標函式的引數,name是子執行緒的名字
m.start()
t=threading.currentThread()   #獲取當前執行緒物件,這裡其實是主執行緒
print t.getName()   #列印當前執行緒名字,其實是主執行緒名字
複製程式碼

可以看到列印結果:

迴圈子執行緒
MainThread
0
1
2
3
4
5
6
7
8
複製程式碼

2.通過基礎thread類來建立

import threading,time
class myThread (threading.Thread):   #建立一個自定義執行緒類mythread,繼承Thread

def __init__(self,name):
    """
    重新init方法
    :param name: 執行緒名
    """
    super(myThread, self).__init__(name=name)
    # self.lock=lock

    print `執行緒名`+name

def run(self):
    """
    重新run方法,這裡面寫我們的邏輯
    :return:
    """
    i=0
    while i<10:
        print i
        time.sleep(1)
        i=i+1


if __name__==`__main__`:
	t=myThread(`mythread`)
	t.start()
複製程式碼

輸出:

執行緒名執行緒
0
1
2
3
4
5
6
7
8
9
複製程式碼

執行緒同步

如果兩個執行緒同時訪問同一個資料的時候,可能會出現無法預料的結果,這時候我們就要用到執行緒同步的概念.

上面我們講到thread模組的時候,已經使用了執行緒鎖的概念,thread物件的Lock和Rlock可以實現簡單的執行緒同步,這兩個物件都有acquire方法和release方法,對於那些需要每次只允許一個執行緒操作的資料,可以將其操作放到acquire和release方法之間.

下面我們來舉例說明,我們需要實現3個執行緒同時訪問一個全域性變數,並且改變這個變數:

1.不加鎖的情況
import threading,time

lock=threading.Lock()   #全域性的鎖物件
temp=0    #我們要多執行緒訪問的全域性屬性

class myThread (threading.Thread):   #建立一個自定義執行緒類mythread,繼承Thread

	def __init__(self,name):
    	"""
    	重新init方法
    	:param name: 執行緒名
    	"""
    	super(myThread, self).__init__(name=name)
    	# self.lock=lock

    	print `執行緒名`+name

	def run(self):
    	"""
    	重新run方法,這裡面寫我們的邏輯
    	:return:
    	"""
    	global temp,lock

    	i=0
    	while i<2:   #這裡迴圈兩次累加全域性變數,目的是增加出錯的概率

        	temp=temp+1  #在子執行緒中實現對全域性變數加1

        	print self.name+`--temp==`+str(temp)
        	i=i+1

	if __name__==`__main__`:


		t1=myThread(`執行緒1`)
		t2=myThread(`執行緒2`)
		t3=myThread(`執行緒3`)

		#建立三個執行緒去執行訪問
		t1.start()
		t2.start()
		t3.start()
複製程式碼

執行結果(由於程式執行很快,你多執行幾次就可能會出現以下結果): 我們可以發現,執行緒1和執行緒2同時訪問到了變數,導致列印出現對等情況

執行緒名執行緒1
執行緒名執行緒2
執行緒名執行緒3
執行緒1--temp==1執行緒2--temp==2
執行緒1--temp==3

執行緒2--temp==4
執行緒3--temp==5
執行緒3--temp==6
複製程式碼
2.加鎖情況
import threading,time

lock=threading.Lock()   #全域性的鎖物件
temp=0    #我們要多執行緒訪問的全域性屬性

class myThread (threading.Thread):   #建立一個自定義執行緒類mythread,繼承Thread

	def __init__(self,name):
    	"""
    	重新init方法
    	:param name: 執行緒名
    	"""
    	super(myThread, self).__init__(name=name)
    	# self.lock=lock

    	print `執行緒名`+name

	def run(self):
    	"""
    	重新run方法,這裡面寫我們的邏輯
    	:return:
    	"""
    	global temp,lock

    	if lock.acquire():  #這裡執行緒進來訪問變數的時候,鎖定變數
        	i = 0
        	while i < 2:  # 這裡迴圈兩次累加全域性變數,目的是增加出錯的概率

            	temp = temp + 1  # 在子執行緒中實現對全域性變數加1

            	print self.name + `--temp==` + str(temp)
            	i = i + 1

        	lock.release()  #訪問完畢,釋放鎖讓另外的執行緒訪問



if __name__==`__main__`:


	t1=myThread(`執行緒1`)
	t2=myThread(`執行緒2`)
	t3=myThread(`執行緒3`)

	#建立三個執行緒去執行訪問
	t1.start()
	t2.start()
	t3.start()
複製程式碼

執行結果(不管執行多少次,都不會出現同時訪問的情況):

執行緒名執行緒1
執行緒名執行緒2
執行緒名執行緒3
執行緒1--temp==1
執行緒1--temp==2
執行緒2--temp==3
執行緒2--temp==4
執行緒3--temp==5
執行緒3--temp==6
複製程式碼

執行緒同步很多地方都會用到,比如搶票,抽獎,我們需要對一些資源進行鎖定,以防止多執行緒訪問的時候出現不可預知的情況.

執行緒佇列

python中的佇列用到了Queue模組,該模組提供了同步的,安全的對序列,包括FIFO(先入先出)佇列Queue,LIFO(後入先出)佇列LifoQueue,和優先順序佇列PriorityQueue.這些佇列都實現了鎖原語,能夠在多執行緒中直接使用。可以使用佇列來實現執行緒間的通訊

Queue模組中的常用方法:

  • Queue.qsize() 返回佇列的大小
  • Queue.empty() 如果佇列為空,返回True,反之False
  • Queue.full() 如果佇列滿了,返回True,反之False
  • Queue.full 與 maxsize 大小對應
  • Queue.get([block[, timeout]])獲取佇列,timeout等待時間
  • Queue.get_nowait() 相當Queue.get(False)
  • Queue.put(item) 寫入佇列,timeout等待時間
  • Queue.put_nowait(item) 相當Queue.put(item, False)
  • Queue.task_done() 在完成一項工作之後,Queue.task_done()函式向任務已經完成的佇列傳送一個訊號
  • Queue.join() 實際上意味著等到佇列為空,再執行別的操作

例子:
tags=[`one`,`tow`,`three`,`four`,`five`,`six`]

q=Queue.LifoQueue()   #先入先出佇列
for t in tags:
q.put(t)   #將陣列資料加入佇列
for i in range(6):
	print q.get()    #取出操作可以放在不同的執行緒中,不會出現同步的問題
複製程式碼

結果:

six
five
four
three
tow
one
複製程式碼

Q&A

這章的多執行緒就到這裡了,我們主要講述了他的基本用法,更多的用法我們可以在以後的開發過程中,根據自己邏輯去設計.

相關文章