Py併發編程及應用(多線程、多進程;異步、全局鎖等)

# -*- coding: utf-8 -*-

'''
Py併發編程及應用.py
(多線程:創建、同步鎖、線程通信、線程池、線程局部變量、線程定時器、任務調度器)
(多進程:創建、進程池、進程通信、併發及異步併發爬蟲、全局解釋器鎖、垃圾回收機制)



深入:
1、線程鎖、線程通信的概念和運用
2、任務調度器的運用
3、asyncio模塊   異步併發模塊(爬蟲應用)
4、aiohttp模塊   異步的requests(爬蟲應用)



注意:
1、併發和並行是兩個概念,
並行指在同一時刻有多條指令在多個處理器上同時執行;
併發是指在同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,使得在宏觀上具有多個進程同時執行的效果。

2、默認情況下,主線程的名字爲 MainThread,用戶啓動的多個線程的名字依次爲 Thread-1、Thread-2、Thread-3、...、Thread-n 等。
3、注意線程執行完成後已經死亡狀態,再次執行將引發 RuntimeError異常。另外對處於新建狀態的線程兩次調用start()方法也會引發異常。
4、如果要將某個線程設置爲後臺線程,則必須在該線程啓動之前進行設置。也就是說,將 daemon 屬性設爲 True,必須在 start() 方法調用之前進行,否則會引發 RuntimeError 異常。
5、加鎖操作RLock較爲常用,注意加鎖方式一般爲 先加鎖,然後try操作,最後finally解鎖。
6、注意解決死鎖問題的方式有:1、避免多次加鎖 2、相同的加鎖順序 3、使用定時鎖 即調用acquire()方法加鎖時候指定timeout參數  4、死鎖檢測
7、注意線程池返回的Future對象方法result()會阻塞當前主線程;另外線程池類方法map()後可循環迭代每一個線程
8、Timer定時器只能控制函數在指定時間內執行一次。
如果要使用 Timer 控制函數多次重複執行,則需要函數內嵌套定時器 來循環執行下一次調度。

9、創建子進程時,multiprocessing.Process類通過實例化對象指定target參數執行函數 在IDLE裏不顯示函數執行內容。
通過在控制檯通過執行文件可顯示target參數執行函數的內容。
10、通過 multiprocessing.Process 來創建並啓動進程時,程序必須先判斷if __name__=='__main__':,否則可能引發異常。
11、進程通信中,使用實例化創建multiprocessing.Pipe類管道對象返回兩個PipeConnection對象,
其中前一個PipeConnection對象用於接受數據,後一個PipeConnection對象用於發送數據

12、GIL 不能絕對保證線程安全
因爲即便 GIL 僅允許一個 Python 線程執行,但Python 還有 check interval 這樣的搶佔機制。



使用:
第一部分:多線程相關
threading模塊    線程封裝

一、Python 主要通過兩種方式來創建線程:
1、使用 threading 模塊中的 Thread 類的構造器創建線程。
即直接對類 threading.Thread 進行實例化,並調用實例化對象的 start 方法創建線程。
2、繼承 threading 模塊中的 Thread 類創建線程類。
即用 threading.Thread 派生出一個新的子類,將新建類實例化,並調用其 start 方法創建線程。

二、Lock類 和 Rlock類 互斥鎖  用來解決數據不同步的問題
threading模塊提供了 Lock 和 RLock 兩個類,都各自提供兩個方法來 加互斥鎖 和 解除互斥鎖

三、線程通信
當線程在系統中運行時,線程的調度具有一定的透明性,通常程序無法準確控制線程的輪換執行,如果有需要,Python 可通過線程通信來保證線程協調運行。
線程通信的實現方式:
1、Condition類   來自threading模塊
2、queue模塊     隊列
3、Event類       來自threading模塊

四、線程池 ThreadPoolExecutor類
Executor 提供了兩個子類,即 ThreadPoolExecutor 和 ProcessPoolExecutor,
其中 ThreadPoolExecutor類 用於創建線程池,而 ProcessPoolExecutor類 用於創建進程池。
1、線程池類方法(submit()、map()、shudown())
2、submit()類方法提交線程池後返回的Future對象方法
Future對象用result()方法來獲取線程任務的運回值,但該方法會阻塞當前主線程,只有等到錢程任務完成後,result() 方法的阻塞纔會被解除。
可通過 Future 的 add_done_callback() 方法來添加回調函數來避免直接調用result()方法阻塞線程
另外線程池類方法map()後可循環迭代每一個線程

五、線程局部變量  threading.local()

六、定時器  Timer子類    來自threading模塊

七、任務調度器  sched模塊  任務調度器模塊
1、實例化任務調度器 sched.scheduler()
2、實例化任務調度器方法:絕對時間執行函數任務、相對時間執行函數任務、取消任務等


第二部分:多進程相關
multiprocessing模塊 進程封裝

八、multiprocessing.Process類    進程黃建
Python 在 multiprocessing模塊下提供了 Process類 來創建新進程。
與 Thread 類似的是,使用 Process 創建新進程也有兩種方式:
1、使用 multiprocessing模塊 中的 Process類 來實例化對象創建新進程,並指定函數作爲target參數函數
2、繼承 Process類 ,並重寫他的 run() 方法來創建進程類,程序創建Process子類的實例作爲進程

九、進程啓動的三種方式  spawn(windows僅支持此方式)、fork、forkserver
multiprocessing.set_start_method(' ')函數用於設置啓動進程的方式,設置代碼必須放在多進程代碼之前。

十、進程池    multiprocessing.Pool類
通過 multiprocessing 模塊的 Pool類來實例化創建進程池,然後通過進程池方法提交進程池、關閉進程池、等待進程池等

十一、進程通信    multiprocessing.Queue類  和 multiprocessing.Pipe類       
Python 爲進程通信提供了兩種機制:
Queue:一個進程向 Queue 中放入數據,另一個進程從 Queue 中讀取數據。
Pipe:Pipe 代表連接兩個進程的管道。程序在調用 Pipe() 函數時會產生兩個連接端,分別交給通信的兩個進程,接下來進程既可從該連接端讀取數據,也可向該連接端寫入數據。

十二、異步併發模塊的性能提升需要後期單獨重點深入
asyncio模塊   異步併發模塊
aiohttp模塊   異步的requests

十三、垃圾回收機制    垃圾回收機制引用計數機制    
引用計數機制 即:對象的引用計數值爲 0 時,說明這個對象永不再用,自然它就變成了垃圾,需要被回收。
gc模塊 手動啓動垃圾回收機制


'''

# =============================================================================
# #threading模塊  線程封裝
# #threading模塊  Python 3 之後的線程模塊,提供了功能豐富的多線程支持
# =============================================================================
#Python 主要通過兩種方式來創建線程:
#1、使用 threading 模塊中的 Thread 類的構造器創建線程。
#即直接對類 threading.Thread 進行實例化,並調用實例化對象的 start 方法創建線程。
#2、繼承 threading 模塊中的 Thread 類創建線程類。
#即用 threading.Thread 派生出一個新的子類,將新建類實例化,並調用其 start 方法創建線程。

import threading


help(threading)
threading.__doc__
threading.__file__
threading.__all__
dir(threading)


#使用 threading 模塊中的 Thread 類的構造器創建線程。
#__init__(self, group=None, target=None, name=None, args=(), kwargs=None, *,daemon=None)

#構造器涉及如下幾個參數:
#group:      指定該線程所屬的線程組。目前該參數還未實現,因此它只能設爲 None。
#target:     指定該線程要調度的目標方法。
#args:       指定一個元組,以位置參數的形式爲 target 指定的函數傳入參數。元組的第一個元素傳給 target 函數的第一個參數,元組的第二個元素傳給 target 函數的第二個參數……依此類推。
#kwargs:     指定一個字典,以關鍵字參數的形式爲 target 指定的函數傳入參數。
#daemon:     指定所構建的線程是否爲後臺線程  即 守護線程。

help(threading.Thread)
threading.Thread.__doc__
dir(threading.Thread)


help(threading.Thread.start)
help(threading.Thread.run)
help(threading.Thread.daemon)
help(threading.Thread.join)



####################
#創建線程方法一:  通過Thread類的構造器創建並啓動多線程的步驟
#1、調用 Thread 類的構造器創建線程對象。在創建線程對象時,target參數 指定的函數將作爲線程執行體。
#2、調用線程對象的 start() 方法啓動該線程。

import threading

#先定義一個普通的action函數,準備作爲線程執行體  即:target參數函數
def action(max):
    for i in range(max):
        #threading.current_thread()獲取當前線程
        #.getName()獲取名稱
        print(threading.current_thread().getName() + ' ' + str(i))       #current_thread()獲取當前線程;#getName()獲取當前線程的名字

#主線程的執行體
for i in range(100):
    #輸出當前線程的名稱
    print(threading.current_thread().getName() + ' ' + str(i))
    if i == 20:
        #創建並啓動第一個線程
        #target參數  指定函數作爲線程執行體,
        #args參數    元組形式位置參數,用於指定爲target參數的函數的參數。
        t1=threading.Thread(target = action, args=(100,))
        t1.start()
        
        #創建並啓動第二個線程
        t2=threading.Thread(target = action, args=(100,))
        t2.start()
#注意:  
#默認情況下,主線程的名字爲 MainThread,用戶啓動的多個線程的名字依次爲 Thread-1、Thread-2、Thread-3、...、Thread-n 等。
print('主線程執行完成!')


