20 Python多線程

多線程編程技術(shù)可以實(shí)現(xiàn)代碼并行,優(yōu)化處理能力,同時(shí)可以將代碼劃分為功能更小的模塊直撤,使代碼的可重用性更好。
這里將介紹Python中的多線程編程蜕着。多線程一直是Python學(xué)習(xí)中的重點(diǎn)和難點(diǎn)谋竖,需要反復(fù)練習(xí)和研究红柱。

線程和進(jìn)程

在學(xué)習(xí)多線程的使用之前,需要先了解線程蓖乘、進(jìn)程锤悄、多線程的概念。

1 進(jìn)程

進(jìn)程(Process嘉抒,有時(shí)被稱為重量級(jí)進(jìn)程)是程序的一次執(zhí)行零聚。每個(gè)進(jìn)程都有自己的地址空間、內(nèi)存些侍、數(shù)據(jù)棧以及記錄運(yùn)行軌跡的輔助數(shù)據(jù)隶症,操作系統(tǒng)管理運(yùn)行的所有進(jìn)程,并為這些進(jìn)程公平分配時(shí)間岗宣。進(jìn)程可以通過(guò)fork和spawn操作完成其他任務(wù)蚂会。因?yàn)楦鱾€(gè)進(jìn)程有自己的內(nèi)存
空間、數(shù)據(jù)棧等耗式,所以只能使用進(jìn)程間通信(IPC)胁住,而不能直接共享信息。

2 線程

線程(Thread刊咳,有時(shí)被稱為輕量級(jí)進(jìn)程)跟進(jìn)程有些相似彪见,不同的是所有線程運(yùn)行在同一個(gè)進(jìn)程中,共享運(yùn)行環(huán)境娱挨。
線程有開(kāi)始余指、順序執(zhí)行和結(jié)束3部分,有一個(gè)自己的指令指針跷坝,記錄運(yùn)行到什么地方浪规。線程的運(yùn)行可能被搶占(中斷)或暫時(shí)被掛起(睡眠),從而讓其他線程運(yùn)行探孝,這叫作讓步。一個(gè)進(jìn)程中的各個(gè)線程之間共享同一片數(shù)據(jù)空間誉裆,所以線程之間可以比進(jìn)程之間更方便地共享數(shù)據(jù)和相互通信顿颅。
線程一般是并發(fā)執(zhí)行的。正是由于這種并行和數(shù)據(jù)共享的機(jī)制足丢,使得多個(gè)任務(wù)的合作變得可能粱腻。實(shí)際上,在單CPU系統(tǒng)中斩跌,真正的并發(fā)并不可能绍些,每個(gè)線程會(huì)被安排成每次只運(yùn)行一小會(huì)兒,然后就把CPU讓出來(lái)耀鸦,讓其他線程運(yùn)行柬批。
在進(jìn)程的整個(gè)運(yùn)行過(guò)程中啸澡,每個(gè)線程都只做自己的事,需要時(shí)再跟其他線程共享運(yùn)行結(jié)果氮帐。多個(gè)線程共同訪問(wèn)同一片數(shù)據(jù)不是完全沒(méi)有危險(xiǎn)的嗅虏,由于數(shù)據(jù)訪問(wèn)的順序不一樣,因此有可能導(dǎo)致數(shù)據(jù)結(jié)果不一致的問(wèn)題上沐,這叫作競(jìng)態(tài)條件皮服。大多數(shù)線程庫(kù)都帶有一系列同步原語(yǔ),用于控制線程的執(zhí)行和數(shù)據(jù)的訪問(wèn)参咙。

3 多線程與多進(jìn)程

