Python 系統編程 進程與線程(2)

1. 多線程-threading

Python的標準庫提供了兩個模塊:_thread和threading,_thread是低級模塊,threading是高級模塊,對_thread進行了封裝。絕大多數情況下,我們只需要使用threading這個高級模塊。
啟動一個線程就是把一個函數傳入并創建Thread實例,然后調用start()開始執行:

  • 單線程執行
import time

def saySorry():
    print("親愛的,我錯了,我能吃飯了嗎?%s"%time.ctime())
    time.sleep(1)

if __name__ == "__main__":
    for i in range(5):
        saySorry()
  • 運行結果
>>>
親愛的,我錯了,我能吃飯了嗎?Mon Jun 12 17:08:33 2017
親愛的,我錯了,我能吃飯了嗎?Mon Jun 12 17:08:34 2017
親愛的,我錯了,我能吃飯了嗎?Mon Jun 12 17:08:35 2017
親愛的,我錯了,我能吃飯了嗎?Mon Jun 12 17:08:36 2017
親愛的,我錯了,我能吃飯了嗎?Mon Jun 12 17:08:37 2017
  • 多線程執行
import time
import threading
def saySorry():
    print("親愛的,我錯了,我能吃飯了嗎? name=%s, %s"%(threading.current_thread().name,time.ctime()))
    time.sleep(1)

if __name__ == "__main__":
    print(threading.current_thread().name)
    for i in range(5):
        t1 = threading.Thread(target=saySorry)
        t1.start()
  • 運行結果
>>>
MainThread
親愛的,我錯了,我能吃飯了嗎? name=Thread-1, Mon Jun 12 19:15:02 2017
親愛的,我錯了,我能吃飯了嗎? name=Thread-2, Mon Jun 12 19:15:02 2017
親愛的,我錯了,我能吃飯了嗎? name=Thread-3, Mon Jun 12 19:15:02 2017
親愛的,我錯了,我能吃飯了嗎? name=Thread-4, Mon Jun 12 19:15:02 2017
親愛的,我錯了,我能吃飯了嗎? name=Thread-5, Mon Jun 12 19:15:02 2017
#多線程的時候可以看出來是同時執行的

說明

  1. 可以明顯看出使用了多線程并發的操作,花費時間要短很多
  2. 創建好的線程,需要調用start()方法來啟動

由于任何進程默認就會啟動一個線程,我們把該線程稱為主線程,主線程又可以啟動新的線程,Python的threading模塊有個current_thread()函數,它永遠返回當前線程的實例。主線程實例的名字叫MainThread,子線程的名字在創建時指定,這里我們沒有指定。名字僅僅在打印時用來顯示,完全沒有其他意義,如果不起名字Python就自動給線程命名為Thread-1,Thread-2……

主線程會等待所有的子線程結束后才結束

查看線程數量

length = len(threading.enumerate())
print('當前運行的線程數為:%d' % length)

2. threading注意點

1.2.2.1線程執行代碼的封裝
通過上一小節,能夠看出,通過使用threading模塊能完成多任務的程序開發,為了讓每個線程的封裝性更完美,所以使用threading模塊時,往往會定義一個新的子類class,只要繼承threading.Thread就可以了,然后重寫run方法
示例如下:

import threading
import time


class MyThread(threading.Thread):
    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "I'm " + self.name + ' @ ' + str(i)  # name屬性中保存的是當前線程的名字
            print(msg)


if __name__ == '__main__':
    t = MyThread()
    t.start()

說明
python的threading.Thread類有一個run方法,用于定義線程的功能函數,可以在自己的線程類中覆蓋該方法。而創建自己的線程實例后,通過Thread類的start方法,可以啟動該線程,交給python虛擬機進行調度,當該線程獲得執行的機會時,就會調用run方法執行線程。

  • 線程的執行順序
import threading
import time

class MyThread(threading.Thread):
    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "I'm " + self.name + ' @ ' + str(i)
            print(msg)

def test():
    for i in range(5):
        t = MyThread()
        t.start()

if __name__ == '__main__':
    test()
  • 運行結果(運行的結果可能不一樣,但是大體是一致的)