#Threading 模塊中,除了 current_thread() 函數外,還經常使用如下 2 個函數:
#threading.enumerate():      返回一個正運行線程的 list。“正運行”是指線程處於“啓動後,且在結束前”狀態,不包括“啓動前”和“終止後”狀態。
#threading.activeCount():    返回正在運行的線程數量。與 len(threading.enumerate()) 有相同的結果



####################
#創建線程方法二:  通過繼承 Thread 類來創建並啓動線程的步驟
#1、定義 Thread 類的子類,並重寫該類的 run() 方法。
#run() 方法的方法體就代表了線程需要完成的任務,因此把 run() 方法稱爲線程執行體。
#2、創建 Thread 子類的實例,即創建線程對象。
#3、調用線程對象的 start() 方法來啓動線程。

import threading

#先定義類繼承Thread類,來準備實例化創建和啓動線程
class FkThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.i = 0
    #重寫run()方法,run()方法代表線程需要完成的任務,成爲線程執行體
    def run(self):
        while self.i < 100:
            print(threading.current_thread().getName() + ' ' + str(self.i))
            self.i += 1

for i in range(100):
    print(threading.current_thread().getName() + ' ' + str(i))
    if i == 20:
        
        #實例化來創建第一個線程 然後 啓動線程
        ft1 = FkThread()
        ft1.start()
        
        #實例化來創建第二條線程 然後 啓動線程
        ft2 = FkThread()
        ft2.start()

print('主線程執行完成!')



####################
#線程的狀態

##########
#線程的新建狀態  和  線程的就緒狀態
#啓動線程使用start()方法,而不是run()方法
#調用 start() 方法來啓動線程,系統會把該 run() 方法當成線程執行體來處理;
#在調用線程對象的 start() 方法之後,該線程立即進入就緒狀態(相當於“等待執行”),但該線程並未真正進入運行狀態。

#但如果直接調用線程對象的 run() 方法,則 run() 方法立即就會被執行,而且在該方法返回之前其他線程無法併發執行。

import threading

# 先定義函數  準備作爲線程執行體的target參數函數
def action(max):
    for i in range(max):
        #直接調用run()方法時,Thread的name屬性返回的是該對象的名字
        #而不是當前線程的名字
        #使用threading.current_thread().name總是獲取當前線程的名字
        print(threading.current_thread().name +  " " + str(i))
        
for i in range(100):
    #調用Thread的currentThread()方法獲取當前線程
    print(threading.current_thread().name +  " " + str(i))
    if i == 20:
        # 直接調用線程對象的run()方法
        # 系統會把線程對象當成普通對象,把run()方法當成普通方法
        # 所以下面兩行代碼並不會啓動兩個線程,而是依次執行兩個run()方法
        threading.Thread(target=action,args=(100,)).run()
        threading.Thread(target=action,args=(100,)).run()


##########
#線程的運行狀態
#如果處於就緒狀態的線程獲得了 CPU,開始執行 run() 方法的線程執行體,則該線程處於運行狀態。


##########
#線程的阻塞狀態
#線程將會進入阻塞狀態的幾種情況:
#1、線程調用 sleep() 方法主動放棄其所佔用的處理器資源。
#2、線程調用了一個阻塞式 I/O 方法,在該方法返回之前,該線程被阻塞。
#3、線程試圖獲得一個鎖對象,但該鎖對象正被其他線程所持有。關於鎖對象的知識,後面將有更深入的介紹。
#4、線程在等待某個通知(Notify)。


##########
#線程的死亡狀態
#線程結束後就處於死亡狀態。線程結束有兩種方式:一種是run()方法或代表線程執行體的targe函數執行完成。另一種是出現異常。

#is_alive()方法  測試某個顯示是否已經死亡。
#當線程處於就緒、運行、阻塞三種狀態時,該方法將返回 True;當線程處於新建、死亡兩種狀態時,該方法將返回 False。
import threading

#先定義一個函數  用於準備作爲線程執行體target參數函數來使用
def action(max):
    '''
    先定義一個函數  用於準備作爲線程執行體target參數函數來使用
    循環輸出當前線程名稱100次
    '''
    for i in range(max):
        print(threading.current_thread().name + ' ' + str(i))

#創建線程對象    進入新建狀態
sd=threading.Thread(target= action, args=(100,))

for i in range(300):
    print(threading.current_thread().name + ' ' + str(i))
    if i == 20:
        #啓動線程    進入就緒狀態
        sd.start()
        print(sd.is_alive())    #當線程處於就緒、運行、阻塞三種狀態時,該方法將返回 True;當線程處於新建、死亡兩種狀態時,該方法將返回 False。

#    if i > 20:
#        #注意線程執行完成後已經死亡狀態,再次執行將引發 RuntimeError異常。另外對處於新建狀態的線程兩次調用start()方法也會引發異常。
#        sd.start()



####################
#join()方法  
#join()方法讓一個線程等待另一個線程完成。
#當某個程序執行流中調用其他線程的 join() 方法時候,調用線程將被阻塞,直到被join()方法加入的join線程執行完成。

import threading

#先定義一個函數  用於準備作爲線程執行體target參數函數來使用
def action(max):
    '''
    先定義一個函數  用於準備作爲線程執行體target參數函數來使用
    循環輸出當前線程名稱100次
    '''
    for i in range(max):
        print(threading.current_thread().name + ' ' + str(i))

#直接新建並啓動 子線程
threading.Thread(target=action,args=(100,),name='新線程').start()

#演示join()方法  
for i in range(100):
    if i == 20:
        #演示阻塞第20次執行線程,等運行完 被join的線程後 繼續運行20次以後的線程
        jt=threading.Thread(target=action,args=(100,),name='被join的線程')
        jt.start()
        
        #join(timeout=None)方法 還可以指定一個 timeout 參數,該參數指定等待被 join 的線程的時間最長爲 timeout 秒。如果在 timeout 秒內被 join 的線程還沒有執行結束,則不再等待
        jt.join()
    print(threading.current_thread().name + ' ' + str(i))
    


####################
#後臺線程(Daemon Thread)”,又稱爲“守護線程”或“精靈線程。調用 Thread 對象的 daemon 屬性可以將指定線程設置成後臺線程

#創建後臺線程有兩種方式:
#1、主動將線程的 daemon屬性 設置爲 True
#2、也可在創建Thread對象時通過daemon參數將其設爲後臺線程  即 守護線程。

#後臺線程有一個特徵,如果所有的前臺線程都死亡了,那麼後臺線程會自動死亡。
#Python 解釋器的垃圾回收線程就是典型的後臺線程。
    
import threading

#先定義一個函數  用於準備作爲線程執行體target參數函數來使用
def action(max):
    '''
    先定義一個函數  用於準備作爲線程執行體target參數函數來使用
    循環輸出當前線程名稱100次
    '''
    for i in range(max):
        print(threading.current_thread().name + ' ' + str(i))

t=threading.Thread(target=action,args=(100,),name='後臺線程')

# 將此線程設置成後臺線程,通過設置線程的daemon屬性爲 True
# 也可在創建Thread對象時通過daemon參數將其設爲後臺線程

#注意:如果要將某個線程設置爲後臺線程,則必須在該線程啓動之前進行設置。
#也就是說,將 daemon 屬性設爲 True,必須在 start() 方法調用之前進行,否則會引發 RuntimeError 異常。
t.daemon=True    
t.start()

for i in range(10):
    print(threading.current_thread().name + ' ' +str(i))











# =============================================================================
# #Lock類 和 Rlock類 互斥鎖  用來解決數據不同步的問題
# #threading模塊提供了 Lock 和 RLock 兩個類,都各自提供兩個方法來 加互斥鎖 和 解除互斥鎖
# =============================================================================


##########
#銀行取錢  數據不同步  問題示例

#先定義一個賬戶類,並且封裝賬戶編號 和 賬戶餘額
class Account:
    '''封裝賬戶編號 和 餘額 這兩個成員屬性'''
    def __init__(self,account_no,balance):
        self.account_no = account_no
        self.balance = balance

import threading
import time
#定義一個函數  來準備作爲線程的執行函數   來模擬取錢操作
def draw(account,draw_amount):
    '''傳入參數 實例化賬戶  和 取錢金額;來實現取錢操作,如果實例化賬戶餘額足夠則取款成功,否則取款失敗'''
    if account.balance >= draw_amount:
        print(threading.current_thread().name + '取錢成功,吐出鈔票:' + str(draw_amount))
        #time.sleep(0.2)
        account.balance -= draw_amount
        print('餘額爲:' + str(account.balance))
    else:
        print(threading.current_thread().name + '取錢失敗,餘額不足!')       

#創建實例化 賬戶對象
acct= Account('1234567',1000)

#啓動兩個線程模擬對同一個實例化 的賬戶對象進行取錢操作
#多次運行,則會出現 餘額不足但仍取款成功 的問題
#原由線程的 run()方法不具有線程安全性
threading.Thread(name='甲', target=draw, args=(acct,800)).start()
threading.Thread(name='乙', target=draw, args=(acct,800)).start()



##########
#Lock類 和 RLock類 的相同:  加鎖和解鎖方法
#1、acquire(blocking=True, timeout=-1):    請求對 Lock 或 RLock 加鎖,其中 timeout 參數指定加鎖多少秒。
#2、release():                             釋放鎖。