對(duì)于“多任務(wù)”這個(gè)詞龄广,相信讀者不會(huì)第一次看見(jiàn),現(xiàn)在的操作系統(tǒng)(如Mac OS X蕴侧、UNIX择同、Linux、Windows等)都支持“多任務(wù)”操作系統(tǒng)戈盈。
什么叫“多任務(wù)”呢奠衔?簡(jiǎn)單地說(shuō),就是系統(tǒng)可以同時(shí)運(yùn)行多個(gè)任務(wù)塘娶。比如归斤,一邊用瀏覽器上網(wǎng),一邊聽(tīng)云音樂(lè)刁岸,一邊聊天脏里,這就是多任務(wù)。此時(shí)手頭已經(jīng)有3個(gè)任務(wù)在運(yùn)行了虹曙。如果查看任務(wù)管理器迫横,可以看到還有很多任務(wù)悄悄在后臺(tái)運(yùn)行著,只是桌面上沒(méi)有顯示而已酝碳。
對(duì)于操作系統(tǒng)來(lái)說(shuō)矾踱,一個(gè)任務(wù)就是一個(gè)進(jìn)程,開(kāi)啟多個(gè)任務(wù)就是多進(jìn)程疏哗。
有些進(jìn)程不止可以同時(shí)做一件事呛讲,比如Word可以同時(shí)打字、拼寫檢查返奉、打印等贝搁。在一個(gè)進(jìn)程內(nèi)部,要同時(shí)做多件事芽偏,就需要同時(shí)運(yùn)行多個(gè)線程雷逆。
多線程類似于同時(shí)執(zhí)行多個(gè)不同的程序,多線程運(yùn)行有以下3個(gè)優(yōu)點(diǎn):
(1)使用線程可以把占據(jù)長(zhǎng)時(shí)間的程序中的任務(wù)放到后臺(tái)去處理污尉。
(2)用戶界面可以更加吸引人膀哲,比如用戶單擊一個(gè)按鈕往产,用于觸發(fā)某些事件的處理,可以彈出一個(gè)進(jìn)度條顯示處理的進(jìn)度等太。
(3)程序的運(yùn)行速度可能加快捂齐。
在實(shí)現(xiàn)一些等待任務(wù)(如用戶輸入、文件讀寫和網(wǎng)絡(luò)收發(fā)數(shù)據(jù)等)時(shí)缩抡,使用多線程更加有用奠宜。在這種情況下,我們可以釋放一些珍貴資源(如內(nèi)存占用等)瞻想。
線程在執(zhí)行過(guò)程中與進(jìn)程還是有區(qū)別的压真。每個(gè)獨(dú)立線程有一個(gè)程序運(yùn)行的入口、順序執(zhí)行序列和程序的出口蘑险。但是線程不能獨(dú)立執(zhí)行滴肿,必須依存在進(jìn)程中,由進(jìn)程提供多個(gè)線程執(zhí)行控制佃迄。
由于每個(gè)進(jìn)程至少要干一件事泼差,因此一個(gè)進(jìn)程至少有一個(gè)線程。當(dāng)然呵俏,如Word這種復(fù)雜的進(jìn)程可以有多個(gè)線程堆缘,多個(gè)線程可以同時(shí)執(zhí)行。多線程的執(zhí)行方式和多進(jìn)程是一樣的普碎,也是由操作系統(tǒng)在多個(gè)線程之間快速切換吼肥,讓每個(gè)線程都短暫交替運(yùn)行,看起來(lái)就像同時(shí)執(zhí)行一樣麻车。當(dāng)然缀皱,真正同時(shí)執(zhí)行多線程需要多核CPU才能實(shí)現(xiàn)。
我們前面編寫的所有Python程序都是執(zhí)行單任務(wù)的進(jìn)程动猬,也就是只有一個(gè)線程啤斗。如果我們要同時(shí)執(zhí)行多個(gè)任務(wù),怎么辦呢赁咙?
有兩種解決方法:一種方法是啟動(dòng)多個(gè)進(jìn)程钮莲,每個(gè)進(jìn)程雖然只有一個(gè)線程,但多個(gè)進(jìn)程可以一起執(zhí)行多個(gè)任務(wù)序目。另一種方法是啟動(dòng)一個(gè)進(jìn)程,在一個(gè)進(jìn)程內(nèi)啟動(dòng)多個(gè)線程伯襟,這樣多個(gè)線程也可以一起執(zhí)行多個(gè)任務(wù)猿涨。
當(dāng)然,還有第3種方法姆怪,就是啟動(dòng)多個(gè)進(jìn)程叛赚,每個(gè)進(jìn)程再啟動(dòng)多個(gè)線程澡绩,這樣同時(shí)執(zhí)行的任務(wù)就更多了,不過(guò)這種模型過(guò)于復(fù)雜俺附,實(shí)際很少采用肥卡。
同時(shí)執(zhí)行多個(gè)任務(wù)時(shí),各個(gè)任務(wù)之間并不是沒(méi)有關(guān)聯(lián)的事镣,而是需要相互通信和協(xié)調(diào)步鉴,有時(shí)任務(wù)1必須暫停等待任務(wù)2完成后才能繼續(xù)執(zhí)行,有時(shí)任務(wù)3和任務(wù)4不能同時(shí)執(zhí)行璃哟。多進(jìn)程和多線程程序的復(fù)雜度遠(yuǎn)遠(yuǎn)高于我們前面寫的單進(jìn)程氛琢、單線程的程序。
不過(guò)很多時(shí)候随闪,沒(méi)有多任務(wù)還真不行阳似。想想在電腦上看電影,必須由一個(gè)線程播放視頻铐伴,另一個(gè)線程播放音頻撮奏,否則使用單線程實(shí)現(xiàn)只能先把視頻播放完再播放音頻,或者先把音頻播放完再播放視頻当宴,這樣顯然不行畜吊。
總而言之,多線程是多個(gè)相互關(guān)聯(lián)的線程的組合即供,多進(jìn)程是多個(gè)互相獨(dú)立的進(jìn)程的組合定拟。線程是最小的執(zhí)行單元,進(jìn)程至少由一個(gè)線程組成逗嫡。

使用線程

如何使用線程青自,線程中有哪些比較值得學(xué)習(xí)的模塊呢?本節(jié)將對(duì)線程的使用做概念性的講解驱证,下一節(jié)再給出一些具體示例以供參考延窜。

1 全局解釋器鎖

Python代碼的執(zhí)行由Python虛擬機(jī)(解釋器主循環(huán))控制。Python在設(shè)計(jì)之初就考慮到在主循環(huán)中只能有一個(gè)線程執(zhí)行抹锄,雖然Python解釋
器中可以“運(yùn)行”多個(gè)線程逆瑞,但是在任意時(shí)刻只有一個(gè)線程在解釋器中運(yùn)行。
Python虛擬機(jī)的訪問(wèn)由全局解釋器鎖(GIL)控制伙单,這個(gè)鎖能保證同一時(shí)刻只有一個(gè)線程運(yùn)行获高。
在多線程環(huán)境中,Python虛擬機(jī)按以下方式執(zhí)行:
(1)設(shè)置GIL吻育。
(2)切換到一個(gè)線程運(yùn)行念秧。
(3)運(yùn)行指定數(shù)量的字節(jié)碼指令或線程主動(dòng)讓出控制(可以調(diào)用time.sleep(0))。
(4)把線程設(shè)置為睡眠狀態(tài)布疼。
(5)解鎖GIL摊趾。
(6)再次重復(fù)以上所有步驟币狠。
在調(diào)用外部代碼(如C/C++擴(kuò)展函數(shù))時(shí),GIL將被鎖定砾层。直到這個(gè)函數(shù)結(jié)束為止(由于在此期間沒(méi)有運(yùn)行Python的字節(jié)碼漩绵,因此不會(huì)做線程切換),編寫擴(kuò)展的程序員可以主動(dòng)解鎖GIL肛炮。

2 退出線程

當(dāng)一個(gè)線程結(jié)束計(jì)算止吐,它就退出了。線程可以調(diào)用_thread.exit()等退出函數(shù)铸董,也可以使用Python退出進(jìn)程的標(biāo)準(zhǔn)方法(如sys.exit()或拋出一個(gè)SystemExit異常)祟印,不過(guò)不可以直接“殺掉”(kill)一個(gè)線程。
不建議使用_thread模塊粟害。很明顯的一個(gè)原因是蕴忆,當(dāng)主線程退出時(shí),其他線程如果沒(méi)有被清除就會(huì)退出悲幅。另一個(gè)模塊threading能確保所有“重要的”子線程都退出后套鹅,進(jìn)程才會(huì)結(jié)束。

3 Python的線程模塊

