python 多程式和多執行緒學習

ckxllf發表於2019-12-18

  寫在前面

  總所周知,unix/linux 為多工作業系統,,即可以支援遠大於CPU數量的任務同時執行

  理解多工就需要知道作業系統的CPU上下文:

  首先,我們都知道cpu一個時間段其實只能執行單個任務,只不過在很短的時間內,CPU快速切換到不同的任務進行執行,造成一種多工同時執行的錯覺

  而在CPU切換到其他任務執行之前,為了確保在切換任務之後還能夠繼續切換回原來的任務繼續執行,並且看起來是一種連續的狀態,就必須將任務的狀態保持起來,以便恢復原始任務時能夠繼續之前的狀態執行,狀態儲存的位置位於CPU的暫存器和程式計數器(,PC)

  簡單來說暫存器是CPU內建的容量小、但速度極快的記憶體,用來儲存程式的堆疊資訊即資料段資訊。程式計數器儲存程式的下一條指令的位置即程式碼段資訊。

  所以,CPU上下文就是指CPU暫存器和程式計數器中儲存的任務狀態資訊;CPU上下文切換就是把前一個任務的CPU上下文儲存起來,然後載入下一個任務的上下文到這些暫存器和程式計數器,再跳轉到程式計數器所指示的位置執行程式。

  python程式預設都是執行單任務的程式,也就是隻有一個執行緒。如果我們要同時執行多個任務怎麼辦?

  有兩種解決方案:

  一種是啟動多個程式,每個程式雖然只有一個執行緒,但多個程式可以一塊執行多個任務。

  還有一種方法是啟動一個程式,在一個程式內啟動多個執行緒,這樣,多個執行緒也可以一塊執行多個任務。

  當然還有第三種方法,就是啟動多個程式,每個程式再啟動多個執行緒,這樣同時執行的任務就更多了,當然這種模型更復雜,實際很少採用。

  Python中的多程式

  在Unix/Linux系統中,提供了一個fork()函式呼叫,相較於普通函式呼叫一次,返回一次的機制,fork()呼叫一次,返回兩次,具體表現為作業系統自動把當前程式(稱為父程式)複製了一份(稱為子程式),然後分別在父程式和子程式內返回。

  子程式永遠返回0,而父程式返回子程式的ID,這樣一個父程式可以輕鬆fork出很多子程式。且父程式會記下每個子程式的ID,而子程式只需要呼叫getppid()就可以拿到父程式的ID。

  python的os模組封裝了fork呼叫方法以實現在python程式中建立子程式,下面具體看兩個例子:

  [root@test-yw-01 opt]# cat test.py

  import os

  print('Process ({}) start...'.format(os.getpid()))

  pid = os.fork()

  print(pid)

  [root@test-yw-01 opt]# python3 test.py

  Process (26620) start...

  26621

  0

  [root@test-yunwei-01 opt]# cat process.py

  import os

  print('Process ({}) start...'.format(os.getpid()))

  pid = os.fork()

  if pid == 0:

  print('The child process is {} and parent process is{}'.format(os.getpid(),os.getppid()))

  else:

  print('I (%s) just created a child process (%s).' % (os.getpid(), pid))

  [root@test-yunwei-01 opt]# pyhton3 process.py

  Process (25863) start...

  I (25863) just created a child process (25864)

  The child process is 25864 and parent process is 25863

  透過fork呼叫這種方法,一個程式在接到新任務時就可以複製出一個子程式來處理新任務,例如nginx就是由父程式(master process)監聽埠,再fork出子程式(work process)來處理新的http請求。

  注意:

  Windows沒有fork呼叫,所以在window pycharm上執行以上程式碼無法實現以上效果。

  multiprocessing模組

  雖然Windows沒有fork呼叫,但是可以憑藉multiprocessing該多程式模組所提供的Process類來實現。

  下面看一例子:

  首先模擬一個使用單程式的下載任務,並列印出程式號

  1)單程式執行:

  import os

  from random import randint

  import time

  def download(filename):

  print("程式號是:%s"%os.getpid())

  downloadtime = 3

  print('現在開始下載:{}'.format(filename))

  time.sleep(downloadtime)

  def runtask():

  start_time = time.time()

  download('水滸傳')

  download('西遊記')

  stop_time = time.time()

  print('下載耗時:{}'.format(stop_time - start_time))

  if __name__ == '__main__':

  runtask()

  得出結果

  接著透過呼叫Process模擬開啟兩個子程式:

  import time

  from os import getpid

  from multiprocessing import Process

  def download(filename):

  print("程式號是:%s"%getpid())

  downloadtime = 3

  print('現在開始下載:{}'.format(filename))

  time.sleep(downloadtime)

  def runtask():

  start_time = time.time()

  task1 = Process(target=download,args=('西遊記',))

  task1.start() #呼叫start()開始執行

  task2 = Process(target=download,args=('水滸傳',))

  task2.start()

  task1.join() # join()方法可以等待子程式結束後再繼續往下執行,通常用於程式間的同步

  task2.join()

  stop_time = time.time()

  print('下載耗時:{}'.format(stop_time - start_time))

  if __name__ == '__main__':

  runtask()

  連線池Pool

  可以用程式池Pool批次建立子程式的方式來建立大量工作子程式

  import os

  from random import randint

  from multiprocessing import Process,Pool

  import time

  def download(taskname):

  print("程式號是:%s"%os.getpid())

  downloadtime = randint(1,3)

  print('現在開始下載:{}'.format(taskname))

  time.sleep(downloadtime)

  def runtask():

  start_time = time.time()

  pool = Pool(4) #定義程式連線池可用連線數量

  for task in range(5):

  pool.apply_async(download,args=(task,))

  pool.close()

  pool.join()

  stop_time = time.time()

  print('完成下載,下載耗時:{}'.format(stop_time - start_time))

  if __name__ == '__main__':

  runtask()

  需要注意的點:

  對pool物件呼叫join()方法會等待所有子程式執行完畢,呼叫join()之前必須先呼叫close(),呼叫close()之後就不能繼續新增新的程式

  pool的預設大小是主機CPU的核數,所以這裡設定成4個程式,這樣就避免了cpu關於程式間切換帶來的額外資源消耗,提高了任務的執行效率

  程式間通訊

  from multiprocessing.Queue import Queue

  相較於普通Queue普通的佇列的先進先出模式,get方法會阻塞請求,直到有資料get出來為止。這個是多程式併發的Queue佇列,用於解決多程式間的通訊問題。

  from multiprocessing import Process, Queue

  import os, time, random

  datas = []

  def write_data(args):

  print('Process to write: %s' % os.getpid())

  for v in "helloword":

  datas.append(v)

  print("write {} to queue".format(v))

  args.put(v)

  time.sleep(random.random())

  print(datas)

  def read_data(args):

  print('Process to read: %s' % os.getpid())

  while True:

  value = args.get(True)

  print("read {} from queue".format(value))

  if __name__ == '__main__':

  queue = Queue()

  write = Process(target=write_data,args=(queue,))

  read = Process(target=read_data,args=(queue,))

  write.start()

  read.start()

  write.join()

  read.terminate()

  程式池中使用佇列

  由於佇列物件不能在父程式與子程式間通訊,所以需要使用Manager().Queue()才能實現佇列中各子程式間進行通訊

  from multiprocessing import Manager

  if __name__=='__main__':

  manager = multiprocessing.Manager()

  # 父程式建立Queue,並傳給各個子程式:

  queue = manager.Queue()

  pool = Pool()

  write = Process(target=write_data,args=(queue,))

  read = Process(target=read_data,args=(queue,))

  write.start()

  read.start()

  write.join()

  read.terminate()

  如果是用程式池,就需要使用Manager().Queue()佇列才能實現在各子程式間進行通訊

  參考文件: https://blog.csdn.net/qq_32446743/article/details/79785684

  https://blog.csdn.net/u013713010/article/details/53325438

  Python中的多執行緒

  相較於資源分配的基本單位程式,執行緒是任務執行排程的基本單位,且由於每一個程式擁有自己獨立的記憶體空間,而執行緒共享所屬程式的記憶體空間,所以在涉及多工執行時,執行緒的上下文切換比程式少了作業系統核心將虛擬記憶體資源即暫存器中的內容切換出這一步驟,也就大大提升了多工執行的效率。

  首先需要明確幾個概念:

  1.當一個程式啟動之後,會預設產生一個主執行緒,因為執行緒是程式執行流的最小單元,當設定多執行緒時,主執行緒會建立多個子執行緒,在python中,預設情況下(其實就是setDaemon(False)),主執行緒執行完自己的任務以後,就退出了,此時子執行緒會繼續執行自己的任務,直到自己的任務結束

  python的提供了關於多執行緒的threading模組,和多程式的啟動類似,就是把函式傳入並建立Thread例項,然後呼叫start()開始執行:

  import os

  import random

  from threading import Thread

  import threading

  import time

  def mysql_dump():

  print('開始執行執行緒{}'.format(threading.current_thread().name)) #返回當前執行緒例項名稱

  dumptime = random.randint(1,3)

  time.sleep(dumptime) #利用time.sleep()方法模擬備份資料庫所花費時間

  class Mutil_thread(Thread):

  def runtask(slef):

  thread_list = []

  print('當前執行緒的名字是: ', threading.current_thread().name)

  start_time = time.time()

  for t in range(5):

  task = Mutil_thread(target=slef.tasks)

  thread_list.append(task)

  for i in thread_list:

  # i.setDaemon(False)

  i.start()

  i.join()#join()所完成的工作就是執行緒同步,即主執行緒任務結束之後,進入阻塞狀態,一直等待其他的子執行緒執行結束之後,主執行緒在終止

  stop_time = time.time()

  print('主執行緒結束!', threading.current_thread().name)

  print('下載耗時:{}'.format(stop_time - start_time))

  if __name__ == '__main__':

  run = Mutil_thread()

  run.tasks = mysql_dump

  run.runtask()

  執行結果:

  程式預設就會啟動一個執行緒,我們把該執行緒稱為主執行緒,例項的名為MainThread,主執行緒又可以啟動新的執行緒,執行緒命名依次為Thread-1,Thread-2…

  LOCK

  多執行緒不同於程式,在多程式中,例如針對同一個變數,各自有一份複製存在於每個程式中,資源相互隔離,互不影響

  但在程式中,執行緒間可以共享程式像系統申請的記憶體空間,雖然實現多個執行緒間的通訊相對簡單,但是當同一個資源(臨界資源)被多個執行緒競爭使用時,例如執行緒共享程式的變數,其就有可能被任何一個執行緒修改,所以對這種臨界資源的訪問需要加上保護,否則資源會處於“混亂”的狀態。

  import time

  from threading import Thread,Lock

  class Account(object): # 假定這是一個銀行賬戶

  def __init__(self):

  self.balance = 0 #初始餘額為0元

  def count(self,money):

  new_balance = self.balance + money

  time.sleep(0.01) # 模擬每次存款需要花費的時間

  self.balance = new_balance #存完之後更新賬戶餘額

  @property

  def get_count(self):

  return(self.balance)

  class Addmoney(Thread): #模擬存款業務,直接繼承Thread

  def __init__(self,action,money):

  super().__init__() #在繼承Thread類的基礎上,再新增action及money屬性,便於main()的直接呼叫

  self.action = action

  self.money = money

  def run(self):

  self.action.count(self.money)

  def main():

  action = Account()

  threads = []

  for i in range(1000): #開啟1000個執行緒同時向賬戶存款

  t = Addmoney(action,1) #每次只存入一元

  threads.append(t)

  t.start()

  for task in threads:

  task.join()

  print('賬戶餘額為: ¥%s元'%action.get_count)

  main()

  檢視執行結果:  鄭州哪個婦科醫院好

  