#Lock 和 RLock 的不同:    
#1、threading.Lock:   #它是一個基本的鎖對象,每次只能鎖定一次,其餘的鎖請求,需等待鎖釋放後才能獲取。
#2、threading.RLock:  #它代表可重入鎖(Reentrant Lock)。對於可重入鎖,在同一個線程中可以對它進行多次鎖定,也可以多次釋放。
#如果使用 RLock,那麼 acquire() 和 release() 方法必須成對出現。
#如果調用了 n 次 acquire() 加鎖,則必須調用 n 次 release() 才能釋放鎖。

import threading

help(threading.Lock)
help(threading.RLock)

#示例
#修改Account類,加鎖,來讓其線程安全
#修改的該類中,定義了一個 RLock 對象。在程序中實現 draw() 方法時,進入該方法開始執行後立即請求對 RLock 對象加鎖,
#當執行完 draw() 方法的取錢邏輯之後,程序使用 finally 塊來確保釋放鎖。
class Account:
    def __init__(self,account_no, balance):
        self.account_no = account_no
        self._balance = balance
        self.lock = threading.RLock()
    
    def getBalance(self):
        return self._balance
    
    def draw(self,draw_amount):
        #加鎖操作
        #加鎖操作RLock較爲常用,注意加鎖方式一般爲 先加鎖,然後try操作,最後finally解鎖。
        self.lock.acquire()
        try:
            if self._balance >= draw_amount:
                print(threading.current_thread().name + '取錢成功,吐出鈔票:' + str(draw_amount))
                time.sleep(0.001)
                #取錢成功後修改餘額
                self._balance -= draw_amount
                print('\t餘額爲:' + str(self._balance))
            else:
                print(threading.current_thread().name + '取錢失敗,餘額不足!')
        finally:
            #解鎖操作
            self.lock.release()

#定義函數  準備作爲線程的target參數執行函數  來模擬取錢操作
def draw(account,draw_amount):
    account.draw(draw_amount)

acct=Account('123456789',1000)
threading.Thread(name='甲', target=draw, args=(acct,800)).start()
threading.Thread(name='乙', target=draw, args=(acct,800)).start()




##########
#死鎖
#當兩個線程相互等待對方釋放同步監視器時,就會發生死鎖。
#Py解釋器沒有監測,也沒有采取措施來處理死鎖情況,所以在進行多線程時應該採取措施避免出現死鎖。
#注意:一旦出現死鎖,整個程序既不會發生任何異常,也不會給出任何提示,只是所有線程都處於阻塞狀態,無法繼續。

#死鎖示例
#在系統中出現多個同步監視器的情況下,很容易發生死鎖
import threading
import time

class A:
    def __init__(self):
        self.lock=threading.RLock()
    def foo(self,b):
        try:
            #加鎖操作
            self.lock.acquire()
            print('當前線程名:' + threading.current_thread().name + '進入A實例的foo()方法')
            time.sleep(0.02)
            print('當前線程名:' + threading.current_thread().name + '企圖調用B實例的last()方法')
            
            #調用另一個類實例方法中也有加鎖解鎖操作,容易發生死鎖問題
            b.last()    
        finally:
            #解鎖操作
            self.lock.release()     
    def last(self):
        try:
            self.lock.acquire()
            print('進入了A類的last()方法內部')
        finally:
            self.lock.release()

class B:
    def __init__(self):
        self.lock=threading.RLock()
    def bar(self,a):
        try:
            self.lock.acquire()
            print('當前線程名:' + threading.current_thread().name + '進入了B實例的bar()方法')
            time.sleep(0.2)
            print('當前線程名:' + threading.current_thread().name + '企圖調用A實例的last()方法')
            a.last()
        finally:
            self.lock.release()
    def last(self):
        try:
            self.lock.acquire()
            print('進入了B類的last()方法內部')
        finally:
            self.lock.release()               
a=A()
b=B()
def init():
    threading.current_thread().name='主線程'
    a.foo(b)
    print('進入了主線程之後')
def action():
    threading.current_thread().name='副線程'
    b.bar(a)
    print('進入了副線程之後')

#以action()爲target參數函數啓動新線程

#兩個線程開啓 即出現死鎖問題。
#注意解決死鎖問題的方式有:1、避免多次加鎖 2、相同的加鎖順序 3、使用定時鎖 即調用acquire()方法加鎖時候指定timeout參數  4、死鎖檢測
threading.Thread(target=action).start()
#init()    #若開啓則出現死鎖問題。











# =============================================================================
# #線程通信
# #當線程在系統中運行時,線程的調度具有一定的透明性,通常程序無法準確控制線程的輪換執行,如果有需要,Python 可通過線程通信來保證線程協調運行。
# #線程通信的實現方式:1、threading模塊的Condition類  2、queue隊列模塊  3、threading模塊的Event類
# =============================================================================

####################
#Condition類 
#來自threading線程模塊
#Condition對象可以讓那些己經得到 Lock 對象卻無法繼續執行的線程釋放 Lock 對象,
#Condition對象也可以喚醒其他處於等待狀態的線程


import threading

help(threading.Condition)
threading.Condition.__doc__

#Condition 類提供瞭如下幾個方法:
#acquire([timeout]) / release():  調用 Condition 關聯的 Lock 的 acquire() 或 release() 方法。
#wait([timeout]):                 導致當前線程進入 Condition 的等待池等待通知並釋放鎖,直到其他線程調用該 Condition 的 notify() 或 notify_all() 方法來喚醒該線程。在調用該 wait() 方法時可傳入一個 timeout 參數,指定該線程最多等待多少秒。
#notify():                        喚醒在該 Condition 等待池中的單個線程並通知它,收到通知的線程將自動調用 acquire() 方法嘗試加鎖。如果所有線程都在該 Condition 等待池中等待,則會選擇喚醒其中一個線程,選擇是任意性的。
#notify_all():                    喚醒在該 Condition 等待池中等待的所有線程並通知它們。


#示例    使用Condition()對象方法實現線程通信
import threading

class Account:
    '''
    定義一個Account 類,提供 draw() 和 deposit() 兩個方法,分別對應於該賬戶的取錢和存款操作。
    因爲這兩個方法可能需要併發修改 Account 類的 self.balance 成員變量的值,所以它們都使用 Lock 來控制線程安全。
    除此之外,這兩個方法還使用了 Condition 的 wait() 和 notify_all() 來控制線程通信。
    
    實現要求存款者和取錢者不斷地重複存款、取錢的動作,而且要求每當存款者將錢存入指定賬戶後,取錢者就立即取出該筆錢。
    不允許存款者連續兩次存錢,也不允許取錢者連續兩次取錢。
    '''
    def __init__(self,account_no, balance):
        #封裝賬戶編號 和 賬戶餘額 的兩個變量
        self.account_no = account_no
        self._balance = balance
        self.cond = threading.Condition()
        self._flag = False                        #定義代表是否已經存錢的旗幟
    
    def getBalance(self):
        #因爲賬戶餘額不允許隨便修改,所以只爲self._balance提供getter方法
        return self._balance
    
    def draw(self,draw_amount):
        #加鎖操作  Condition類對象加鎖操作,
        #Condition類對象的加鎖方法 相當於調用 Condition 綁定的Lock的acquire()
        self.cond.acquire()
        try:
            if not self._flag:
                #Condition類對象阻塞操作,將當前線程放入等待池 等待通知
                self.cond.wait()                  #如果沒有存錢,則將取錢方法阻塞 並放入等待池等待通知
            else:
                print(threading.current_thread().name + '取錢:' + str(draw_amount))
                self._balance -= draw_amount      #取錢 並 餘額變動
                print('賬戶餘額爲:' + str(self._balance))
                self._flag = False                #將標識賬戶是否已有存款的旗幟設爲 False
                #Condition類對象喚醒操作, 喚醒等待池中的其他所有線程 並 通知他們
                self.cond.notify_all()            #喚醒等待池中的其他所有線程 並 通知他們
        #解鎖操作  Condition類對象解鎖操作
        finally:
            self.cond.release()
            
    def deposit(self, deposit_amount):
        #加鎖操作  Condition類對象加鎖操作,
        #Condition類對象的加鎖方法 相當於調用 Condition 綁定的Lock的acquire()        self.cond.acquire()
        self.cond.acquire()
        try:
            if self._flag:
                #Condition類對象阻塞操作,將當前線程放入等待池 等待通知
                self.cond.wait()                  #如果已經存錢,則將存錢方法阻塞 並放入等待喫等待通知
            else:
                print(threading.current_thread().name + '存款:' + str(deposit_amount))
                self._balance += deposit_amount   #存錢 並 餘額變動
                print('賬戶餘額爲:' + str(self._balance))
                self._flag =  True                #將標識賬戶是否已有存款的旗幟設爲 True
                #Condition類對象喚醒操作, 喚醒等待池中的其他所有線程 並 通知他們
                self.cond.notify_all()            #喚醒等待池中的其他所有線程 並 通知他們
        #解鎖操作  Condition類對象解鎖操作
        finally:
            self.cond.release()


'''
程序使用 Condition 的 wait() 和 notify_all() 方法進行控制,
對存款者線程而言,當程序進入 deposit() 方法後,如果 self._flag 爲 True,則表明賬戶中已有存款,
程序調用 Condition 的 wait() 方法被阻塞;否則,程序向下執行存款操作,
當存款操作執行完成後,系統將 self._flag 設爲 True,然後調用 notify_all() 來喚醒其他被阻塞的線程。

如果系統中有存款者線程,存款者線程也會被喚醒,但該存款者線程執行到 ① 號代碼處時再次進入阻塞狀態,
只有執行 draw() 方法的取錢者線程纔可以向下執行。
同理,取錢者線程的運行流程也是如此
'''

