進程、線程和協程的理解

進程、線程和協程的理解

進程線程協程之間的關係和區別也困擾我一陣子了,最近有一些心得,寫一下。

進程擁有自己獨立的堆和棧,既不共享堆,亦不共享棧,進程由操作系統調度。

線程擁有自己獨立的棧和共享的堆,共享堆,不共享棧,線程亦由操作系統調度(標準線程是的)。

協程和線程一樣共享堆,不共享棧,協程由程序員在協程的代碼裏顯示調度。

進程和其他兩個的區別還是很明顯的。

協程和線程的區別是:協程避免了無意義的調度,由此可以提高性能,但也因此,程序員必須自己承擔調度的責任,同時,協程也失去了標準線程使用多CPU的能力。

打個比方吧,假設有一個操作系統,是單核的,系統上沒有其他的程序需要運行,有兩個線程 A 和 B ,A 和 B 在單獨運行時都需要 10 秒來完成自己的任務,而且任務都是運算操作,A B 之間也沒有競爭和共享數據的問題。現在 A B 兩個線程並行,操作系統會不停的在 A B 兩個線程之間切換,達到一種僞並行的效果,假設切換的頻率是每秒一次,切換的成本是 0.1 秒(主要是棧切換),總共需要 20 + 19 * 0.1 = 21.9 秒。如果使用協程的方式,可以先運行協程 A ,A 結束的時候讓位給協程 B ,只發生一次切換,總時間是 20 + 1 * 0.1 = 20.1 秒。如果系統是雙核的,而且線程是標準線程,那麼 A B 兩個線程就可以真並行,總時間只需要 10 秒,而協程的方案仍然需要 20.1 秒。

一個實際一點的例子:thread.py

    #!/usr/bin/python
    # python thread.py
    # python -m gevent.monkey thread.py

    import threading

    class Thread(threading.Thread):

        def __init__(self, name):
            threading.Thread.__init__(self)
            self.name = name

        def run(self):
            for i in xrange(10):
                print self.name

    threadA = Thread("A")
    threadB = Thread("B")

    threadA.start()
    threadB.start()
    

運行:

python thread.py

如果你的輸出是均勻的:

A
B
A
B
...

那麼總共發生了 20 次切換:主線程 -> A -> B -> A -> B …

再看一個協程的例子:gr.py

    #!/usr/bin/python
    # python gr.py

    import greenlet

    def run(name, nextGreenlets):
        for i in xrange(10):
            print name
        if nextGreenlets:
            nextGreenlets.pop(0).switch(chr(ord(name) + 1), nextGreenlets)

    greenletA = greenlet.greenlet(run)
    greenletB = greenlet.greenlet(run)

    greenletA.switch('A', [greenletB])
    

greenlet 是 python 的協程實現。

運行:

python gr.py

此時發生了 2 次切換:主協程 -> A -> B

可能你已經注意到了,還有一個命令:

python -m gevent.monkey thread.py

gevent 是基於 greenlet 的一個 python 庫,它可以把 python 的內置線程用 greenlet 包裝,這樣在我們使用線程的時候,實際上使用的是協程,在上一個協程的例子裏,協程 A 結束時,由協程 A 讓位給協程 B ,而在 gevent 裏,所有需要讓位的協程都讓位給主協程,由主協程決定運行哪一個協程,gevent 也會包裝一些可能需要阻塞的方法,比如 sleep ,比如讀 socket ,比如等待鎖,等等,在這些方法裏會自動讓位給主協程,而不是由程序員顯示讓位,這樣程序員就可以按照線程的模式進行線性編程,不需要考慮切換的邏輯。

gevent 版的命令發生了 3 次切換:主協程 -> A -> 主協程 -> B

假設代碼質量相同,用原生的協程實現需要切換 n 次,用協程包裝後的線程實現,就需要 2n - 1 次,姑且算是兩倍吧。很顯然,單純從效率上來說,代碼質量相同的前提下,用 gevent 永遠也不可能比用 greenlet 快,然而,問題往往不那麼單純,比方說,單純從效率上來說,代碼質量相同的前提下,用 C 實現的程序永遠不可能比彙編快。

再來說說 python 的線程,python 的線程不是標準線程,在 python 中,一個進程內的多個線程只能使用一個 CPU 。

重新來看一下協程和線程的區別:協程避免了無意義的調度,由此可以提高性能,但也因此,程序員必須自己承擔調度的責任,同時,協程也失去了標準線程使用多CPU的能力。

如果使用 gevent 包裝後的線程,程序員就不必承擔調度的責任,而 python 的線程本身就沒有使用多 CPU 的能力,那麼,用 gevent 包裝後的線程,取代 python 的內置線程,不是隻有避免無意義的調度,提高性能的好處,而沒有什麼壞處了嗎?

答案是否定的。舉一個例子,有一個 GUI 程序,上面有兩個按鈕,一個 運算 一個 取消 ,點擊運算,會有一個運算線程啓動,不停的運算,點擊取消,會取消這個線程,如果使用 python 的內置線程或者標準線程,都是沒有問題的,即便運算線程不停的運算,調度器仍然會給 GUI 線程分配時間片,用戶可以點擊取消,然而,如果使用 gevent 包裝後的線程就完蛋了,一旦運算開始,GUI 就會失去相應,因爲那個運算線程(協程)霸着 CPU 不讓位。不單是 GUI ,所有和用戶交互的程序都會有這個問題。


本文轉自http://blog.leiqin.name/2012/12/02/%E8%BF%9B%E7%A8%8B%E3%80%81%E7%BA%BF%E7%A8%8B%E5%92%8C%E5%8D%8F%E7%A8%8B%E7%9A%84%E7%90%86%E8%A7%A3.html

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