Python提供了幾個(gè)用于多線程編程的模塊汰具,包括_thread卓鹿、threading和Queue等。_thread和threading模塊允許程序員創(chuàng)建和管理線程留荔。_thread模塊提供了基本線程和鎖的支持吟孙,threading提供了更高級(jí)別、功能更強(qiáng)的線程管理功能聚蝶。Queue模塊允許用戶創(chuàng)建一個(gè)可以用于多個(gè)線程之間共享數(shù)據(jù)的隊(duì)列數(shù)據(jù)結(jié)構(gòu)杰妓。
避免使用_thread模塊,原因有3點(diǎn)碘勉。首先巷挥,更高級(jí)別的threading模塊更為先進(jìn),對(duì)線程的支持更為完善验靡,而且使用_thread模塊里的屬性有可能與threading沖突倍宾;其次,低級(jí)別的_thread模塊的同步原語(yǔ)很少(實(shí)際上只有一個(gè))胜嗓,而threading模塊有很多高职;再者,_thread模塊中在主線程結(jié)束時(shí)辞州,所有線程都會(huì)被強(qiáng)制結(jié)束怔锌,沒(méi)有警告也不會(huì)有正常清除工作,至少threading模塊能確保重要子線程退出后進(jìn)程才退出。

_thread模塊

Python中調(diào)用_thread模塊中的start_new_thread()函數(shù)產(chǎn)生新線程产禾。_thread的語(yǔ)法如下:
_thread.start_new_thread (function, args[, kwargs])
其中,function為線程函數(shù)牵啦;args為傳遞給線程函數(shù)的參數(shù)亚情,必須是tuple類型;kwargs為可選參數(shù)哈雏。
_thread模塊除了產(chǎn)生線程外楞件,還提供基本同步數(shù)據(jù)結(jié)構(gòu)鎖對(duì)象(lock object,也叫原語(yǔ)鎖裳瘪、簡(jiǎn)單鎖土浸、互斥鎖、互斥量彭羹、二值信號(hào)量)黄伊。同步原語(yǔ)與線程管理是密不可分的。
我們看如下示例派殷。

#! /usr/bin/python
# -*-coding:UTF-8-*-

import _thread
from time import sleep
from datetime import datetime

date_time_format = '%y-%M-%d %H:%M:%S'

def date_time_str(date_time):
    return datetime.strftime(date_time, date_time_format)

def loop_one():
    print('+++線程一開(kāi)始于:', date_time_str(datetime.now()))
    print('+++線程一休眠4 秒')
    sleep(4)
    print('+++線程一休眠結(jié)束还最,結(jié)束于:', date_time_str(datetime.now()))

def loop_two():
    print('***線程二開(kāi)始時(shí)間:', date_time_str(datetime.now()))
    print('***線程二休眠2 秒')
    sleep(2)
    print('***線程二休眠結(jié)束,結(jié)束時(shí)間:', date_time_str(datetime.now()))

def main():
    print('------所有線程開(kāi)始時(shí)間:', date_time_str(datetime.now()))
    _thread.start_new_thread(loop_one, ())
    _thread.start_new_thread(loop_two, ())
    sleep(6)
    print('------所有線程結(jié)束時(shí)間:', date_time_str(datetime.now()))

if __name__ == '__main__':
    main()

執(zhí)行結(jié)果如下:

------所有線程開(kāi)始時(shí)間: 16-44-06 21:44:05
+++線程一開(kāi)始于: 16-44-06 21:44:05
+++線程一休眠4 秒
***線程二開(kāi)始時(shí)間: 16-44-06 21:44:05
***線程二休眠2 秒
***線程二休眠結(jié)束毡惜,結(jié)束時(shí)間: 16-44-06 21:44:07
+++線程一休眠結(jié)束拓轻,結(jié)束于: 16-44-06 21:44:09
------所有線程結(jié)束時(shí)間: 16-44-06 21:44:11

_thread模塊提供了簡(jiǎn)單的多線程機(jī)制,兩個(gè)循環(huán)并發(fā)執(zhí)行经伙,總的運(yùn)行時(shí)間為最慢的線程的運(yùn)行時(shí)間(主線程6s)扶叉,而不是所有線程的運(yùn)行時(shí)間之和。start_new_thread()要求至少傳兩個(gè)參數(shù)帕膜,即使想要運(yùn)行的函數(shù)不要參數(shù)枣氧,也要傳一個(gè)空元組。
sleep(6)是讓主線程停下來(lái)泳叠。主線程一旦運(yùn)行結(jié)束作瞄,就關(guān)閉運(yùn)行著的其他兩個(gè)線程。這可能造成主線程過(guò)早或過(guò)晚退出危纫,這時(shí)就要使用線程鎖宗挥,主線程可認(rèn)在兩個(gè)子線程都退出后立即退出。
示例代碼如下:

#! /usr/bin/python
# -*-coding:UTF-8-*-

import _thread
from time import sleep
from datetime import datetime

loops = [4, 2]
date_time_format = '%y-%M-%d %H:%M:%S'

def date_time_str(date_time):
    return datetime.strftime(date_time, date_time_format)

def loop(n_loop, n_sec, lock):
    print('線程(', n_loop, ')開(kāi)始執(zhí)行:',
           date_time_str(datetime.now()), '种蝶,先休眠(', n_sec, ')秒')
    sleep(n_sec)
    print('線程(', n_loop, ')休眠結(jié)束契耿,結(jié)束于:', date_time_str(datetime.now()))
    lock.release()

def main():
    print('---所有線程開(kāi)始執(zhí)行...')
    locks = []
    n_loops = range(len(loops))

    for i in n_loops:
         lock = _thread.allocate_lock()
         lock.acquire()
         locks.append(lock)

    for i in n_loops:
         _thread.start_new_thread(loop, (i, loops[i], locks[i]))

    for i in n_loops:
         while locks[i].locked(): pass

    print('---所有線程執(zhí)行結(jié)束:', date_time_str(datetime.now()))

if __name__ == '__main__':
    main()

執(zhí)行結(jié)果如下:

---所有線程開(kāi)始執(zhí)行...
線程( 1 )開(kāi)始執(zhí)行: 16-44-06 21:44:11 ,先休眠( 2 )秒
線程( 0 )開(kāi)始執(zhí)行: 16-44-06 21:44:11 螃征,先休眠( 4 )秒
線程( 1 )休眠結(jié)束搪桂,結(jié)束于: 16-44-06 21:44:13
線程( 0 )休眠結(jié)束,結(jié)束于: 16-44-06 21:44:15
---所有線程執(zhí)行結(jié)束: 16-44-06 21:44:15