#定義一個函數,作爲線程的target參數執行函數。  模擬重複max次執行取錢操作
def draw_many(account, draw_amount,max):
    for i in range(max):
        account.draw(draw_amount)

#定義一個函數,作爲線程的target參數執行函數。  模擬重複max次執行存錢操作
def deposit_many(account,deposit_amount,max):
    for i in range(max):
        account.deposit(deposit_amount)
        

acct=Account('123456789',0)                        #實例化創建一個賬戶
threading.Thread(name='取錢者 X ', target=draw_many, args=(acct,800,100)).start()
threading.Thread(name='存款者 甲 ', target=deposit_many, args=(acct,800,100)).start()
threading.Thread(name='存款者 乙 ', target=deposit_many, args=(acct,800,100)).start()
threading.Thread(name='存款者 丙 ', target=deposit_many, args=(acct,800,100)).start()




####################
#queue隊列模塊
#掌握Queue阻塞隊列的特性後,利用Queue來實現線程通信

import queue
help(queue)
queue.__doc__
queue.__all__
dir(queue)

#queue模塊的三個隊列類的簡單介紹如下:
#queue.Queue(maxsize=0):       代表 FIFO(先進先出)的常規隊列,maxsize 可以限制隊列的大小。如果隊列的大小達到隊列的上限,就會加鎖,再次加入元素時就會被阻塞,直到隊列中的元素被消費。如果將 maxsize 設置爲 0 或負數,則該隊列的大小就是無限制的。
#queue.LifoQueue(maxsize=0):   代表 LIFO(後進先出)的隊列,與 Queue 的區別就是出隊列的順序不同。
#PriorityQueue(maxsize=0):     代表優先級隊列,優先級最小的元素先出隊列。


#這三個隊列類的屬性和方法基本相同, 它們都提供瞭如下屬性和方法:
#
#Queue.qsize():                返回隊列的實際大小,也就是該隊列中包含幾個元素。
#Queue.empty():                判斷隊列是否爲空。
#Queue.full():                 判斷隊列是否已滿。
#
#Queue.put(item, block=True, timeout=None):向隊列中放入元素。
#如果隊列己滿,且 block 參數爲 True(阻塞),當前線程被阻塞,
#timeout 指定阻塞時間,如果將 timeout 設置爲 None,則代表一直阻塞,直到該隊列的元素被消費;
#如果隊列己滿,且 block 參數爲 False(不阻塞),則直接引發 queue.FULL 異常。
#
#Queue.put_nowait(item):       向隊列中放入元素,不阻塞。相當於在上一個方法中將 block 參數設置爲 False。
#
#Queue.get(item, block=True, timeout=None):從隊列中取出元素(消費元素)。
#如果隊列已滿,且 block 參數爲 True(阻塞),當前線程被阻塞,
#timeout 指定阻塞時間,如果將 timeout 設置爲 None,則代表一直阻塞,直到有元素被放入隊列中; 
#如果隊列己空,且 block 參數爲 False(不阻塞),則直接引發 queue.EMPTY 異常。
#
#Queue.get_nowait(item):       從隊列中取出元素,不阻塞。相當於在上一個方法中將 block 參數設置爲 False。




#示例  阻塞隊列線程
import queue

#先定義了一個大小爲 2 的 Queue,程序先向該隊列中放入兩個元素,此時隊列還沒有滿,兩個元素都可以被放入。
#當程序試圖放入第三個元素時,如果使用 put() 方法嘗試放入元素將會阻塞線程
bq=queue.Queue(2)              #實例化創建一個長度爲2的先進先出常規隊列
bq.put('Python')
bq.put('python')
print('11111111')
#bq.put('Python')               #阻塞線程
print('22222222')



#示例  掌握Queue阻塞隊列的特性後,利用Queue來實現線程通信
import threading
import time
import queue

def product(bq):
    #定義函數 用於線程的target參數執行函數
    #參數Queue隊列對象,函數內實現隊列的放入元素功能
    str_tuple=('Python','Kotlin','Swift')
    for i in range(9):
        print(threading.current_thread().name + '生產者準備生產元組元素...')
        time.sleep(0.2)
        #Queue.put()方法 向隊列中放入元素  如果隊列已滿則阻塞,直到元素被Queue.get()方法消費
        bq.put(str_tuple[i % 3])        #%求餘操作
        print(threading.current_thread().name + '生產者生產元組元素完成!')

def consume(bq):
    #定義函數 用於線程target參數執行函數
    #參數Queue隊列對象,函數內實現隊列的取出元素消費功能
    while True:
        print(threading.current_thread().name + '消費者準備消費元組元素...')
        time.sleep(0.2)
        #Queue.get()方法 從隊列中取出元素  如果隊列已空則阻塞,直到元素被Queue.put()方法存入
        t=bq.get()
        print(threading.current_thread().name + '消費者消費 [%s] 元素完成!!!'%t)

#實例化創建一個容量爲1的常規隊列  先進先出  用作線程target參數執行函數的參數。
bq=queue.Queue(maxsize=1)      #創建一個容量爲1的隊列

#啓動3個生產者 線程
threading.Thread(target=product, args=(bq,)).start()
threading.Thread(target=product, args=(bq,)).start()
threading.Thread(target=product, args=(bq,)).start()

#Queue 隊列的大小爲 1,因此三個生產者線程無法連續放入元素,必須等待消費者線程取出一個元素後,其中的一個生產者線程才能放入一個元素。
#三個生產者線程都想向 Queue 中放入元素,但只要其中一個生產者線程向該隊列中放入元素之後,其他生產者線程就必須等待,等待消費者線程取出 Queue 隊列中的元素。

#啓動1個消費 線程
threading.Thread(target=consume, args=(bq,)).start()






####################
#Event類 來自threading線程模塊
#Event類 是一種非常簡單的線程通信機制,一個線程發出一個 Event,另一個線程可通過該 Event 被觸發。

#Event類 本身管理一個內部旗標,
#程序可以通過 Event 的 set() 方法將該旗標設置爲 True,也可以調用 clear() 方法將該旗標設置爲 False。
#程序可以調用 wait() 方法來阻塞當前線程,直到 Event 的內部旗標被設置爲 True。

import threading
help(threading.Event)
threading.Event.__doc__
dir(threading.Event)

#Event 提供瞭如下方法:
#is_set():                 該方法返回 Event 的內部旗標是否爲True。
#set():                    該方法將會把 Event 的內部旗標設置爲 True,並喚醒所有處於等待狀態的線程。
#clear():                  該方法將 Event 的內部旗標設置爲 False,通常接下來會調用 wait() 方法來阻塞當前線程。
#wait(timeout=None):       該方法會阻塞當前線程。


#示例  Event的簡單用法
import threading
import time

event=threading.Event()

def cal(name):
    #定義函數 準備用於線程target參數執行函數
    #函數內實現 等待事件,進入等待阻塞狀態
    print('%s 啓動\t' % threading.current_thread().getName())
    print('%s 準備開始計算狀態' % name)
    event.wait()            #wait方法阻塞當前線程進入等待狀態
    
    #收到事件後才能繼續進入運行狀態
    print('%s 收到通知了\t' % threading.current_thread().getName())
    print('%s 正式開始計算!' % name)

#創建並啓動2條線程,他們都會在啓動、準備開始計算狀態處 阻塞,等待。
threading.Thread(target=cal, args=('甲',)).start()
threading.Thread(target=cal, args=('乙',)).start()
time.sleep(2)
print('--------------------')

#發出事件  直到set方法設置Event內部旗幟爲True 並 喚醒等待線程開始繼續往下執行
print('主線程發出事件')
event.set()                 #set方法喚醒所有處於等待狀態的線程



#示例
#結合 Event 的內部旗標,同樣實現前面的 Account 的生產者-消費者效果:
#存錢線程(生產者)存錢之後,必須等取錢線程(消費者)取錢之後才能繼續向下執行。

#Event 實際上類似於 Condition 和旗標的結合體,但 Event 本身並不帶 Lock 對象,因此如果要實現線程同步,還需要額外的 Lock 對象。

import threading

