參考資料
[1]. 廖雪峰 <Python 2.7 教程>
[2]. <Python性能雞湯> http://www.oschina.net/question/1579_45822
內容整理
函數
- 函數返回: 函數執行時遇到return即執行完畢, 返回結果; 沒有return, 執行完畢返回None; return None可以簡寫為return.
- 內建函數: 高效快速, e.g. input(), ord(), pow(), isinstance(), iter()
- 局部變量: 比全局變量快, 盡量避免global; 但下文有提到全局字典保存不同線程專屬對象的技巧
- 默認參數: 必須指向不變對象: 函數在定義時, 默認參數值就會被計算出來
- Python解釋器: 只檢查參數個數, 不檢查參數類型. 但它會檢查自己的內置函數參數格式和類型, 這不公平...對於自己寫的函數, 可以用內置函數isinstance實現參數類型檢查
1 if not isinstance(x, (int)): 2 raise TypeError('not int type')
- 多返回值: return x, y, 當然本質是省略了括號的tuple
- 多重賦值: x,y = y,x 要比三條語句執行快
- 字符串連接: 和Java語法一樣, 使用join()要比"+"高效, "+"會創建一個新的字符串並復制內容
- 延遲加載: ipmort在需要時再導入
- 無限循環: while 1比while True高效, 因為while 1是單步運算
- 列表生成式(list comprehension): 取代for和while更高效, 因為Python解釋器能夠在循環中發現它是一個可以預測的模式而被優化, 同時list更具可讀性. 和Java一樣, 可以在循環中節省一個額外的計數變量. e.g. evens = [i for i in range(10) if i%2 == 0] 要比 while i<10: if i%2 == 0: evens.append(i) i+=1 易讀高效
- 生成器(generator): <Python 2.7 教程>里提到, 生成器保存的是算法, 每次調用next()時才會計算出下一個值, 返回后保存這次計算的位置. <Python性能雞湯>里也給出了逐個字節發送視頻流的例子: chunk=(1000*i for i in xrange(1000)), chunk.next(), chunk.next(). 從兩個例子中可以看出生成器的共性: 是一個表達式, 再調用的時候才執行一次, 再次調用時執行第二次...而構造生成器, 要么是一個xrange表達式, 要么是在函數里合適的位置加入yield, 保存當前狀態后返回本次計算結果
- 查找效率: dict和set的查找很快, 因為是哈希表實現. list的實現是數組, 所以在list前插入效率不高, 因為list的后續下標不得不全部改變. 類比到Java里的集合框架: ArrayList數組線性表不適合在中間插入, 因為要改變后續元素下標; Java的Set和Python的set一樣都是元素不重復的, 而且由Hash實現. Java的集合框架太強大太繁雜, Python則簡化了很多, 記住dict, set, list, tuple目前就夠用了
- 裝飾器: 目前只掌握了簡單修飾, 加參數修飾的用法
進程和線程的基礎知識
- CPU執行代碼是順序執行, 單核CPU通過讓任務交替執行, "模擬"除了多任務並發執行. 真正的多任務並發, 是在多核CPU上, 每個CPU負責執行一個任務. 但實際任務數量遠多於CPU核心數量, 所以最終還是操作系統把多任務輪流調度到不同的核心上執行.
- 進程/線程和物理內存(寄存器)/CPU的關聯: 函數調用, 會在棧中分配一塊空間, 存放局部變量和參數, 調用結束, 棧空間被釋放. 每個線程都有獨立的棧, 寄存器. 同一進程里的所有線程共享文件, 代碼和數據.
- 進程獨立性: 以CPU中的程序計數器PC為例, 物理的PC只有一個, 但是每個進程有獨立的邏輯PC, 保存了程序運行中的值, 但CPU輪到該進程時, 就將邏輯PC值復制到物理PC.
Python跨平台支持多進程
- Unix/Linux平台Python支持fork(): 父進程復制出一個子進程
- Windows平台Python支持Process(target,args): "模擬出fork()的效果", 即父進程所有Python對象pickle后傳遞給子進程
Python進程間通信的數據交換方式
multiprocessing模塊提供的Queue, Pipes
Python多線程基礎知識
- 標准庫提供了兩個模塊: thread和threading(推薦).
- 任務進程都默認啟動一個叫做MainThread的主線程, 主線程可以啟動新的子線程.
- 多線程的變量鎖: 高級語言的一條語句在CPU執行時是若干條語句, 計算中的結果會存入臨時變量中, 每個線程都有自己的臨時變量.
- 獲取鎖后一定要釋放, 否則等待鎖的線程會一直阻塞下去, 成為死線程. Python中可以通過try...finally確保釋放.
- 鎖的壞處: (1) 加鎖的代碼只能以單線程模式執行, 阻止了多線程並發, 降低效率; (2) 可能造成死鎖.
Python的GIL鎖導致多線程不能充分利用多核CPU
- Python的解釋器有一個GIL(Global Interpreter Lock)鎖, 任何線程執行前, 先獲得GIL鎖, 每執行100行代碼, 解釋器會自動釋放GIL鎖, 讓別的線程有機會執行. 所以即使100個線程跑在100核CPU上, 也只能用到1個核.
- GIL是Python解釋器設計的歷史遺留問題, 使得Python不能有效利用多核, 但可以通過多進程實現.
- 多個Python進程有各自獨立的GIL鎖, 互不影響.
- GIL: 只是CPython解釋器存在. "GIL會序列化你的所有線程". 可以使用線程來管理多個派生進程, 使這些進程獨立運行於Python代碼之外.
多線程下選擇加鎖的全局變量or不得不傳遞下去的局部變量?
加鎖和傳遞局部變量都是老方法了, 只是局部變量傳遞起來很麻煩. 如果遇到每個線程一個專屬對象, 不能使用全局變量的情況, 作者就想到用一個全局dict, 但是通過線程自身作為key獲得保存的對應的對象, 例如:
1 # -*-coding:utf-8 -*- 2 import time, threading 3 4 global_dict = {} 5 6 class Student(object): 7 8 def __init__(self, name): 9 self.name = name 10 11 def return_name(self): 12 return self.name 13 14 15 def do_task_1(): 16 std = Student('Elsa') 17 global_dict[threading.currentThread()] = std 18 std = global_dict[threading.current_thread()] 19 print 'thread %s, do_task_1() std.name = %s\n' % (threading.currentThread().name, std.return_name()) 20 21 def do_task_2(): 22 std = Student('Anna') 23 global_dict[threading.currentThread()] = std 24 std = global_dict[threading.current_thread()] 25 print 'thread %s, do_task_2() std.name = %s\n' % (threading.currentThread().name, std.return_name()) 26 27 28 if __name__ == '__main__': 29 print 'thread %s is running...' % threading.currentThread().name 30 t1 = threading.Thread(target=do_task_1, name='t1') 31 t2 = threading.Thread(target=do_task_2, name='t2') 32 t1.start() 33 t2.start() 34 t1.join() 35 t2.join() 36 print 'thread %s ended.' % threading.currentThread().name
global_dict = {}作為一個全局變量, 保存了線程和它的專屬對象. 運行結果:
D:\WorkspaceVSCode>python multiprocess.py thread MainThread is running... thread t1, do_task_1() std.name = Elsa thread t2, do_task_2() std.name = Anna thread MainThread ended.
然后Python又簡化了對每個線程專屬對象的存儲和訪問, 也就是封裝了上面的global_dict = {}. 可以理解成, local_school = threading.local()是一個全局變量, 但它里面保存的是各個線程的局部變量, 還不用管理線程鎖了, 感覺很方便的樣子.
多線程時使用ThreadLocal用全局變量保存線程的局部變量
常用於為每個線程綁定一個數據庫連接, HTTP請求, 用戶身份信息等.
進程, 線程和計算密集, IO密集型任務
- 多進程更穩定, 系統的子進程互相內存獨立, 子進程崩潰不會影響其它進程(主進程除外)
- 多線程更高效, 但由於所有線程共享進程的內存, 一個崩潰會引發系統強制結束整個進程
- 計算密集型任務, 如視頻高清解碼, 主要開銷在CPU計算, 任務數=核心數最好
- IO密集型任務, 如網絡, 磁盤IO等, CPU消耗很少, 大部分時間都是在等待IO操作完成, 任務越多, CPU效率越高(有限度)
CPU和IO操作間巨大的速度差異催生了異步IO
既然一個任務在執行時大部分時間都是CPU等待IO, 單進程單線程並不能充分的利用CPU資源, 在CPU等待IO的時候, 不放用空閑時間去執行CPU計算任務. "現代操作系統對IO操作已經做出了巨大改進, 最大的特點就是支持異步IO. 如果充分利用操作系統提供的異步IO支持, 就可以用單進程單線程模型來執行多任務." (Android里的AsyncTask就是一種異步, 但是它本質是另外起了一個小線程, 所以異步就是多線程?)
分布式多進程, 還能夠通過網絡共享任務隊列
- Python的multiprocessing模塊, 支持多進程, 還支持多進程分布到多台設備
- 原理就是將多進程共享的Queue通過網絡暴露出去, 其它機器就可以訪問Queue了(就像remoterobotserver通過網絡暴露出robot的庫一樣)
性能調優工具
python -m cProfile mypython.py
Native+包裝器
Python程序可以直接加載已變成的二進制庫(.dll或.so)文件, 不用編寫C/C++代碼(直接源碼+NDK)或構建依賴(Gradle中添加.so庫構建依賴)