可以看到,以上代碼使用了線程鎖踢械。

threading模塊

更高級(jí)別的threading模塊不僅提供了Thread類酗电,還提供了各種非常好用的同步機(jī)制。
_thread模塊不支持守護(hù)線程内列,當(dāng)主線程退出時(shí)撵术,所有子線程無(wú)論是否在工作,都會(huì)被強(qiáng)行退出话瞧。threading模塊支持守護(hù)線程嫩与,守護(hù)線程一般是一個(gè)等待客戶請(qǐng)求的服務(wù)器,如果沒(méi)有客戶提出請(qǐng)求交排,就一直等著划滋。如果設(shè)定一個(gè)線程為守護(hù)線程,就表示這個(gè)線程不重要埃篓,在進(jìn)程退出時(shí)处坪,不用等待這個(gè)線程退出。如果主線程退出時(shí)不用等待子線程完成架专,就要設(shè)定這些線程的daemon屬性稻薇,即在線程Thread.start()開(kāi)始前,調(diào)用setDaemon()函數(shù)設(shè)定線程的daemon標(biāo)志(Thread.setDaemon(True))胶征,表示這個(gè)線程“不重要”塞椎。如果一定要等待子線程執(zhí)行完成再退出主線程,就什么都不用做或顯式調(diào)用Thread.setDaemon(False)以保證daemon標(biāo)志為False睛低,可以調(diào)用Thread.isDaemon()函數(shù)判斷daemon標(biāo)志的值案狠。新的子線程會(huì)繼承父線程的daemon標(biāo)志,整個(gè)Python在所有非守護(hù)線程退出后才會(huì)結(jié)束钱雷,即進(jìn)程中沒(méi)有非守護(hù)線程存在時(shí)才結(jié)束骂铁。

threadingThread

Thread有很多_thread模塊里沒(méi)有的函數(shù),Thread對(duì)象的函數(shù)很豐富罩抗。下面創(chuàng)建一個(gè)Thread的實(shí)例拉庵,傳給它一個(gè)函數(shù)。示例如下:

#! /usr/bin/python
# -*-coding:UTF-8-*-

import threading
from time import sleep
from datetime import datetime

loops = [4, 2]
date_time_format = '%y-%M-%d %H:%M:%S'

def date_time_str(date_time):
    return datetime.strftime(date_time, date_time_format)

def loop(n_loop, n_sec):
    print('線程(', n_loop, ')開(kāi)始執(zhí)行:',
           date_time_str(datetime.now()), '套蒂,先休眠(', n_sec, ')秒')
    sleep(n_sec)
    print('線程(', n_loop, ')休眠結(jié)束钞支,結(jié)束于:', date_time_str(datetime.now()))

def main():
    print('---所有線程開(kāi)始執(zhí)行:', date_time_str(datetime.now()))
    threads = []
    n_loops = range(len(loops))

    for i in n_loops:
         t = threading.Thread(target=loop, args=(i, loops[i]))
         threads.append(t)

    for i in n_loops:    # start threads
         threads[i].start()

    for i in n_loops:    # wait for all
         threads[i].join() # threads to finish

    print('---所有線程執(zhí)行結(jié)束于:', date_time_str(datetime.now()))

if __name__ == '__main__':
    main()

執(zhí)行結(jié)果如下:

---所有線程開(kāi)始執(zhí)行: 16-44-06 21:44:15
線程( 0 )開(kāi)始執(zhí)行: 16-44-06 21:44:15 ,先休眠( 4 )秒
線程( 1 )開(kāi)始執(zhí)行: 16-44-06 21:44:15 操刀,先休眠( 2 )秒
線程( 1 )休眠結(jié)束烁挟,結(jié)束于: 16-44-06 21:44:17
線程( 0 )休眠結(jié)束,結(jié)束于: 16-44-06 21:44:19
---所有線程執(zhí)行結(jié)束于: 16-44-06 21:44:19

由執(zhí)行結(jié)果我們看到骨坑,實(shí)例化一個(gè)Thread(調(diào)用Thread())與調(diào)用_thread.start_new_thread()最大的區(qū)別是新的線程不會(huì)立即開(kāi)始撼嗓。創(chuàng)建線程對(duì)象卻不想馬上開(kāi)始運(yùn)行線程時(shí),Thread是一個(gè)很有用的同步特性。所有線程都創(chuàng)建之后且警,再一起調(diào)用start()函數(shù)啟動(dòng)粉捻,而不是每創(chuàng)建一個(gè)線程就啟動(dòng)。而且不用管理一堆鎖的狀態(tài)(分配鎖斑芜、獲得鎖杀迹、釋放鎖、檢查鎖的等狀態(tài))押搪,只要簡(jiǎn)單對(duì)每個(gè)線程調(diào)用join()主線程,等待子線程結(jié)束即可浅碾。join()還可以設(shè)置timeout參數(shù)大州,即主線程的超時(shí)時(shí)間。
join()的另一個(gè)比較重要的方面是可以完全不用調(diào)用垂谢。一旦線程啟動(dòng)厦画,就會(huì)一直運(yùn)行,直到線程的函數(shù)結(jié)束并退出為止滥朱。如果主線程除了等線程結(jié)束外根暑,還有其他事情要做,就不用調(diào)用join()徙邻,只有在等待線程結(jié)束時(shí)才調(diào)用排嫌。
我們?cè)倏词纠瑒?chuàng)建一個(gè)Thread的實(shí)例缰犁,并傳給它一個(gè)可調(diào)用的類對(duì)象淳地。代碼如下:

#! /usr/bin/python
# -*-coding:UTF-8-*-

import threading
from time import sleep
from datetime import datetime

loops = [4, 2]
date_time_format = '%y-%M-%d %H:%M:%S'

class ThreadFunc(object):
    def __init__(self, func, args, name=''):
         self.name = name
         self.func = func
         self.args = args

    def __call__(self):
         self.func(*self.args)

def date_time_str(date_time):
    return datetime.strftime(date_time, date_time_format)