class Account:
    '''
    定義一個Account 類,提供 draw() 和 deposit() 兩個方法,分別對應於該賬戶的取錢和存款操作。

    
    實現要求存款者和取錢者不斷地重複存款、取錢的動作,而且要求每當存款者將錢存入指定賬戶後,取錢者就立即取出該筆錢。
    不允許存款者連續兩次存錢,也不允許取錢者連續兩次取錢。
    '''
    
    #定義構造器  封裝賬戶編號、賬戶餘額兩個類屬性
    def __init__(self,account_no,balance):
        self.accout_no =  account_no
        self._balance = balance
        self.lock = threading.Lock()
        self.event = threading.Event()
    
    #因爲賬戶餘額不允許隨便修改,所以只爲self._balance提供getter方法
    def getBalance(self):
        return self._balance
    
    #定義取錢操作 線程加鎖的安全方式
    def draw(self,draw_amount):
        #加鎖操作
        self.lock.acquire()
        if self.event.is_set():        #is_set()方法返回 Event 的內部旗標是否爲True。
            print(threading.current_thread().name + '取錢:' + str(draw_amount))
            self._balance -= draw_amount
            print('賬戶餘額爲:' + str(self._balance))
            
            #clear方法設置Event內部旗幟爲False,通常接下來會調用 wait() 方法來阻塞當前線程。
            self.event.clear()         #clear方法設置Event內部旗幟爲False,通常接下來會調用 wait() 方法來阻塞當前線程。
            self.lock.release()        #解鎖操作
            self.event.wait()          #阻塞當前線程 進入等待狀態
        else:
            self.lock.release()        #解鎖操作
            self.event.wait()          #阻塞當前線程 進入等待狀態
    
    #定義存錢操作 線程加鎖的安全方式
    def deposit(self,deposit_amount):
        #加鎖操作
        self.lock.acquire()
        if not self.event.is_set():
            print(threading.current_thread().name + '存款:' + str(deposit_amount))
            self._balance += deposit_amount
            print('賬戶餘額爲:' + str(self._balance))
            
            #set方法將內部旗標設置爲 True,並喚醒所有處於等待狀態的線程
            self.event.set()
            self.lock.release()        #解鎖操作
            self.event.wait()          #阻塞當前線程 進入等待狀態
        else:
            self.lock.release()        #解鎖操作
            self.event.wait()          #阻塞當前線程 進入等待狀態
        
        
#定義一個函數,模擬重複max次執行取錢操作
def draw_many(account, draw_amount,max):
    for i in range(max):
        account.draw(draw_amount)

#定義一個函數,模擬重複max次執行存錢操作
def deposit_many(account,deposit_amount,max):
    for i in range(max):
        account.deposit(deposit_amount)
        

acct=Account('123456789',0)                        #實例化創建一個賬戶
threading.Thread(name='取錢者 X ', target=draw_many, args=(acct,800,100)).start()
threading.Thread(name='存款者 甲 ', target=deposit_many, args=(acct,800,100)).start()
threading.Thread(name='存款者 乙 ', target=deposit_many, args=(acct,800,100)).start()
threading.Thread(name='存款者 丙 ', target=deposit_many, args=(acct,800,100)).start()











# =============================================================================
# #線程池    ThreadPoolExecutor類
# #線程池的基類是 concurrent.futures 模塊中的 Executor,
# #Executor 提供了兩個子類,即 ThreadPoolExecutor 和 ProcessPoolExecutor,
# #其中 ThreadPoolExecutor類 用於創建線程池,而 ProcessPoolExecutor類 用於創建進程池。
# =============================================================================
'''
注意:
1、submit()線程池類方法提交線程池後返回的Future對象,用result()方法來獲取線程任務的運回值,
但該方法會阻塞當前主線程,只有等到錢程任務完成後,result() 方法的阻塞纔會被解除。
可通過 Future 的 add_done_callback() 方法來添加回調函數來避免直接調用result()方法阻塞線程
另外線程池類方法map()後可循環迭代每一個線程
'''

from concurrent.futures import ThreadPoolExecutor

help(ThreadPoolExecutor)
ThreadPoolExecutor.__doc__
dir(ThreadPoolExecutor)


#Exectuor類提供瞭如下常用方法:
#1、submit(fn, *args, **kwargs): 將 fn 函數提交給線程池。*args 代表傳給 fn 函數的參數,*kwargs 代表以關鍵字參數的形式爲 fn 函數傳入參數。
#2、map(func, *iterables, timeout=None, chunksize=1):該函數類似於全局函數 map(func, *iterables),
#只是該函數將會啓動多個線程,以異步方式立即對 iterables 執行 map 處理。
#3、shutdown(wait=True):         關閉線程池


#程序將 fn 函數提交(submit)給線程池後,submit 方法會返回一個 Future 對象,
#Future類主要用於獲取線程任務函數的返回值。
#由於線程任務會在新線程中以異步方式執行,因此,線程執行的函數相當於一個“將來完成”的任務,所以 Python 使用 Future 來代表。


#Future對象提供瞭如下方法:
#1、cancel():                  取消該 Future 代表的線程任務。如果該任務正在執行,不可取消,則該方法返回 False;否則,程序會取消該任務,並返回 True。
#2、cancelled():               返回 Future 代表的線程任務是否被成功取消。
#3、running():                 如果該 Future 代表的線程任務正在執行、不可被取消,該方法返回 True。
#4、done():                    如果該 Funture 代表的線程任務被成功取消或執行完成,則該方法返回 True。
#5、result(timeout=None):      獲取該 Future 代表的線程任務最後返回的結果。
#如果 Future 代表的線程任務還未完成,該方法將會阻塞當前線程,
#其中 timeout 參數指定最多阻塞多少秒。
#6、exception(timeout=None):   獲取該 Future 代表的線程任務所引發的異常。如果該任務成功完成,沒有異常,則該方法返回 None。
#7、add_done_callback(fn):     爲該 Future 代表的線程任務註冊一個“回調函數”,當該任務成功完成時,程序會自動觸發該 fn 函數。

'''
使用線程池來執行線程任務的步驟如下:
1、調用 ThreadPoolExecutor 類的構造器創建一個線程池。
2、定義一個普通函數作爲線程任務。
3、調用 ThreadPoolExecutor 對象的 submit() 方法來提交線程任務。
4、當不想提交任何任務時,調用 ThreadPoolExecutor 對象的 shutdown() 方法來關閉線程池。
'''


###################
#示例
#示例如何使用線程池來執行線程任務
from concurrent.futures import ThreadPoolExecutor
import threading
import time

def action(max):
    #定義一個函數  準備用作線程池fn參數函數
    my_sum = 0
    for i in range(max):
        print(threading.current_thread().name + ' ' + str(i))
        my_sum += 1
    return my_sum

#實例化創建線程池  參數max_workers指定工作線程數量爲2條
pool=ThreadPoolExecutor(max_workers=2)        #創建一個包含2條線程的線程池

#類方法submit()  將fn參數函數提交給線程池
future1=pool.submit(action,50)                #向線程池提交一個fn參數函數action,50爲函數參數
future2=pool.submit(action,100)               #繼續向線程池提交一個fn參數函數action,100爲函數參數

type(future1)                                 #查看 實例化提交線程池以後的類型

#類方法done()  判斷提交線程池後的future對象 即線程任務 是否運行完成
time.sleep(1)
print(future1.done())                         #輸出future對象是否運行完畢
time.sleep(1)
print(future2.done())                         #輸出future對象是否運行完畢

#類方法result()  返回提交線程池後的future對象 即線程任務 的程序運行結果  
#注意提交線程池後返回的Future對象,用result()方法來獲取線程任務的運回值,
#但該方法會阻塞當前主線程,只有等到錢程任務完成後,result() 方法的阻塞纔會被解除。
#可通過 Future 的 add_done_callback() 方法來添加回調函數來避免直接調用result()方法阻塞線程
print(future1.result())                       #輸出future對象的程序運行結果
print(future2.result())                       #輸出future對象的程序運行結果

#類方法shutdown()  關閉線程池
pool.shutdown()                               #線程池關閉



####################
#示例   add_done_callback()  提交線程池後的Future對象方法  添加回調函數
#通過 Future 的 add_done_callback() 方法來添加回調函數來避免直接調用result()方法阻塞線程
from concurrent.futures import ThreadPoolExecutor
import threading
import time

def action(max):
    #定義一個函數  準備用作線程池fn參數函數    
    my_sum = 0
    for i in range(max):
        print(threading.current_thread().name + ' ' + str(i))
        my_sum += i 
    return my_sum


with ThreadPoolExecutor(max_workers=2) as pool:
    future1 = pool.submit(action,50)
    future2 = pool.submit(action,100)
    
    def get_result(future):
        print(future.result())
        
    future1.add_done_callback(get_result)
    future2.add_done_callback(get_result)
    print('--------------------')
    
'''
由於程序並未直接調用 future1、future2 的 result() 方法,因此主線程不會被阻塞,
可以立即看到輸出主線程打印出的橫線。
接下來將會看到兩個新線程併發執行,當線程任務執行完成後,get_result() 函數被觸發,輸出線程任務的返回值。
'''



####################
#示例  線程池類方法 map(func, *iterables, timeout=None, chunksize=1)
#該方法的功能類似於全局函數 map(),區別在於線程池的 map() 方法會爲 iterables 的每個元素啓動一個線程,以併發方式來執行 func 函數。
#這種方式相當於啓動 len(iterables) 個線程,井收集每個線程的執行結果。
from concurrent.futures import ThreadPoolExecutor
import threading
import time

def action(max):
    #定義一個函數  準備用作線程池fn參數函數    
    my_sum = 0
    for i in range(max):
        print(threading.current_thread().name + ' ' + str(i))
        my_sum += i 
    return my_sum

with ThreadPoolExecutor(max_workers=4) as pool:
    results = pool.map(action,(50,100,150))           #啓動3個線程
    print('---------------')
    for r in results:                                 #for循環迭代每個線程及結果
        print(r)











# =============================================================================
# #線程局部變量
# #線程局部變量爲每一個使用該變量的線程都提供一個變量的副本,使每一個線程都可以獨立地改變自己的副本,而不會和其他線程的副本衝突。
# #從線程的角度看,就好像每一個線程都完全擁有該變量一樣。
# =============================================================================

#示例  threading.local()來賦值聲明線程局部變量
#作用好像每個線程都完成擁有該變量亦一樣
from concurrent.futures import ThreadPoolExecutor
import threading

