Python 系統(tǒng)編程 進(jìn)程與線程(2)

1. 多線程-threading

Python的標(biāo)準(zhǔn)庫(kù)提供了兩個(gè)模塊:_thread和threading,_thread是低級(jí)模塊,threading是高級(jí)模塊,對(duì)_thread進(jìn)行了封裝。絕大多數(shù)情況下,我們只需要使用threading這個(gè)高級(jí)模塊。
啟動(dòng)一個(gè)線程就是把一個(gè)函數(shù)傳入并創(chuàng)建Thread實(shí)例,然后調(diào)用start()開始執(zhí)行:

  • 單線程執(zhí)行
import time

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

if __name__ == "__main__":
    for i in range(5):
        saySorry()
  • 運(yùn)行結(jié)果
>>>
親愛的,我錯(cuò)了,我能吃飯了嗎?Mon Jun 12 17:08:33 2017
親愛的,我錯(cuò)了,我能吃飯了嗎?Mon Jun 12 17:08:34 2017
親愛的,我錯(cuò)了,我能吃飯了嗎?Mon Jun 12 17:08:35 2017
親愛的,我錯(cuò)了,我能吃飯了嗎?Mon Jun 12 17:08:36 2017
親愛的,我錯(cuò)了,我能吃飯了嗎?Mon Jun 12 17:08:37 2017
  • 多線程執(zhí)行