def loop(n_loop, n_sec):
    print('線程(', n_loop, ')開(kāi)始執(zhí)行:',
           date_time_str(datetime.now()), ',先休眠(', n_sec, ')秒')
    sleep(n_sec)
    print('線程(', n_loop, ')休眠結(jié)束帅容,結(jié)束于:', date_time_str(datetime.now()))

def main():
    print('---所有線程開(kāi)始執(zhí)行:', date_time_str(datetime.now()))
    threads = []
    nloops = range(len(loops))

    for i in nloops: # create all threads
         t = threading.Thread(
             target=ThreadFunc(loop, (i, loops[i]), loop.__name__))
         threads.append(t)

    for i in nloops: # start all threads
         threads[i].start()

    for i in nloops: # wait for completion
         threads[i].join()

    print('---所有線程執(zhí)行結(jié)束于:', date_time_str(datetime.now()))

if __name__ == '__main__':
    main()

執(zhí)行結(jié)果如下:

---所有線程開(kāi)始執(zhí)行: 16-03-06 22:03:18
線程( 0 )開(kāi)始執(zhí)行: 16-03-06 22:03:18 颇象,先休眠( 4 )秒
線程( 1 )開(kāi)始執(zhí)行: 16-03-06 22:03:18 ,先休眠( 2 )秒
線程( 1 )休眠結(jié)束并徘,結(jié)束于: 16-03-06 22:03:20
線程( 0 )休眠結(jié)束遣钳,結(jié)束于: 16-03-06 22:03:22
---所有線程執(zhí)行結(jié)束于: 16-03-06 22:03:22

由執(zhí)行結(jié)果看到,與傳一個(gè)函數(shù)很相似的一個(gè)方法是麦乞,在創(chuàng)建線程時(shí)蕴茴,傳一個(gè)可調(diào)用的類的實(shí)例供線程啟動(dòng)時(shí)執(zhí)行,這是多線程編程的一個(gè)面向?qū)ο蟮姆椒ń阒薄O鄬?duì)于一個(gè)或幾個(gè)函數(shù)來(lái)說(shuō)荐开,類對(duì)象可以使用類的強(qiáng)大功能。創(chuàng)建新線程時(shí)简肴,Thread對(duì)象會(huì)調(diào)用ThreadFunc對(duì)象晃听,這時(shí)會(huì)用到一個(gè)特殊函數(shù)__call__()。由于已經(jīng)有了要用的參數(shù),因此不用再傳到Thread()的構(gòu)造函數(shù)中能扒。對(duì)于有一個(gè)參數(shù)的元組佣渴,要使用self.func(*self.args)方法。
Thread派生一個(gè)子類初斑,創(chuàng)建這個(gè)子類的實(shí)例辛润。從上面的代碼派生的代碼如下:

#! /usr/bin/python
# -*-coding:UTF-8-*-

import threading
from time import sleep
from datetime import datetime

loops = [4, 2]
date_time_format = '%y-%M-%d %H:%M:%S'

class MyThread(threading.Thread):
    def __init__(self, func, args, name=''):
         threading.Thread.__init__(self)
         self.name = name
         self.func = func
         self.args = args

    def getResult(self):
         return self.res

    def run(self):
         print('starting', self.name, 'at:', date_time_str(datetime.now()))
         self.res = self.func(*self.args)
         print(self.name, 'finished at:', date_time_str(datetime.now()))

def date_time_str(date_time):
    return datetime.strftime(date_time, date_time_format)

def loop(n_loop, n_sec):
    print('線程(', n_loop, ')開(kāi)始執(zhí)行:',
           date_time_str(datetime.now()), ',先休眠(', n_sec, ')秒')
    sleep(n_sec)
    print('線程(', n_loop, ')休眠結(jié)束见秤,結(jié)束于:', date_time_str(datetime.now()))

def main():
    print('---所有線程開(kāi)始執(zhí)行:', date_time_str(datetime.now()))
    threads = []
    n_loops = range(len(loops))

    for i in n_loops:
         t = MyThread(loop, (i, loops[i]),
         loop.__name__)
         threads.append(t)

    for i in n_loops:
         threads[i].start()

    for i in n_loops:
         threads[i].join()

    print('---所有線程執(zhí)行結(jié)束于:', date_time_str(datetime.now()))
if __name__ == '__main__':
    main()

執(zhí)行結(jié)果如下:

---所有線程開(kāi)始執(zhí)行: 16-22-06 22:22:20
starting loop at: 16-22-06 22:22:20
線程( 0 )開(kāi)始執(zhí)行: 16-22-06 22:22:20 砂竖,先休眠( 4 )秒
starting loop at: 16-22-06 22:22:20
線程( 1 )開(kāi)始執(zhí)行: 16-22-06 22:22:20 ,先休眠( 2 )秒
線程( 1 )休眠結(jié)束鹃答,結(jié)束于: 16-22-06 22:22:22
loop finished at: 16-22-06 22:22:22
線程( 0 )休眠結(jié)束乎澄,結(jié)束于: 16-22-06 22:22:24
loop finished at: 16-22-06 22:22:24
---所有線程執(zhí)行結(jié)束于: 16-22-06 22:22:24

由代碼片段和執(zhí)行結(jié)果我們看到,子類化Thread類测摔,MyThread子類的構(gòu)造函數(shù)一定要先調(diào)用基類的構(gòu)造函數(shù)置济,特殊函數(shù)__call__()在子類中,名字要改為run()锋八。在MyThread類中浙于,加入一些用于調(diào)試的輸出信息,把代碼保存到MyThread模塊中挟纱,并導(dǎo)入這個(gè)類羞酗。使用self.func()函數(shù)運(yùn)行這些函數(shù),并把結(jié)果保存到實(shí)現(xiàn)的self.res屬性中紊服,創(chuàng)建一個(gè)新函數(shù)getResult()得到結(jié)果整慎。

線程同步