mydata = threading.local()        #定義線程局部變量

def action(max):
    for i in range(max):
        try:
            mydata.x += i
        except:
            mydata.x = i
        
        print('%s mydata.x 的值爲: %d' %(threading.current_thread().name, mydata.x))
    

with ThreadPoolExecutor(max_workers=2) as pool:
    pool.submit(action,10)
    pool.submit(action,10)

'''
程序中作爲線程執行體的 action 函數使用 mydata.x 記錄 0~10 的累加值,
如果兩個線程共享同一個 mydata 變量,將會看到 mydata.x 最後會累加到 90(0~9 的累加值是 45,但兩次累加會得到 90)。
但由於 mydata 是 threading.local 變量,因此程序會爲每個線程都創建一個該變量的副本,所以將會看到兩個線程的 mydata.x 最後都累加到 45。
'''











# =============================================================================
# #線程定時器
# #線程定時器是線程類threading類的Timer子類,該子類可用於控制指定函數在特定時間內執行一次
# =============================================================================
'''
注意:
#Timer定時器只能控制函數在指定時間內執行一次,
#如果要使用 Timer 控制函數多次重複執行,則需要函數內嵌套定時器 來循環執行下一次調度。
'''

from threading import Timer
help(Timer)
Timer.__doc__
dir(Timer)                      


###################
#示例  定時器Timer 控制 10s 後執行 hello 函數。
from threading import Timer

def hello():
    print('hello,world')

#創建一個定時器線程
t=Timer(10,hello)               #定時10秒以後執行hello函數
t.start()                       #定時器線程啓動




###################
#示例  循環調用定時器
#Timer定時器只能控制函數在指定時間內執行一次,
#如果要使用 Timer 控制函數多次重複執行,則需要函數內嵌套定時器 來循環執行下一次調度。
from threading import Timer
import time

count=0 
def print_time():
    '''
    定義函數,用於Timer()定時器中參數函數
    函數中嵌套Timer()定時器,可循環判斷,如果count<10,則循環調用10次定時器輸出當前時間
    '''
    print('當前時間:%s' % time.ctime())
    global t,count
    count+=1
    if count<10:
        t=Timer(1, print_time)
        t.start()

t = Timer(1,print_time)
t.start()











# =============================================================================
# #sched模塊  任務調度模塊
# #sched模塊  可以比 Timer線程定時器 執行更復雜的任務調度
# ##sched模塊提供了 sched.scheduler 類,該類代表一個任務調度器。
# =============================================================================
'''
使用:
1、實例化任務調度器 sched.scheduler()
2、實例化任務調度器方法:絕對時間執行函數任務、相對時間執行函數任務、取消任務等
'''


import sched
help(sched)
sched.__doc__
sched.__all__                 #只有一個方法 scheduler() 定義線程調度器
dir(sched)


#sched.scheduler(timefunc=time.monotonic, delayfunc=time.sleep) 構造器支持兩個參數:
#1、timefunc:        該參數指定生成時間戳的時間函數,默認使用 time.monotonic 來生成時間戳。
#2、delayfunc:       該參數指定阻塞程序的函數,默認使用 time.sleep 函數來阻塞程序。

help(sched.scheduler)
dir(sched.scheduler)


help(sched.scheduler.enterabs)       #絕對時間執行函數任務
help(sched.scheduler.enter)          #相對時間執行函數任務
help(sched.scheduler.run)            #運行所有需要調度的任務
help(sched.scheduler.queue)          #返回調度器的調度隊列,根據任務優先級


#sched.scheduler 調度器支持如下常用屬性和方法:

#1、scheduler.enterabs(time, priority, action, argument=(), kwargs={}):
#指定在 time 時間點執行 action 函數,argument 和 kwargs 都用於向 action 函數傳入參數,其中 argument 使用位置參數的形式傳入參數,kwargs 使用關鍵字參數的形式傳入參數。
#該方法返回一個 event,它可作爲 cancel() 方法的參數用於取消該調度。
#priority 參數指定該任務的優先級,當在同一個時間點有多個任務需要執行時,優先級高(值越小代表優先級越高)的任務會優先執行。

#2、scheduler.enter(delay, priority, action, argument=(),kwargs={}):
#該方法與上一個方法基本相同,只是 delay 參數用於指定多少秒之後執行 action 任務。

#3、scheduler.cancel(event):         取消任務。如果傳入的 event 參數不是當前調度隊列中的 event,程序將會引發 ValueError 異常。
#4、scheduler.empty():               判斷當前該調度器的調度隊列是否爲空。

#5、scheduler.run(blocking=True):    運行所有需要調度的任務。
#如果調用該方法的 blocking 參數爲 True,該方法將會阻塞線程,直到所有被調度的任務都執行完成。

#6、scheduler.queue:                 該只讀屬性返回該調度器的調度隊列。



###################
#示例     sched.scheduler執行任務調度
import sched
import time
import threading

def print_time(name='default'):
    print('%s的時間:%s' % (name,time.ctime()))

print('主線程開始時間:',time.ctime())

#實例化創建 線程調度器
s=sched.scheduler()

#enter()方法  相對時間  任務調度
s.enter(10,1,print_time)                               #參數1指定10秒後開始運行,參數2位運行優先等級爲1,參數3爲調度運行函數
s.enter(5,2,print_time, argument=('位置參數',))        #參數1指定10秒後開始運行,參數2位運行優先等級爲1,參數3爲調度運行函數,參數4爲參數3函數的元組參數
s.enter(5,1,print_time, kwargs={'name':'字典參數'})    #參數1指定10秒後開始運行,參數2位運行優先等級爲1,參數3爲調度運行函數,參數3爲參數3函數的字典參數

#run()方法  運行所有需要調度的任務
s.run()
print('主線程結束時間:',time.ctime())











# =============================================================================
# #multiprocessding模塊  進程封裝
# =============================================================================
'''
注意:
1、multiprocessing.Process類通過實例化對象指定target參數執行函數 在IDLE裏不顯示函數執行內容。
通過在控制檯通過執行文件可顯示target參數執行函數的內容。
2、通過 multiprocessing.Process 來創建並啓動進程時,程序必須先判斷if __name__=='__main__':,否則可能引發異常。
'''


#探索模塊
import multiprocessing

help(multiprocessing)
multiprocessing.__doc__
multiprocessing.__all__
dir(multiprocessing)


####################
#multiprocessing.Process類  進程創建

#Python 在 multiprocessing模塊下提供了 Process 來創建新進程。
#與 Thread 類似的是,使用 Process 創建新進程也有兩種方式:
#1、使用 multiprocessing模塊 中的 Process類 來實例化對象創建新進程,並指定函數作爲target參數函數
#2、繼承 Process類 ,並重寫他的 run() 方法來創建進程類,程序創建Process子類的實例作爲進程

import multiprocessing

help(multiprocessing.Process)
dir(multiprocessing.Process)

#Process 類也有如下類似的方法和屬性:
#run():                  重寫該方法可實現進程的執行體。
#start():                該方法用於啓動進程。
#join([timeout]):        該方法類似於線程的 join() 方法,當前進程必須等待被 join 的進程執行完成才能向下執行。
#name:                   該屬性用於設置或訪問進程的名字。
#is_alive():             判斷進程是否還活着。
#daemon:                 該屬性用於判斷或設置進程的後臺狀態。
#pid:                    返回進程的 ID。
#authkey:                返回進程的授權 key。
#terminate():            中斷該進程。

help(multiprocessing.Process.start)
help(multiprocessing.Process.run)
help(multiprocessing.Process.daemon)
help(multiprocessing.Process.join)



#示例    通過實例化創建進程    指定target參數執行函數
#注意:
#1、multiprocessing.Process類通過實例化對象指定target參數執行函數 在IDLE裏不顯示函數執行內容。
#通過在控制檯通過執行文件可顯示target參數執行函數的內容。
import multiprocessing
import os

def action(max):
    for i in range(max):
        print('(%s)子進程  (父進程:(%s)):%d' %(os.getpid(), os.getppid(), i))

if __name__ == '__main__':
    for i in range(100):
        print('(%s)主進程:%d' %(os.getpid(), i))
        if i == 20:
            np1=multiprocessing.Process(target=action, args=(100,))
            np1.start()
            
            np2=multiprocessing.Process(target=action, args=(100,))
            np2.start()
            np2.join()
    print('主進程執行完畢')
    


#示例    繼承Process類來創建子進程
#注意:
#1、multiprocessing.Process類通過實例化對象指定target參數執行函數 在IDLE裏不顯示函數執行內容。
#通過在控制檯通過執行文件可顯示target參數執行函數的內容。
import multiprocessing
import os

class MyProcess(multiprocessing.Process):
    def __init__(self,max):
        self.max=max
        super().__init__()
    #重寫run方法作爲進程執行體
    def run(self):
        for i in range(self.max):
            print('(%s)子進程(父進程:(%s)):%d' %(os.getpid(), os.getppid(), i))
    
if __name__ == '__main__':
    for i in range(100):
        print('(%s)主進程:%d' %(os.getpid(),i))
        if i == 20:
            mp1=MyProcess(100)
            mp1.start()
            mp2=MyProcess(100)
            mp2.start()
            mp2.join()
    print('主進程執行完畢!')