I'm Thread-2 @ 0
I'm Thread-1 @ 0
I'm Thread-3 @ 0
I'm Thread-4 @ 0
I'm Thread-5 @ 0
I'm Thread-1 @ 1
I'm Thread-2 @ 1
I'm Thread-3 @ 1
I'm Thread-4 @ 1
I'm Thread-5 @ 1
I'm Thread-2 @ 2
I'm Thread-1 @ 2
I'm Thread-3 @ 2
I'm Thread-4 @ 2
I'm Thread-5 @ 2

說明
從代碼和執行結果我們可以看出,多線程程序的執行順序是不確定的。當執行到sleep語句時,線程將被阻塞(Blocked),到sleep結束后,線程進入就緒(Runnable)狀態,等待調度。而線程調度將自行選擇一個線程執行。上面的代碼中只能保證每個線程都運行完整個run函數,但是線程的啟動順序、run函數中每次循環的執行順序都不能確定。
總結

  1. 每個線程一定會有一個名字,盡管上面的例子中沒有指定線程對象的name,但是python會自動為線程指定一個名字。
  2. 當線程的run()方法結束時該線程完成。
  3. 無法控制線程調度程序,但可以通過別的方式來影響線程調度的方式。
  4. 線程的幾種狀態

3. 多線程-共享全局變量

  • 在一個進程內的所有線程共享全局變量,能夠在不適用其他方式的前提下完成多線程之間的數據共享(這點要比多進程要好)
  • 缺點就是,線程是對全局變量隨意遂改可能造成多線程之間對全局變量的混亂(即線程非安全)

4. 進程VS線程

定義

  • 進程是系統進行資源分配和調度的一個獨立單位.
  • 線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源.

區別

  • 一個程序至少有一個進程,一個進程至少有一個線程.
  • 線程的劃分尺度小于進程(資源比進程少),使得多線程程序的并發性高。
  • 進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大地提高了程序的運行效率
  • 線程不能夠獨立執行,必須依存在進程中

優缺點

  • 線程和進程在使用上各有優缺點:線程執行開銷小,但不利于資源的管理和保護;而進程正相反。

5. 同步的概念

  1. 什么是同步
    同步就是協同步調,按預定的先后次序進行運行。如:你說完,我再說。
    "同"字從字面上容易理解為一起動作
    其實不是,"同"字應是指協同、協助、互相配合。
    如進程、線程同步,可理解為進程或線程A和B一塊配合,A執行到一定程度時要依靠B的某個結果,于是停下來,示意B運行;B依言執行,再將結果給A;A再繼續操作。
  2. 多線程的優勢在于可以同時運行多個任務(至少感覺起來是這樣)。但是當線程需要共享數據時,可能存在數據不同步的問題。
    考慮這樣一種情況:一個列表里所有元素都是0,線程"set"從后向前把所有元素改成1,而線程"print"負責從前往后讀取列表并打印。
    那么,可能線程"set"開始改的時候,線程"print"便來打印列表了,輸出就成了一半0一半1,這就是數據的不同步。為了避免這種情況,引入了鎖的概念。

6. 互斥鎖

threading模塊中定義了Lock類,可以方便的處理鎖定:

import threading

#創建鎖
myLock = threading.Lock()
print(myLock)
print('1...')
#鎖住,如果此鎖,已經鎖了,如果再鎖,會阻塞,直到開鎖了,才能再鎖
#myLock.acquire()
myLock.acquire()
print('2...')
#開鎖,如果鎖住了,可以開鎖。如果沒鎖,直接開,報錯
myLock.release()
print('3...')
myLock.acquire()
print('4...')
  • 運行結果
>>>
<unlocked _thread.lock object at 0x0000000000AEBC60>
1...
2...
3...
4...

其中,鎖定方法acquire可以有一個blocking參數。

  • 如果設定blocking為True,則當前線程會堵塞,直到獲取到這個鎖為止(如果沒有指定,那么默認為True)
  • 如果設定blocking為False,則當前線程不會堵塞

例子

import threading
import time

num = 0
myLock=threading.Lock()

def fun1():
    global num
    for i in range(1000000):
        myLock.acquire()
        num +=1
        myLock.release()
    print('fun1...num=%d,%s'%(num,time.ctime()))

def fun2():
    global num
    for i in range(10000000):
        myLock.acquire()
        num += 1
        myLock.release()
    #time.sleep(2)
    print('fun2...num=%d,%s'%(num,time.ctime()))

def main():
    t1 = threading.Thread(target=fun1)
    t2 = threading.Thread(target=fun2)
    t1.start()
    #time.sleep(2)
    t2.start()
    print('num=%d,%s'%(num,time.ctime()))