如果多個(gè)線程共同修改某個(gè)數(shù)據(jù),就可能出現(xiàn)不可預(yù)料的結(jié)果围苫。為了保證數(shù)據(jù)的正確性裤园,需要對(duì)多個(gè)線程進(jìn)行同步。
使用Thread對(duì)象的LockRLock可以實(shí)現(xiàn)簡(jiǎn)單的線程同步剂府,這兩個(gè)對(duì)象都有acquire方法和release方法拧揽。對(duì)于每次只允許一個(gè)線程操作的數(shù)據(jù),可以將操作放到acquirerelease方法之間腺占。
多線程的優(yōu)勢(shì)在于可以同時(shí)運(yùn)行多個(gè)任務(wù)淤袜,但當(dāng)線程需要共享數(shù)據(jù)時(shí),可能存在數(shù)據(jù)不同步的問(wèn)題衰伯。
考慮這樣一種情況:一個(gè)列表里所有元素都是0铡羡,線程set從后向前把所有元素改成1,而線程print負(fù)責(zé)從前往后讀取列表并輸出意鲸。
線程set開(kāi)始改的時(shí)候烦周,線程print可能就來(lái)輸出列表了尽爆,輸出就成了一半0一半1,這就是數(shù)據(jù)不同步的問(wèn)題读慎。為了避免這種情況漱贱,引入了鎖的概念。
鎖有兩種狀態(tài)——鎖定和未鎖定夭委。當(dāng)一個(gè)線程(如set)要訪問(wèn)共享數(shù)據(jù)時(shí)幅狮,必須先獲得鎖定;如果已經(jīng)有別的線程(如print)獲得鎖定了株灸,就讓線程set暫停崇摄,也就是同步阻塞;等到線程print訪問(wèn)完畢慌烧,釋放鎖以后逐抑,再讓線程set繼續(xù)。
經(jīng)過(guò)這樣的處理杏死,輸出列表時(shí)要么全部輸出0,要么全部輸出1捆交,不會(huì)再出現(xiàn)一半0一半1的尷尬場(chǎng)面淑翼。
示例代碼如下:

#! /usr/bin/python
# -*-coding:UTF-8-*-

import threading
from time import sleep
from datetime import datetime

date_time_format = '%y-%M-%d %H:%M:%S'

class MyThread (threading.Thread):
    def __init__(self, threadID, name, counter):
         threading.Thread.__init__(self)
         self.threadID = threadID
         self.name = name
         self.counter = counter

    def run(self):
         print ("開(kāi)啟線程: " + self.name)
         # 獲取鎖,用于線程同步
         threadLock.acquire()
         print_time(self.name, self.counter, 3)
         # 釋放鎖品追,開(kāi)啟下一個(gè)線程
        threadLock.release()

def date_time_str(date_time):
    return datetime.strftime(date_time, date_time_format)

def print_time(threadName, delay, counter):
    while counter:
         sleep(delay)
         print ("%s: %s" % (threadName, date_time_str(datetime.now())))
         counter -= 1

def main():
    # 創(chuàng)建新線程
    thread1 = MyThread(1, "Thread-1", 1)
    thread2 = MyThread(2, "Thread-2", 2)

    # 開(kāi)啟新線程
    thread1.start()
    thread2.start()

    # 添加線程到線程列表
    threads.append(thread1)
    threads.append(thread2)

    # 等待所有線程完成
    for t in threads:
         t.join()
    print("退出主線程")

if __name__ == "__main__":
    threadLock = threading.Lock()
    threads = []
    main()

執(zhí)行結(jié)果如下:

開(kāi)啟線程: Thread-1
開(kāi)啟線程: Thread-2
Thread-1: 16-15-06 23:15:25
Thread-1: 16-15-06 23:15:26
Thread-1: 16-15-06 23:15:27
Thread-2: 16-15-06 23:15:29
Thread-2: 16-15-06 23:15:31
Thread-2: 16-15-06 23:15:33
退出主線程

由執(zhí)行結(jié)果看到玄括,程序正確得到了同步效果。

線程優(yōu)先級(jí)隊(duì)列

Queue模塊可以用來(lái)進(jìn)行線程間的通信肉瓦,讓各個(gè)線程之間共享數(shù)據(jù)遭京。

Python的Queue模塊提供了同步、線程安全的隊(duì)列類泞莉,包括FIFO(先入先出)隊(duì)列Queue哪雕、LIFO(后入先出)隊(duì)列LifoQueue和優(yōu)先級(jí)隊(duì)列PriorityQueue。這些隊(duì)列都實(shí)現(xiàn)了鎖原語(yǔ)鲫趁,能夠在多線程中直接使用斯嚎。可以使用隊(duì)列實(shí)現(xiàn)線程間的同步挨厚。

Queue模塊中的常用方法如表所示堡僻。
1.Queue模塊中的常用方法

下面通過(guò)以下示例了解其中一些方法的使用。

#! /usr/bin/python
# -*-coding:UTF-8-*-

import threading
import queue
from time import sleep

class MyThread (threading.Thread):
    def __init__(self, threadID, name, q):
         threading.Thread.__init__(self)
         self.threadID = threadID
         self.name = name
         self.q = q

    def run(self):
         print ("開(kāi)啟線程:" + self.name)
         process_data(self.name, self.q)
         print ("退出線程:" + self.name)

def process_data(threadName, q):
    while not exitFlag:
         queueLock.acquire()
         if not workQueue.empty():
             data = q.get()
             queueLock.release()
             print ("%s processing %s" % (threadName, data))
         else:
             queueLock.release()
         sleep(1)

def main():
    global exitFlag
    exitFlag = 0
    threadList = ["Thread-1", "Thread-2", "Thread-3"]
    nameList = ["One", "Two", "Three", "Four", "Five"]

    threads = []
    threadID = 1

    # 創(chuàng)建新線程
    for tName in threadList:
         thread = MyThread(threadID, tName, workQueue)
         thread.start()
         threads.append(thread)
         threadID += 1

    # 填充隊(duì)列
    queueLock.acquire()
    for word in nameList:
         workQueue.put(word)
    queueLock.release()

    # 等待隊(duì)列清空
    while not workQueue.empty():
         pass

    # 通知線程是退出的時(shí)候了
    exitFlag = 1

    # 等待所有線程完成
    for t in threads:
         t.join()
    print ("退出主線程")

if __name__ == "__main__":
    queueLock = threading.Lock()
    workQueue = queue.Queue(10)
    main()

執(zhí)行結(jié)果如下:

開(kāi)啟線程:Thread-1
開(kāi)啟線程:Thread-2
開(kāi)啟線程:Thread-3
Thread-3 processing One
Thread-2 processing Two
Thread-1 processing Three
Thread-3 processing Four
Thread-1 processing Five
退出線程:Thread-3
退出線程:Thread-1
退出線程:Thread-2
退出主線程

線程與進(jìn)程比較

多進(jìn)程和多線程是實(shí)現(xiàn)多任務(wù)最常用的兩種方式疫剃。下面通過(guò)線程切換钉疫、計(jì)算密集情況和異步性能3方面討論一下這兩種方式的優(yōu)缺點(diǎn)。
首先巢价,要實(shí)現(xiàn)多任務(wù)牲阁,我們通常會(huì)設(shè)計(jì)Master-Worker模式固阁,Master負(fù)責(zé)分配任務(wù),Worker負(fù)責(zé)執(zhí)行任務(wù)咨油。因此您炉,在多任務(wù)環(huán)境下,通常是一個(gè)Master役电、多個(gè)Worker赚爵。
如果用多進(jìn)程實(shí)現(xiàn)Master-Worker,主進(jìn)程就是Master法瑟,其他進(jìn)程就是Worker冀膝。
如果用多線程實(shí)現(xiàn)Master-Worker,主線程就是Master霎挟,其他線程就是Worker窝剖。
多進(jìn)程模式最大的優(yōu)點(diǎn)是穩(wěn)定性高,因?yàn)橐粋€(gè)子進(jìn)程崩潰不會(huì)影響主進(jìn)程和其他子進(jìn)程(當(dāng)然酥夭,主進(jìn)程掛了所有進(jìn)程就全掛了赐纱,但是Master進(jìn)程只負(fù)責(zé)分配任務(wù),掛掉的概率低)熬北。著名的Apache最早就采用多進(jìn)程模式疙描。
多進(jìn)程模式的缺點(diǎn)是創(chuàng)建進(jìn)程的代價(jià)大。在UNIX/Linux系統(tǒng)下用fork調(diào)用還行讶隐,在Windows下創(chuàng)建進(jìn)程開(kāi)銷非常大起胰。另外,操作系統(tǒng)能同時(shí)運(yùn)行的進(jìn)程數(shù)有限巫延,在內(nèi)存和CPU的限制下效五,如果幾千個(gè)進(jìn)程同時(shí)運(yùn)行,操作系統(tǒng)就連調(diào)度都會(huì)出問(wèn)題炉峰。
多線程模式通常比多進(jìn)程快一點(diǎn)畏妖,但是也快不了多少。多線程模式致命的缺點(diǎn)是任何一個(gè)線程掛掉都可能直接造成整個(gè)進(jìn)程崩潰疼阔,因?yàn)樗?br> 有線程共享進(jìn)程的內(nèi)存瓜客。在Windows中,如果一個(gè)線程執(zhí)行的代碼出了問(wèn)題竿开,就可以看到這樣的提示:“該程序執(zhí)行了非法操作谱仪,即將關(guān)閉”,其實(shí)往往是某個(gè)線程出了問(wèn)題否彩,但是操作系統(tǒng)會(huì)強(qiáng)制結(jié)束整個(gè)進(jìn)程疯攒。
在Windows中,多線程的效率比多進(jìn)程高列荔,所以微軟的IIS服務(wù)器默認(rèn)采用多線程模式敬尺。由于多線程存在穩(wěn)定性的問(wèn)題枚尼,因此IIS的穩(wěn)定性不如Apache。為了緩解這個(gè)問(wèn)題砂吞,IIS和Apache有了多進(jìn)程+多線程的混合模式署恍,問(wèn)題越來(lái)越復(fù)雜。

1 線程切換

無(wú)論是多進(jìn)程還是多線程蜻直,數(shù)量太多盯质,效率肯定上不去。
我們打個(gè)比方概而,你正在準(zhǔn)備中考呼巷,每天晚上需要做語(yǔ)文、數(shù)學(xué)赎瑰、英語(yǔ)王悍、物理、化學(xué)5科作業(yè)餐曼,每科作業(yè)耗時(shí)1小時(shí)油挥。
如果你先花1小時(shí)做語(yǔ)文作業(yè)覆山,做完后再花1小時(shí)做數(shù)學(xué)作業(yè)坎炼,這樣依次全部做完粉寞,一共花5小時(shí)沽讹,這種方式稱為單任務(wù)模型或批處理任務(wù)模型吗跋。
如果你打算切換到多任務(wù)模型逢艘,可以先做1分鐘語(yǔ)文蒸绩,切換到數(shù)學(xué)作業(yè)做1分鐘鳞青,再切換到英語(yǔ)霸饲,以此類推,只要切換速度足夠快臂拓,這種方式就和單核CPU執(zhí)行多任務(wù)一樣了厚脉。以幼兒園小朋友的眼光來(lái)看,你就正在同時(shí)寫5科作業(yè)胶惰。
不過(guò)切換作業(yè)是有代價(jià)的傻工,比如從語(yǔ)文切換到數(shù)學(xué),要先收拾桌子上的語(yǔ)文書(shū)本孵滞、鋼筆(保存現(xiàn)場(chǎng))中捆,然后打開(kāi)數(shù)學(xué)課本,找出圓規(guī)直尺(準(zhǔn)備新環(huán)境)坊饶,才能開(kāi)始做數(shù)學(xué)作業(yè)泄伪。操作系統(tǒng)在切換進(jìn)程或線程時(shí)也一樣,需要先保存當(dāng)前執(zhí)行的現(xiàn)場(chǎng)環(huán)境(CPU寄存器狀態(tài)匿级、內(nèi)存頁(yè)等)蟋滴,然后把新任務(wù)的執(zhí)行環(huán)境準(zhǔn)備好(恢復(fù)上次的寄存器狀態(tài)染厅,切換內(nèi)存頁(yè)等),才能開(kāi)始執(zhí)行津函。這個(gè)切換過(guò)程雖然很快肖粮,但是也需要耗費(fèi)時(shí)間。如果有幾千個(gè)任務(wù)同時(shí)進(jìn)行尔苦,操作系統(tǒng)可能主要忙著切換任務(wù)涩馆,根本沒(méi)有多少時(shí)間執(zhí)行任務(wù)。這種情況最常見(jiàn)的就是硬盤狂響蕉堰、點(diǎn)窗口無(wú)反應(yīng)凌净,這時(shí)系統(tǒng)處于假死狀態(tài)。
所以屋讶,多任務(wù)一旦多到一個(gè)限度冰寻,就會(huì)消耗系統(tǒng)所有資源,導(dǎo)致效率急劇下降皿渗,所有任務(wù)都做不好斩芭。