# =============================================================================
# #進程啓動的三種方式  spawn(windows僅支持此方式)、fork、forkserver
# #spawn:      父進程啓動一個全新的python解釋器(windows平臺僅支持此方式啓動進程)
# #fork:        父進程使用 os.fork() 來啓動一個 Python 解釋器進程。
# #forkserver:  如果使用這種方式來啓動進程,程序將會啓動一個服務器進程。
# =============================================================================
'''
注意:
1、multiprocessing.set_start_method(' ')函數用於設置啓動進程的方式,設置代碼必須放在多進程代碼之前。
2、window只支持spawn方式啓動進程
'''

#示例
import multiprocessing
import os

#help(multiprocessing.set_start_method)

def foo(q):
    '''參數q爲一個隊列,可在隊列內存取元素'''
    print('被啓動的新進程: (%s)' % os.getpid())
    q.put('Python')
    
if __name__ == '__main__':
    #設置使用fork方式啓動進程
    multiprocessing.set_start_method('spawn')

    #創建一個進程的隊列
    q = multiprocessing.Queue()

    #創建並啓動子進程
    mp = multiprocessing.Process(target=foo, args=(q, ))
    mp.start()
    
    #獲取隊列中的消息
    print(q.get())
    mp.join()










# =============================================================================
# #進程池    multiprocessing.Pool類
# #通過 multiprocessing 模塊的 Pool類來實例化創建進程池,然後通過進程池方法提交進程池、關閉進程池、等待進程池等
# =============================================================================

import multiprocessing 

help(multiprocessing.Pool)
multiprocessing.Pool.__doc__
dir(multiprocessing.Pool)


#進程池具有如下常用方法:

#1、apply(func[, args[, kwds]]):          將 func 函數提交給進程池處理。其中 args 代表傳給 func 的位置參數,kwds 代表傳給 func 的關鍵字參數。
#該方法會被阻塞直到 func 函數執行完成。
#2、apply_async(func[, args[, kwds[, callback[, error_callback]]]]):
#這是 apply() 方法的異步版本,該方法不會被阻塞。其中 callback 指定 func 函數完成後的回調函數,error_callback 指定 func 函數出錯後的回調函數。

#3、map(func, iterable[, chunksize]):     類似於 Python 的 map() 全局函數,只不過此處使用新進程對 iterable 的每一個元素執行 func 函數。
#4、map_async(func, iterable[, chunksize[, callback[, error_callback]]]):
#這是 map() 方法的異步版本,該方法不會被阻塞。其中 callback 指定 func 函數完成後的回調函數,error_callback 指定 func 函數出錯後的回調函數。

#5、imap(func, iterable[, chunksize]):    這是 map() 方法的延遲版本。
#6、imap_unordered(func, iterable[, chunksize]):
#功能類似於 imap() 方法,但該方法不能保證所生成的結果(包含多個元素)與原 iterable 中的元素順序一致。

#7、starmap(func, iterable[,chunksize]):  功能類似於 map() 方法,但該方法要求 iterable 的元素也是 iterable 對象,程序會將每一個元素解包之後作爲 func 函數的參數。

#8、close():                              關閉進程池。在調用該方法之後,該進程池不能再接收新任務,它會把當前進程池中的所有任務執行完成後再關閉自己。
#9、terminate():                          立即中止進程池。
#10、join():                              等待所有進程完成。


#示例    實例化創建進程池
#使用 apply_async() 方法啓動進程:
import multiprocessing
import time
import os

#定義一個函數,準備用作進程池func參數函數
def action(name='default'):
    print('(%s)進程正在執行,參數爲:%s'%(os.getpid(), name))
    time.sleep(3)

#通過 multiprocessing.Process 來創建並啓動進程時,程序必須先判斷if __name__=='__main__':,否則可能引發異常。
if __name__ == '__main__':
    #實例化創建進程池
    pool=multiprocessing.Pool(processes=4)
    #異步提交進程池
    pool.apply_async(action)
    pool.apply_async(action,args=('位置參數',))
    pool.apply_async(action,kwds={'name':'字典參數'})
    #關閉進程池,不再接受新任務,會等待進程池中所有任務完成後關閉
    pool.close()
    #等待所有進程完成
    pool.join()




#示例    實例化創建進程池  通過上下文管理協議創建
#使用map()方法來啓動進程
import multiprocessing
import time
import os

#定義一個函數,準備用作進程池func參數函數
def action(max):
    my_sum=0
    for i in range(max):
        print('(%s)進程正在執行:%d'%(os.getpid(), i))
        my_sum+=i
    return my_sum

#通過 multiprocessing.Process 來創建並啓動進程時,程序必須先判斷if __name__=='__main__':,否則可能引發異常。
if __name__ == '__main__':
    #上下文管理器創建進程池  進程數量爲4
    with multiprocessing.Pool(processes=4) as pool:
        #提交併啓動三個進程
        results=pool.map(action,(50,100,150))
        print('-----------------')
        #for循環迭代每個進程及結果
        for r in results:             
            print(r)











# =============================================================================
# #進程通信    multiprocessing.Queue類  和 multiprocessing.Pipe類
#            
# #Python 爲進程通信提供了兩種機制:
# #Queue:一個進程向 Queue 中放入數據,另一個進程從 Queue 中讀取數據。
# #Pipe:Pipe 代表連接兩個進程的管道。程序在調用 Pipe() 函數時會產生兩個連接端,分別交給通信的兩個進程,接下來進程既可從該連接端讀取數據,也可向該連接端寫入數據。
# =============================================================================
'''
注意:
進程通信中,使用實例化創建multiprocessing.Pipe類管道對象返回兩個PipeConnection對象,
其中前一個PipeConnection對象用於接受數據,後一個PipeConnection對象用於發送數據
'''



import multiprocessing

help(multiprocessing.Queue)
help(multiprocessing.Pipe)



##########
#Queue:一個進程向 Queue 中放入數據,另一個進程從 Queue 中讀取數據。
dir(multiprocessing.Queue)
#multiprocessing 模塊下的 Queue 和 queue 模塊下的 Queue 基本類似,
#它們都提供了 qsize()、empty()、full()、put()、put_nowait()、get()、get_nowait() 等方法。
#區別只是 multiprocessing 模塊下的 Queue 爲進程提供服務,而 queue 模塊下的 Queue 爲線程提供服務。


#示例    使用multiprocessing.Queue類來實現進程之間的通信
import multiprocessing

#
def f(q):
    '''
    創建一個函數,用作進程target參數的執行函數
    參數q爲一個實例化創建的multiprocessing.Queue類對象
    '''
    print('(%s)進程開始放入數據...'%multiprocessing.current_process().pid)
    #向隊列中放入元素
    q.put('血皇敖天')

if __name__ == "__main__":
    #實例化創建進程通信的隊列
    q=multiprocessing.Queue()
    #實例化創建並啓動子進程運行函數
    p=multiprocessing.Process(target=f, args=(q,))
    p.start()
    print('(%s)進程開始取出數據...'%multiprocessing.current_process().pid)
    #從進程通信的隊列中取出元素
    print(q.get())
    #插入實現子進程
    p.join()




##########
#Pipe:Pipe 代表連接兩個進程的管道。程序在調用 Pipe() 函數時會產生兩個連接端,分別交給通信的兩個進程,接下來進程既可從該連接端讀取數據,也可向該連接端寫入數據。

dir(multiprocessing.Pipe)
#使用 Pipe 實現進程通信,程序會調用 multiprocessing.Pipe() 函數來創建一個管道,
#該函數會返回兩個 PipeConnection 對象,代表管道的兩個連接端(一個管道有兩個連接端,分別用於連接通信的兩個進程)。


#PipeConnection 對象包含如下常用方法:

#1、send(obj):          發送一個 obj 給管道的另一端,另一端使用 recv() 方法接收。需要說明的是,該 obj 必須是可 picklable 的(Python 的序列化機制),如果該對象序列化之後超過 32MB,則很可能會引發 ValueError 異常。
#2、recv():             接收另一端通過 send() 方法發送過來的數據。
#3、fileno():           關於連接所使用的文件描述器。
#4、close():            關閉連接。
#5、poll([timeout]):    返回連接中是否還有數據可以讀取。

#6、send_bytes(buffer[, offset[, size]]):
#發送字節數據。如果沒有指定 offset、size 參數,則默認發送 buffer 字節串的全部數據;如果指定了 offset 和 size 參數,則只發送 buffer 字節串中從 offset 開始、長度爲 size 的字節數據。通過該方法發送的數據,應該使用 recv_bytes() 或 recv_bytes_into 方法接收。

#7、recv_bytes([maxlength]):  
#接收通過 send_bytes() 方法發迭的數據,maxlength 指定最多接收的字節數。該方法返回接收到的字節數據。

#8、recv_bytes_into(buffer[, offset]):
#功能與 recv_bytes() 方法類似,只是該方法將接收到的數據放在 buffer 中。


#示例    使用multiprocessing.Pipe類來實現進程之間的通信
import multiprocessing

def f(conn):
    '''
    創建一個函數,用作進程target參數的執行函數
    參數conn爲實例化創建的multiprocessing.Pipe類管道對象返回的兩個PipeConnection對象中的後一個用於發送數據的對象
    '''
    print('(%s)進程開始發送數據...'%multiprocessing.current_process().pid)
    conn.send('血皇敖天')