if __name__ == '__main__':
    main()
  • 運行結果
num=22642,Mon Jun 12 20:29:23 2017
fun1...num=2060762,Mon Jun 12 20:29:25 2017
fun2...num=11000000,Mon Jun 12 20:29:29 2017

在打印fun1的時候,cpu權限切換到fun2,此時num已經在瘋狂運算了,所以打印的時候不是1000000.如果沒有互斥鎖的話,最后這個fun2打印出來的不會是這個值。

例子2:簡單的售票,同步鎖完成售票

import threading
import time
import os


def doChore():  # 作為間隔  每次調用間隔0.5s
    time.sleep(0.5)


def booth(tid):
    global i
    global lock
    while True:
        lock.acquire()                      # 得到一個鎖,鎖定
        if i != 0:
            i = i - 1                       # 售票 售出一張減少一張
            print(tid, ':now left:', i)    # 剩下的票數
            doChore()
        else:
            print("Thread_id", tid, " No more tickets")
            os._exit(0)                     # 票售完   退出程序
        lock.release()                      # 釋放鎖
        doChore()


#全局變量
i = 15                      # 初始化票數
lock = threading.Lock()     # 創建鎖


def main():
    # 總共設置了3個線程
    for k in range(3):
        # 創建線程; Python使用threading.Thread對象來代表線程
        new_thread = threading.Thread(target=booth, args=(k,))
        # 調用start()方法啟動線程
        new_thread.start()

if __name__ == '__main__':
    main()
  • 運行結果
0 :now left: 14
1 :now left: 13
2 :now left: 12
0 :now left: 11
1 :now left: 10
2 :now left: 9
0 :now left: 8
1 :now left: 7
0 :now left: 6
2 :now left: 5
1 :now left: 4
0 :now left: 3
2 :now left: 2
1 :now left: 1
0 :now left: 0
Thread_id 2  No more tickets

總結
鎖的好處:

  • 確保了某段關鍵代碼只能由一個線程從頭到尾完整地執行
    鎖的壞處:
  • 阻止了多線程并發執行,包含鎖的某段代碼實際上只能以單線程模式執行,效率就大大地下降了
  • 由于可以存在多個鎖,不同的線程持有不同的鎖,并試圖獲取對方持有的鎖時,可能會造成死鎖

7. 多線程-非共享數據

對于全局變量,在多線程中要格外小心,否則容易造成數據錯亂的情況發生

在多線程開發中,全局變量是多個線程都共享的數據,而局部變量等是各自線程的,是非共享的

8. 死鎖

  • 在線程間共享多個資源的時候,如果兩個線程分別占有一部分資源并且同時等待對方的資源,就會造成死鎖。
  • 盡管死鎖很少發生,但一旦發生就會造成應用的停止響應。

避免死鎖

  • 程序設計時要盡量避免(銀行家算法)
  • 添加超時時間等

9. 同步應用

from threading import Thread, Lock
from time import sleep
'''
    等待鎖的打開:阻塞-喚醒  機制

    還有一種實現形式:輪循,效率低
'''

class Task1(Thread):
    def run(self):
        while True:
            if lock1.acquire():
                print("------Task 1 -----")
                sleep(0.5)
                lock2.release()


class Task2(Thread):
    def run(self):
        while True:
            if lock2.acquire():
                print("------Task 2 -----")
                sleep(0.5)
                lock3.release()


class Task3(Thread):
    def run(self):
        while True:
            if lock3.acquire():
                print("------Task 3 -----")
                sleep(0.5)
                lock1.release()


# 使用Lock創建出的鎖默認沒有“鎖上”
lock1 = Lock()
# 創建另外一把鎖,并且“鎖上”
lock2 = Lock()
lock2.acquire()
# 創建另外一把鎖,并且“鎖上”
lock3 = Lock()
lock3.acquire()

if __name__ == '__main__':
    t1 = Task1()
    t2 = Task2()
    t3 = Task3()

    t1.start()
    t2.start()
    t3.start()
  • 運行結果
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
...省略...

總結

  • 可以使用互斥鎖完成多個任務,有序的進程工作,這就是線程的同步

10. 生產者與消費者模式

  1. 隊列
    先進先出

  2. 先進后出