import time
import threading
def saySorry():
    print("親愛的,我錯(cuò)了,我能吃飯了嗎? 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()
  • 運(yùn)行結(jié)果
>>>
MainThread
親愛的,我錯(cuò)了,我能吃飯了嗎? name=Thread-1, Mon Jun 12 19:15:02 2017
親愛的,我錯(cuò)了,我能吃飯了嗎? name=Thread-2, Mon Jun 12 19:15:02 2017
親愛的,我錯(cuò)了,我能吃飯了嗎? name=Thread-3, Mon Jun 12 19:15:02 2017
親愛的,我錯(cuò)了,我能吃飯了嗎? name=Thread-4, Mon Jun 12 19:15:02 2017
親愛的,我錯(cuò)了,我能吃飯了嗎? name=Thread-5, Mon Jun 12 19:15:02 2017
#多線程的時(shí)候可以看出來(lái)是同時(shí)執(zhí)行的

說(shuō)明

  1. 可以明顯看出使用了多線程并發(fā)的操作,花費(fèi)時(shí)間要短很多
  2. 創(chuàng)建好的線程,需要調(diào)用start()方法來(lái)啟動(dòng)

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

主線程會(huì)等待所有的子線程結(jié)束后才結(jié)束

查看線程數(shù)量

length = len(threading.enumerate())
print('當(dāng)前運(yùn)行的線程數(shù)為:%d' % length)

2. threading注意點(diǎn)

1.2.2.1線程執(zhí)行代碼的封裝
通過(guò)上一小節(jié),能夠看出,通過(guò)使用threading模塊能完成多任務(wù)的程序開發(fā),為了讓每個(gè)線程的封裝性更完美,所以使用threading模塊時(shí),往往會(huì)定義一個(gè)新的子類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屬性中保存的是當(dāng)前線程的名字
            print(msg)


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

說(shuō)明
python的threading.Thread類有一個(gè)run方法,用于定義線程的功能函數(shù),可以在自己的線程類中覆蓋該方法。而創(chuàng)建自己的線程實(shí)例后,通過(guò)Thread類的start方法,可以啟動(dòng)該線程,交給python虛擬機(jī)進(jìn)行調(diào)度,當(dāng)該線程獲得執(zhí)行的機(jī)會(huì)時(shí),就會(huì)調(diào)用run方法執(zhí)行線程。

  • 線程的執(zhí)行順序
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()
  • 運(yùn)行結(jié)果(運(yùn)行的結(jié)果可能不一樣,但是大體是一致的)
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

說(shuō)明
從代碼和執(zhí)行結(jié)果我們可以看出,多線程程序的執(zhí)行順序是不確定的。當(dāng)執(zhí)行到sleep語(yǔ)句時(shí),線程將被阻塞(Blocked),到sleep結(jié)束后,線程進(jìn)入就緒(Runnable)狀態(tài),等待調(diào)度。而線程調(diào)度將自行選擇一個(gè)線程執(zhí)行。上面的代碼中只能保證每個(gè)線程都運(yùn)行完整個(gè)run函數(shù),但是線程的啟動(dòng)順序、run函數(shù)中每次循環(huán)的執(zhí)行順序都不能確定。
總結(jié)

  1. 每個(gè)線程一定會(huì)有一個(gè)名字,盡管上面的例子中沒有指定線程對(duì)象的name,但是python會(huì)自動(dòng)為線程指定一個(gè)名字。
  2. 當(dāng)線程的run()方法結(jié)束時(shí)該線程完成。
  3. 無(wú)法控制線程調(diào)度程序,但可以通過(guò)別的方式來(lái)影響線程調(diào)度的方式。
  4. 線程的幾種狀態(tài)

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

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

4. 進(jìn)程VS線程

定義

  • 進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位.
  • 線程是進(jìn)程的一個(gè)實(shí)體,是CPU調(diào)度和分派的基本單位,它是比進(jìn)程更小的能獨(dú)立運(yùn)行的基本單位.線程自己基本上不擁有系統(tǒng)資源,只擁有一點(diǎn)在運(yùn)行中必不可少的資源(如程序計(jì)數(shù)器,一組寄存器和棧),但是它可與同屬一個(gè)進(jìn)程的其他的線程共享進(jìn)程所擁有的全部資源.

區(qū)別

  • 一個(gè)程序至少有一個(gè)進(jìn)程,一個(gè)進(jìn)程至少有一個(gè)線程.
  • 線程的劃分尺度小于進(jìn)程(資源比進(jìn)程少),使得多線程程序的并發(fā)性高。
  • 進(jìn)程在執(zhí)行過(guò)程中擁有獨(dú)立的內(nèi)存單元,而多個(gè)線程共享內(nèi)存,從而極大地提高了程序的運(yùn)行效率
  • 線程不能夠獨(dú)立執(zhí)行,必須依存在進(jìn)程中

優(yōu)缺點(diǎn)

  • 線程和進(jìn)程在使用上各有優(yōu)缺點(diǎn):線程執(zhí)行開銷小,但不利于資源的管理和保護(hù);而進(jìn)程正相反。

5. 同步的概念

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

6. 互斥鎖

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

import threading

#創(chuàng)建鎖
myLock = threading.Lock()
print(myLock)
print('1...')
#鎖住,如果此鎖,已經(jīng)鎖了,如果再鎖,會(huì)阻塞,直到開鎖了,才能再鎖
#myLock.acquire()
myLock.acquire()
print('2...')
#開鎖,如果鎖住了,可以開鎖。如果沒鎖,直接開,報(bào)錯(cuò)
myLock.release()
print('3...')
myLock.acquire()
print('4...')
  • 運(yùn)行結(jié)果
>>>
<unlocked _thread.lock object at 0x0000000000AEBC60>
1...
2...
3...
4...

其中,鎖定方法acquire可以有一個(gè)blocking參數(shù)。

  • 如果設(shè)定blocking為True,則當(dāng)前線程會(huì)堵塞,直到獲取到這個(gè)鎖為止(如果沒有指定,那么默認(rèn)為True)
  • 如果設(shè)定blocking為False,則當(dāng)前線程不會(huì)堵塞

例子

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()
  • 運(yùn)行結(jié)果
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的時(shí)候,cpu權(quán)限切換到fun2,此時(shí)num已經(jīng)在瘋狂運(yùn)算了,所以打印的時(shí)候不是1000000.如果沒有互斥鎖的話,最后這個(gè)fun2打印出來(lái)的不會(huì)是這個(gè)值。

例子2:簡(jiǎn)單的售票,同步鎖完成售票

import threading
import time
import os


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


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


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


def main():
    # 總共設(shè)置了3個(gè)線程
    for k in range(3):
        # 創(chuàng)建線程; Python使用threading.Thread對(duì)象來(lái)代表線程
        new_thread = threading.Thread(target=booth, args=(k,))
        # 調(diào)用start()方法啟動(dòng)線程
        new_thread.start()

if __name__ == '__main__':
    main()
  • 運(yùn)行結(jié)果
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

總結(jié)
鎖的好處:

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

7. 多線程-非共享數(shù)據(jù)

對(duì)于全局變量,在多線程中要格外小心,否則容易造成數(shù)據(jù)錯(cuò)亂的情況發(fā)生

在多線程開發(fā)中,全局變量是多個(gè)線程都共享的數(shù)據(jù),而局部變量等是各自線程的,是非共享的

8. 死鎖

  • 在線程間共享多個(gè)資源的時(shí)候,如果兩個(gè)線程分別占有一部分資源并且同時(shí)等待對(duì)方的資源,就會(huì)造成死鎖。
  • 盡管死鎖很少發(fā)生,但一旦發(fā)生就會(huì)造成應(yīng)用的停止響應(yīng)。

避免死鎖

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

9. 同步應(yīng)用

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

    還有一種實(shí)現(xiàn)形式:輪循,效率低
'''

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創(chuàng)建出的鎖默認(rèn)沒有“鎖上”
lock1 = Lock()
# 創(chuàng)建另外一把鎖,并且“鎖上”
lock2 = Lock()
lock2.acquire()
# 創(chuàng)建另外一把鎖,并且“鎖上”
lock3 = Lock()
lock3.acquire()

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

    t1.start()
    t2.start()
    t3.start()
  • 運(yùn)行結(jié)果
------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 -----
...省略...

總結(jié)

  • 可以使用互斥鎖完成多個(gè)任務(wù),有序的進(jìn)程工作,這就是線程的同步

10. 生產(chǎn)者與消費(fèi)者模式

  1. 隊(duì)列
    先進(jìn)先出

  2. 先進(jìn)后出

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

'''
    隊(duì)列:
    1、進(jìn)程之間的通信:       q = multiprocessing.Queue()
    2、進(jìn)程池之間的通信:     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 = '生成產(chǎn)品' + 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 + '消費(fèi)了 ' + queue.get()
                    print(msg)
            time.sleep(1)

#全局變量
queue = Queue()

if __name__ == '__main__':


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

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

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

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

11. ThreadLocal

在多線程環(huán)境下,每個(gè)線程都有自己的數(shù)據(jù)。一個(gè)線程使用自己的局部變量比使用全局變量好,因?yàn)榫植孔兞恐挥芯€程自己能看見,不會(huì)影響其他線程,而全局變量的修改必須加鎖。

  • 使用ThreadLocal的方法
    ThreadLocal應(yīng)運(yùn)而生,不用查找dict,ThreadLocal幫你自動(dòng)做這件事:
'''
    threadlocal就有倆功能:

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

import threading

# 創(chuàng)建全局ThreadLocal對(duì)象:
local_school = threading.local()


def process_student():
    # 獲取當(dāng)前線程關(guān)聯(lián)的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()
  • 運(yùn)行結(jié)果
Hello, yongGe (in Thread-A)
Hello, 老王 (in Thread-B)

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

12. 異步

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


def test():
    print("---進(jìn)程池中的進(jìn)程---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方法執(zhí)行完,再執(zhí)行callback,并且可以獲取func的返回值作為callback的參數(shù)
    pool.apply_async(func=test, callback=test2)
    #pool.apply_async(func=test)

    #模擬主進(jìn)程在做任務(wù)
    time.sleep(5)

    print("----主進(jìn)程-pid=%d.....服務(wù)器是不關(guān)閉的----" % os.getpid())
  • 運(yùn)行結(jié)果
---進(jìn)程池中的進(jìn)程---pid=8808,ppid=8072--
----0---
----1---
----2---
1...
----主進(jìn)程-pid=8072.....服務(wù)器是不關(guān)閉的----
---callback func--pid=8072
---callback func--args=老王
2...
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 1.進(jìn)程和線程 隊(duì)列:1、進(jìn)程之間的通信: q = multiprocessing.Queue()2、...
    一只寫程序的猿閱讀 1,135評(píng)論 0 17
  • 線程 1.同步概念 1.多線程開發(fā)可能遇到的問(wèn)題 同步不是一起的意思,是協(xié)同步調(diào) 假設(shè)兩個(gè)線程t1和t2都要對(duì)nu...
    TENG書閱讀 628評(píng)論 0 1
  • 又來(lái)到了一個(gè)老生常談的問(wèn)題,應(yīng)用層軟件開發(fā)的程序員要不要了解和深入學(xué)習(xí)操作系統(tǒng)呢? 今天就這個(gè)問(wèn)題開始,來(lái)談?wù)劜?..
    tangsl閱讀 4,176評(píng)論 0 23
  • 第三章 Java內(nèi)存模型 3.1 Java內(nèi)存模型的基礎(chǔ) 通信在共享內(nèi)存的模型里,通過(guò)寫-讀內(nèi)存中的公共狀態(tài)進(jìn)行隱...
    澤毛閱讀 4,384評(píng)論 2 22
  • 你是那種凡事將就、得過(guò)且過(guò)的人,還是做事精益求精、追求最好的人?如果你是后者,那么你肯定會(huì)對(duì)《如何把事情做到最好》...
    葡萄的小叮當(dāng)閱讀 802評(píng)論 0 4