if __name__ =='__main__':
    #實例化創建進程通信的pipe管道
    #返回兩個PipeConnection對象,前一個用於接收數據,後一個用於發送數據
    parent_conn, child_conn = multiprocessing.Pipe()
    #實例化創建並啓動子進程 來執行函數 從而發送管道數據
    p=multiprocessing.Process(target=f, args=(child_conn,))
    p.start()
    print('(%s)進程開始接收數據...'%multiprocessing.current_process().pid)
    #前一個生成返回的實例化對象用於接受數據
    print(parent_conn.recv())
    p.join()












# =============================================================================
# #併發及異步併發爬蟲
# #concurrent.futures.ThreadPoolExecutor     線程池類、
# #concurrent.futures.ProcessPoolExecutor    進程池類
# #asyncio模塊   異步併發模塊
# #aiohttp模塊   異步的requests
# =============================================================================
'''
Py併發編程Futures併發爬蟲.py
Py併發編程asyncio模塊異步併發爬蟲.py


深入:
asyncio模塊   異步併發模塊
aiohttp模塊   異步的requests
'''



#示例    單線程簡易爬蟲
import requests
import time

def download_one(url):
    resp = requests.get(url)
    print('Read {} from {}'.format(len(resp.content), url))
   
def download_all(sites):
    for site in sites:
        download_one(site)
def main():
    sites = [
        'http://c.biancheng.net',
        'http://c.biancheng.net/c',
        'http://c.biancheng.net/python'
    ]
    start_time = time.perf_counter()
    download_all(sites)
    end_time = time.perf_counter()
    print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))
   
if __name__ == '__main__':
    main()



####################
#示例    多線程/多進程 簡易爬蟲
import concurrent.futures
import requests
import threading
import time

def download_one(url):
    resp = requests.get(url)
    print('Read {} from {}'.format(len(resp.content), url))
    
def download_all(sites):
#    '''多線程版本'''
#    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
#        executor.map(download_one, sites)
    
    '''多進程版本'''
    # ProcessPoolExecutor() 表示類實例化創建進程池,使用多個進程並行的執行程序。
    #通常省略參數 workers,因爲系統會自動返回 CPU 的數量作爲可以調用的進程數。
    with concurrent.futures.ProcessPoolExecutor() as executor:
        executor.map(download_one, sites)

def main():
    sites = [
        'http://c.biancheng.net',
        'http://c.biancheng.net/c',
        'http://c.biancheng.net/python'
    ]
    start_time = time.perf_counter()
    download_all(sites)
    end_time = time.perf_counter()
    print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))
    
if __name__ == '__main__':
    main()



####################
#示例    多線程迭代器  簡易爬蟲
import concurrent.futures
import requests
import time

def download_one(url):
    resp = requests.get(url)
    print('Read {} from {}'.format(len(resp.content), url))

def download_all(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        to_do = []
        for site in sites:
            future = executor.submit(download_one, site)
            to_do.append(future)
        #as_completed(fs)  針對給定的 future 迭代器 fs,在其完成後返回完成後的迭代器。
        for future in concurrent.futures.as_completed(to_do):
            future.result()

def main():
    sites = [
        'http://c.biancheng.net',
        'http://c.biancheng.net/c',
        'http://c.biancheng.net/python'
    ]
    start_time = time.perf_counter()
    download_all(sites)
    end_time = time.perf_counter()
    print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))

if __name__ == '__main__':
    main()


####################
'''
異步併發爬蟲 需後期深入:
asyncio模塊   異步併發模塊
aiohttp模塊   異步的requests
'''










# =============================================================================
# #GIL 全局解釋器鎖
# #GIL 限制了 Python 多線程的性能,其本質上類似操作系統的 Mutex。
# #GIL 的功能是:在 CPython 解釋器中執行的每一個 Python 線程,都會先鎖住自己,以阻止別的線程執行。
# =============================================================================
'''
#注意:
GIL 不能絕對保證線程安全
因爲即便 GIL 僅允許一個 Python 線程執行,但Python 還有 check interval 這樣的搶佔機制。
'''

#示例    單線程 測速
import time
start = time.time()
def CountDown(n):
    while n > 0:
        n -= 1
CountDown(100000)
print("Time used:",(time.time() - start))


#示例    多線程 是否會加速
#結果顯示並 比單線程計算 沒有 提速;緣由是CPython 解釋器存在Gil全局解釋器鎖,
#GIL 的功能是:在 CPython 解釋器中執行的每一個 Python 線程,都會先鎖住自己,以阻止別的線程執行。
import time
from threading import Thread
start = time.time()
def CountDown(n):
    while n > 0:
        n -= 1
t1 = Thread(target=CountDown, args=[100000 // 2])
t2 = Thread(target=CountDown, args=[100000 // 2])
t1.start()
t2.start()
t1.join()
t2.join()
print("Time used:",(time.time() - start))


#CPython 使用引用計數來管理內容    即: 垃圾回收機制
#所有 Python 腳本中創建的實例,都會配備一個引用計數,來記錄有多少個指針來指向它。
#當實例的引用計數的值爲 0 時,會自動釋放其所佔的內存。

#示例
import sys

a=[]
b=a
print(sys.getrefcount(a))    #輸出引用計數值,此處爲3



#注意:GIL 不能絕對保證線程安全
#因爲即便 GIL 僅允許一個 Python 線程執行,但Python 還有 check interval 這樣的搶佔機制。

#示例:
import threading

n = 0
def foo():
    global n
    n += 1
    
threads = []
for i in range(100):
    t = threading.Thread(target=foo)
    threads.append(t)

for t in threads:
    t.start()

for t in threads:
    t.join()

print(n)
'''
執行此代碼會發現,其大部分時候會打印 100,但有時也會打印 99 或者 98,
原因在於 n+=1 這一句代碼讓線程並不安全。
'''












# =============================================================================
# #垃圾回收機制    gc模塊 手動啓動垃圾回收機制
# #垃圾回收機制引用計數機制    
# #即:對象的引用計數值爲 0 時,說明這個對象永不再用,自然它就變成了垃圾,需要被回收。
# =============================================================================


import psutil         #獲取系統信息模塊
import os


help(psutil)
psutil.__doc__
psutil.__all__
dir(psutil)

help(psutil.Process)
psutil.Process.__doc__
dir(psutil.Process)

help(psutil.Process.memory_full_info)
psutil.Process.memory_full_info.__doc__



####################
#示例    通過函數內部局部變量是否垃圾回收 來查看內存佔用空間
import os
import psutil         #獲取系統信息模塊

#顯示當前 python 程序佔用的內存大小
def show_memory_info(hint):
    pid = os.getpid()
    p = psutil.Process(pid)
    info = p.memory_full_info()
    memory = info.uss / 1024 / 1024
    print('{} memory used: {} MB'.format(hint, memory))

def func():
    show_memory_info('initial')            #輸出較小內存佔用空間
    #global a                              #如果將局部變量聲明成全局變量,則持續佔用較大內存空間
    a = [i for i in range(10000000)]       #臨時變量佔用較大內存空間。  函數運行完畢後即垃圾回收機制清除臨時變量
    show_memory_info('after a created')    #輸出垃圾回收機制後的較小內存佔用空間
    #return a                              #如果函數返回局部變量,則也會持續佔用較大內存空間

#輸出佔用內存空間 
func()
show_memory_info('finished')



####################
#示例    python內部的引用計數機制
import sys

a = []

#sys.getrefcount() 函數用於查看一個變量的引用次數
print(sys.getrefcount(a))        # 兩次引用,一次來自a,一次來自 getrefcount

def func(a):
    print(sys.getrefcount(a))    # 四次引用,a,python的函數調用棧,函數參數,和 getrefcount

func(a)
print(sys.getrefcount(a))        # 兩次引用,一次來自a,一次來自getrefcount。函數func調用已經不存在被垃圾回收了




#####################
#示例    手動啓動垃圾回收
import gc             #gc(garbage collector)模塊的引用計數技術來進行垃圾回收
import psutil         #獲取系統信息模塊

#顯示當前 python 程序佔用的內存大小
def show_memory_info(hint):
    pid = os.getpid()
    p = psutil.Process(pid)
    info = p.memory_full_info()
    memory = info.uss / 1024 / 1024
    print('{} memory used: {} MB'.format(hint, memory))

def func():
    show_memory_info('initial')            #輸出較小內存佔用空間
    #global a                              #如果將局部變量聲明成全局變量,則持續佔用較大內存空間
    a = [i for i in range(10000000)]       #臨時變量佔用較大內存空間。  函數運行完畢後即垃圾回收機制清除臨時變量
    show_memory_info('after a created')    #輸出垃圾回收機制後的較小內存佔用空間
    #return a                              #如果函數返回局部變量,則也會持續佔用較大內存空間

show_memory_info('initial')
a = [i for i in range(10000000)]
show_memory_info('after a created')

del a

gc.collect()                              #gc(garbage collector)模塊的引用計數技術來進行垃圾回收
show_memory_info('finish')

print(a)




####################
#示例    手動啓動垃圾回收    循環調用嵌套示例
#Python使用標記清除(mark-sweep)算法和分代收集(generational),來啓用針對循環引用的自動垃圾回收。
import gc

#循環引用嵌套,導致其a,b的引用計數不爲0,從而垃圾會機制不成立,需要手動顯示啓動垃圾回收機制。
def func():
    show_memory_info('initial')
    a = [i for i in range(10000000)]
    b = [i for i in range(10000000)]
    show_memory_info('after a, b created')
    a.append(b)
    b.append(a)

func()
gc.collect()                              #這裏如果不進行手動啓動垃圾回收,則內存暫用空間依然高企
show_memory_info('finished')










發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章