2 計(jì)算密集型與IO密集型

是否采用多任務(wù)的第二個(gè)考慮是任務(wù)類型。我們可以把任務(wù)分為計(jì)算密集型和IO密集型乐疆。
計(jì)算密集型任務(wù)的特點(diǎn)是要進(jìn)行大量計(jì)算划乖,消耗CPU資源,如計(jì)算圓周率挤土、對(duì)視頻進(jìn)行高清解碼等琴庵,全靠CPU的運(yùn)算能力。計(jì)算密集型任務(wù)雖然可以用多任務(wù)完成仰美,但是任務(wù)越多迷殿,花在任務(wù)切換的時(shí)間就越多,CPU執(zhí)行任務(wù)的效率就越低咖杂。要最高效地利用CPU庆寺,計(jì)算密集型任務(wù)同時(shí)進(jìn)行的數(shù)量應(yīng)當(dāng)?shù)扔贑PU的核心數(shù)。
由于計(jì)算密集型任務(wù)時(shí)主要消耗CPU資源诉字,因此代碼運(yùn)行效率至關(guān)重要懦尝。Python腳本語(yǔ)言運(yùn)行效率很低,完全不適合計(jì)算密集型任務(wù)壤圃。計(jì)算密集型任務(wù)最好用C語(yǔ)言編寫陵霉。
涉及網(wǎng)絡(luò)、磁盤IO的任務(wù)都是IO密集型任務(wù)伍绳,這類任務(wù)的特點(diǎn)是CPU消耗很少踊挠,任務(wù)的大部分時(shí)間都在等待IO操作完成(因?yàn)镮O的速度遠(yuǎn)遠(yuǎn)低于CPU和內(nèi)存的速度)。IO密集型任務(wù)的任務(wù)越多墨叛,CPU效率越高止毕,不過(guò)有一個(gè)限度模蜡。大部分任務(wù)都是IO密集型任務(wù),如Web應(yīng)用扁凛。
IO密集型任務(wù)執(zhí)行期間忍疾,99%的時(shí)間都花在IO上,花在CPU上的時(shí)間很少谨朝,因此用運(yùn)行速度極快的C語(yǔ)言替換Python這樣運(yùn)行速度極低的腳本語(yǔ)言完全無(wú)法提升運(yùn)行效率卤妒。對(duì)于IO密集型任務(wù)而言,最適合的語(yǔ)言是開(kāi)發(fā)效率高(代碼量最少)的語(yǔ)言字币,腳本語(yǔ)言是首選则披,C語(yǔ)言最差。

3 異步IO

考慮到CPU和IO之間速度差異很大洗出,一個(gè)任務(wù)在執(zhí)行的過(guò)程中大部分時(shí)間都在等待IO操作士复,單進(jìn)程單線程模型會(huì)導(dǎo)致別的任務(wù)無(wú)法并行執(zhí)行,因此需要多進(jìn)程模型或多線程模型支持多任務(wù)并發(fā)執(zhí)行翩活。
現(xiàn)在的操作系統(tǒng)對(duì)IO操作已經(jīng)做了很大改進(jìn)阱洪,最大的特點(diǎn)是支持異步IO。如果充分利用操作系統(tǒng)提供的異步IO支持菠镇,就可以用單進(jìn)程單線程模型執(zhí)行多任務(wù)冗荸,這種全新模型稱為事件驅(qū)動(dòng)模型。Nginx就是支持異步IO的Web服務(wù)器利耍,在單核CPU上采用單進(jìn)程模型就可以高效支持多任務(wù)蚌本;在多核CPU上可以運(yùn)行多個(gè)進(jìn)程(數(shù)量與CPU核心數(shù)相同),充分利用多核CPU隘梨。由于系統(tǒng)總的進(jìn)程數(shù)量十分有限程癌,因此操作系統(tǒng)調(diào)度非常高效。用異步IO編程模型實(shí)現(xiàn)多任務(wù)是主要趨勢(shì)出嘹。
對(duì)應(yīng)到Python語(yǔ)言席楚,單進(jìn)程的異步編程模型稱為協(xié)程咬崔。有了協(xié)程的支持税稼,可以基于事件驅(qū)動(dòng)編寫高效的多任務(wù)程序。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末垮斯,一起剝皮案震驚了整個(gè)濱河市郎仆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兜蠕,老刑警劉巖扰肌,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異熊杨,居然都是意外死亡曙旭,警方通過(guò)查閱死者的電腦和手機(jī)盗舰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)桂躏,“玉大人钻趋,你說(shuō)我怎么就攤上這事〖料埃” “怎么了蛮位?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)鳞绕。 經(jīng)常有香客問(wèn)我失仁,道長(zhǎng),這世上最難降的妖魔是什么们何? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任萄焦,我火速辦了婚禮,結(jié)果婚禮上冤竹,老公的妹妹穿的比我還像新娘楷扬。我一直安慰自己,他們只是感情好贴见,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開(kāi)白布烘苹。 她就那樣靜靜地躺著,像睡著了一般片部。 火紅的嫁衣襯著肌膚如雪镣衡。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,287評(píng)論 1 301
  • 那天档悠,我揣著相機(jī)與錄音廊鸥,去河邊找鬼。 笑死辖所,一個(gè)胖子當(dāng)著我的面吹牛惰说,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播缘回,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼吆视,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了酥宴?” 一聲冷哼從身側(cè)響起啦吧,我...
    開(kāi)封第一講書(shū)人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拙寡,沒(méi)想到半個(gè)月后授滓,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年般堆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了在孝。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡淮摔,死狀恐怖浑玛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情噩咪,我是刑警寧澤顾彰,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站胃碾,受9級(jí)特大地震影響涨享,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜仆百,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一厕隧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧俄周,春花似錦吁讨、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至波势,卻和暖如春翎朱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背尺铣。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工拴曲, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人凛忿。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓澈灼,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親店溢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子叁熔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354