Python的Queue模塊中提供了同步的、線程安全的隊列類,包括FIFO(先入先出)隊列Queue,LIFO(后入先出)隊列LifoQueue,和優先級隊列PriorityQueue。這些隊列都實現了鎖原語(可以理解為原子操作,即要么不做,要么就做完),能夠在多線程中直接使用。可以使用隊列來實現線程間的同步。
用FIFO隊列實現上述生產者與消費者問題的代碼如下:

'''
    隊列:
    1、進程之間的通信:       q = multiprocessing.Queue()
    2、進程池之間的通信:     q = multiprocessing.Manager().Queue()
    3、線程之間的通信:       q = queue.Queue()
'''

import threading
import time

# python2中
# from Queue import Queue

# python3中
from queue import Queue


class Producer(threading.Thread):
    def run(self):
        global queue
        count = 0
        while True:
            if queue.qsize() < 1000:
                for i in range(100):
                    count = count + 1
                    msg = '生成產品' + str(count)
                    queue.put(msg)
                    print(msg)
            time.sleep(1)


class Consumer(threading.Thread):
    def run(self):
        global queue
        while True:
            if queue.qsize() > 100:
                for i in range(3):
                    msg = self.name + '消費了 ' + queue.get()
                    print(msg)
            time.sleep(1)

#全局變量
queue = Queue()

if __name__ == '__main__':


    for i in range(500):
        queue.put('初始產品' + str(i))

    for i in range(2):
        p = Producer()
        p.start()

    for i in range(5):
        c = Consumer()
        c.start()

Queue的說明
1.對于Queue,在多線程通信之間扮演重要的角色
2.添加數據到隊列中,使用put()方法
3.從隊列中取數據,使用get()方法
4.判斷隊列中是否還有數據,使用qsize()方法

11. ThreadLocal

在多線程環境下,每個線程都有自己的數據。一個線程使用自己的局部變量比使用全局變量好,因為局部變量只有線程自己能看見,不會影響其他線程,而全局變量的修改必須加鎖。

  • 使用ThreadLocal的方法
    ThreadLocal應運而生,不用查找dict,ThreadLocal幫你自動做這件事:
'''
    threadlocal就有倆功能:

    1、將各自的局部變量綁定到各自的線程中
    2、局部變量可以傳遞了,而且并沒有變成形參
'''

import threading

# 創建全局ThreadLocal對象:
local_school = threading.local()


def process_student():
    # 獲取當前線程關聯的student:
    std = local_school.student
    print('Hello, %s (in %s)' % (std, threading.current_thread().name))


def process_thread(name):
    # 綁定ThreadLocal的student:
    local_school.student = name
    process_student()


t1 = threading.Thread(target=process_thread, args=('yongGe',), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('老王',), name='Thread-B')
t1.start()
t2.start()
  • 運行結果
Hello, yongGe (in Thread-A)
Hello, 老王 (in Thread-B)

小結
一個ThreadLocal變量雖然是全局變量,但每個線程都只能讀寫自己線程的獨立副本,互不干擾。ThreadLocal解決了參數在一個線程中各個函數之間互相傳遞的問題

12. 異步

  • 同步調用就是你 喊 你朋友吃飯 ,你朋友在忙 ,你就一直在那等,等你朋友忙完了 ,你們一起去
  • 異步調用就是你 喊 你朋友吃飯 ,你朋友說知道了 ,待會忙完去找你 ,你就去做別的了。
  • 代碼
from multiprocessing import Pool
import time
import os


def test():
    print("---進程池中的進程---pid=%d,ppid=%d--" % (os.getpid(), os.getppid()))
    for i in range(3):
        print("----%d---" % i)
        time.sleep(1)
    return "老王"


def test2(args):
    print('1...')
    time.sleep(10)
    print("---callback func--pid=%d" % os.getpid())
    print("---callback func--args=%s" % args)
    print('2...')

if __name__ == '__main__':

    pool = Pool(3)
    #callback表示前面的func方法執行完,再執行callback,并且可以獲取func的返回值作為callback的參數
    pool.apply_async(func=test, callback=test2)
    #pool.apply_async(func=test)

    #模擬主進程在做任務
    time.sleep(5)

    print("----主進程-pid=%d.....服務器是不關閉的----" % os.getpid())
  • 運行結果
---進程池中的進程---pid=8808,ppid=8072--
----0---
----1---
----2---
1...
----主進程-pid=8072.....服務器是不關閉的----
---callback func--pid=8072
---callback func--args=老王
2...
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內容