在這裡插入圖片描述

  執行上面的程式,1000執行緒分別向賬戶中轉入1元錢,結果小於100元。之所以出現這種情況是因為我們沒有對balance餘額該執行緒共享的變數加以保護,當多個執行緒同時向賬戶中存錢時,會一起執行到new_balance = self.balance + money這行程式碼,多個執行緒得到的賬戶餘額都是初始狀態下的0,所以都是0上面做了+1的操作,因此得到了錯誤的結果。

  如果我們要確保balance計算正確,就要給Account().count()上一把鎖,當某個執行緒開始執行Account().count()時,該執行緒因為獲得了鎖,因此其他執行緒不能同時執行,只能等待鎖被釋放後,獲得該鎖以後才能更改改。由於鎖只有一個,無論多少執行緒,同一時刻最多隻有一個執行緒持有該鎖,所以,不會造成修改的衝突。建立一個鎖就是透過threading.Lock()來實現:

  import time

  from threading import Thread,Lock

  import time

  from threading import Thread,Lock

  class Account(object): # 假定這是一個銀行賬戶

  def __init__(self):

  self.balance = 0 #初始餘額為0元

  self.lock = Lock()

  def count(self,money):

  self.lock.acquire()

  try:

  new_balance = self.balance + money

  time.sleep(0.01) # 模擬每次存款需要花費的時間

  self.balance = new_balance #存完之後更新賬戶餘額

  finally: #在finally中執行釋放鎖的操作保證正常異常鎖都能釋放

  self.lock.release()

  def get_count(self):

  return(self.balance)

  class Addmoney(Thread): #模擬存款業務

  def __init__(self,action,money):

  super().__init__()

  self.action = action

  self.money = money

  self.lock = Lock()

  def run(self):

  self.action.count(self.money)

  def main():

  action = Account()

  threads = []

  for i in range(1000): #開啟100000個執行緒同時向賬戶存款

  t = Addmoney(action,1) #每次只存入一元

  threads.append(t)

  t.start()

  for task in threads:

  task.join()

  print('賬戶餘額為: ¥%s元'%action.get_count())

  main()

  執行結果:

  

在這裡插入圖片描述


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69945560/viewspace-2669224/,如需轉載,請註明出處,否則將追究法律責任。

相關文章