第10天,線程、協程、IO多路復用、socketserver

目錄

一、開啟線程的兩種方式
    1.1 直接利用利用threading.Thread()類實例化
    1.2 創建一個類,并繼承Thread類
    1.3 在一個進程下開啟多個線程與在一個進程下開啟多個子進程的區別
        1.3.1 誰的開啟速度更快?
        1.3.2 看看PID的不同
        1.3.3 練習
        1.3.4 線程的join與setDaemon
        1.3.5 線程相關的其他方法補充

二、 Python GIL
    2.1 什么是全局解釋器鎖GIL
    2.2 全局解釋器鎖GIL設計理念與限制

三、 Python多進程與多線程對比
四、鎖
    4.1 同步鎖
    GIL vs Lock
    4.2 死鎖與遞歸鎖
    4.3 信號量Semaphore
    4.4 事件Event
    4.5 定時器timer
    4.6 線程隊列queue

五、協程
    5.1 yield實現協程
    5.2 greenlet實現協程
    5.3 gevent實現協程

六、IO多路復用

七、socketserver實現并發
    7.1 ThreadingTCPServer
    
八、基于UDP的套接字

一、開啟線程的兩種方式

在python中開啟線程要導入threading,它與開啟進程所需要導入的模塊multiprocessing在使用上,有很大的相似性。在接下來的使用中,就可以發現。

同開啟進程的兩種方式相同:

1.1 直接利用利用threading.Thread()類實例化

from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    t.start()
    
    print('主線程')

1.2 創建一個類,并繼承Thread類

from threading import Thread
import time
calss Sayhi(Thread):
    def __init__(self,name):
        super().__init__()
        self.name = name
    def run(self):
        time.sleep(2)
        print("%s say hello" %self.name)

if __name__ == "__main__":
    t = Sayhi("egon")
    t.start()
    print("主線程")

1.3 在一個進程下開啟多個線程與在一個進程下開啟多個子進程的區別

1.3.1 誰的開啟速度更快?

from threading import Thread
from multiprocessing import Process
import os

def work():
    print('hello')

if __name__ == '__main__':
    #在主進程下開啟線程
    t=Thread(target=work)
    t.start()
    print('主線程/主進程')
    '''
    打印結果:
    hello
    主線程/主進程
    '''

    #在主進程下開啟子進程
    t=Process(target=work)
    t.start()
    print('主線程/主進程')
    '''
    打印結果:
    主線程/主進程
    hello
    '''

結論:由于創建子進程是將主進程完全拷貝一份,而線程不需要,所以線程的創建速度更快。

1.3.2 看看PID的不同

from threading import Thread
from multiprocessing import Process
import os

def work():
    print('hello',os.getpid())

if __name__ == '__main__':
    #part1:在主進程下開啟多個線程,每個線程都跟主進程的pid一樣
    t1=Thread(target=work)
    t2=Thread(target=work)
    t1.start()
    t2.start()
    print('主線程/主進程pid',os.getpid())

    #part2:開多個進程,每個進程都有不同的pid
    p1=Process(target=work)
    p2=Process(target=work)
    p1.start()
    p2.start()
    print('主線程/主進程pid',os.getpid())


'''
hello 13552
hello 13552
主線程pid: 13552
主線程pid: 13552
hello 1608
hello 6324
'''

總結:可以看出,主進程下開啟多個線程,每個線程的PID都跟主進程的PID一樣;而開多個進程,每個進程都有不同的PID。

1.3.3 練習

練習一:利用多線程,實現socket 并發連接
服務端:

from threading import Thread
from socket import *
import os

tcpsock = socket(AF_INET,SOCK_STREAM)
tcpsock.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcpsock.bind(("127.0.0.1",60000))
tcpsock.listen(5)

def work(conn,addr):
    while True:
        try:
            data = conn.recv(1024)
            print(os.getpid(),addr,data.decode("utf-8"))
            conn.send(data.upper())
        except Exception:
            break

if __name__ == '__main__':
    while True:
        conn,addr = tcpsock.accept()
        t = Thread(target=work,args=(conn,addr))
        t.start()

"""
開啟了4個客戶端
服務器端輸出:
13800 ('127.0.0.1', 63164) asdf
13800 ('127.0.0.1', 63149) asdf
13800 ('127.0.0.1', 63154) adsf
13800 ('127.0.0.1', 63159) asdf

可以看出每個線程的PID都是一樣的。
""

客戶端:

from socket import *

tcpsock = socket(AF_INET,SOCK_STREAM)
tcpsock.connect(("127.0.0.1",60000))

while True:
    msg = input(">>: ").strip()
    if not msg:continue
    tcpsock.send(msg.encode("utf-8"))
    data = tcpsock.recv(1024)
    print(data.decode("utf-8"))

練習二:有三個任務,一個接收用戶輸入,一個將用戶輸入的內容格式化成大寫,一個將格式化后的結果存入文件。

from threading import Thread

recv_l = []
format_l = []

def Recv():
    while True:
        inp = input(">>: ").strip()
        if not inp:continue
        recv_l.append(inp)

def Format():
    while True:
        if recv_l:
            res = recv_l.pop()
            format_l.append(res.upper())

def Save(filename):
    while True:
        if format_l:
            with open(filename,"a",encoding="utf-8") as f:
                res = format_l.pop()
                f.write("%s\n" %res)

if __name__ == '__main__':
    t1 = Thread(target=Recv)
    t2 = Thread(target=Format)
    t3 = Thread(target=Save,args=("db.txt",))
    t1.start()
    t2.start()
    t3.start()

1.3.4 線程的join與setDaemon

與進程的方法都是類似的,其實multiprocessing模塊是模仿threading模塊的接口;

from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    t.setDaemon(True) #設置為守護線程,主線程結束,子線程也跟著線束。
    t.start()
    t.join()  #主線程等待子線程運行結束
    print('主線程')
    print(t.is_alive())

1.3.5 線程相關的其他方法補充

Thread實例對象的方法:

  • isAlive():返回純種是否是活躍的;
  • getName():返回線程名;
  • setName():設置線程名。

threading模塊提供的一些方法:

  • threading.currentThread():返回當前的線程變量
  • threading.enumerate():返回一個包含正在運行的線程的列表。正在運行指線程啟動后、結束前,不包括啟動前和終止后。
  • threading.activeCount():返回正在運行的線程數量,與len(threading.enumerate())有相同結果。
from threading import Thread
import threading
import os

def work():
    import time
    time.sleep(3)
    print(threading.current_thread().getName())


if __name__ == '__main__':
    #在主進程下開啟線程
    t=Thread(target=work)
    t.start()

    print(threading.current_thread().getName()) #獲取當前線程名
    print(threading.current_thread()) #主線程
    print(threading.enumerate()) #連同主線程在內有兩個運行的線程,返回的是活躍的線程列表
    print(threading.active_count())  #活躍的線程個數
    print('主線程/主進程')

    '''
    打印結果:
    MainThread
    <_MainThread(MainThread, started 140735268892672)>
    [<_MainThread(MainThread, started 140735268892672)>, <Thread(Thread-1, started 123145307557888)>]
    2
    主線程/主進程
    Thread-1
    '''

二、 Python GIL

GIL全稱Global Interpreter Lock,即全局解釋器鎖。首先需要明確的一點是GIL并不是Python的特性,它是在實現Python解析器(CPython)時所引入的一個概念。就好比C++是一套語言(語法)標準,但是可以用不同的編譯器來編譯成可執行代碼。有名的編譯器例如GCC,INTEL C++,Visual C++等。Python也一樣,同樣一段代碼可以通過CPython,PyPy,Psyco等不同的Python執行環境來執行。像其中的JPython就沒有GIL。然而因為CPython是大部分環境下默認的Python執行環境。所以在很多人的概念里CPython就是Python,也就想當然的把GIL歸結為Python語言的缺陷。所以這里要先明確一點:GIL并不是Python的特性,Python完全可以不依賴于GIL

2.1 什么是全局解釋器鎖GIL

Python代碼的執行由Python 虛擬機(也叫解釋器主循環,CPython版本)來控制,Python 在設計之初就考慮到要在解釋器的主循環中,同時只有一個線程在執行,即在任意時刻,只有一個線程在解釋器中運行。對Python 虛擬機的訪問由全局解釋器鎖(GIL)來控制,正是這個鎖能保證同一時刻只有一個線程在運行。
在多線程環境中,Python 虛擬機按以下方式執行:

  1. 設置GIL
  2. 切換到一個線程去運行
  3. 運行:
    a. 指定數量的字節碼指令,或者
    b. 線程主動讓出控制(可以調用time.sleep(0))
  4. 把線程設置為睡眠狀態
  5. 解鎖GIL
  6. 再次重復以上所有步驟

在調用外部代碼(如C/C++擴展函數)的時候,GIL 將會被鎖定,直到這個函數結束為止(由于在這期間沒有Python 的字節碼被運行,所以不會做線程切換)。

2.2 全局解釋器鎖GIL設計理念與限制

GIL的設計簡化了CPython的實現,使得對象模型,包括關鍵的內建類型如字典,都是隱含可以并發訪問的。鎖住全局解釋器使得比較容易的實現對多線程的支持,但也損失了多處理器主機的并行計算能力。
但是,不論標準的,還是第三方的擴展模塊,都被設計成在進行密集計算任務是,釋放GIL。
還有,就是在做I/O操作時,GIL總是會被釋放。對所有面向I/O 的(會調用內建的操作系統C 代碼的)程序來說,GIL 會在這個I/O 調用之前被釋放,以允許其它的線程在這個線程等待I/O 的時候運行。如果是純計算的程序,沒有 I/O 操作,解釋器會每隔 100 次操作就釋放這把鎖,讓別的線程有機會執行(這個次數可以通過 sys.setcheckinterval 來調整)如果某線程并未使用很多I/O 操作,它會在自己的時間片內一直占用處理器(和GIL)。也就是說,I/O 密集型的Python 程序比計算密集型的程序更能充分利用多線程環境的好處。

下面是Python 2.7.9手冊中對GIL的簡單介紹:
The mechanism used by the CPython interpreter to assure that only one thread executes Python bytecode at a time. This simplifies the CPython implementation by making the object model (including critical built-in types such as dict) implicitly safe against concurrent access. Locking the entire interpreter makes it easier for the interpreter to be multi-threaded, at the expense of much of the parallelism afforded by multi-processor machines.
However, some extension modules, either standard or third-party, are designed so as to release the GIL when doing computationally-intensive tasks such as compression or hashing. Also, the GIL is always released when doing I/O.
Past efforts to create a “free-threaded” interpreter (one which locks shared data at a much finer granularity) have not been successful because performance suffered in the common single-processor case. It is believed that overcoming this performance issue would make the implementation much more complicated and therefore costlier to maintain.

從上文中可以看到,針對GIL的問題做的很多改進,如使用更細粒度的鎖機制,在單處理器環境下反而導致了性能的下降。普遍認為,克服這個性能問題會導致CPython實現更加復雜,因此維護成本更加高昂。

三、 Python多進程與多線程對比

有了GIL的存在,同一時刻同一進程中只有一個線程被執行?這里也許人有一個疑問:多進程可以利用多核,但是開銷大,而Python多線程開銷小,但卻無法利用多核的優勢?要解決這個問題,我們需要在以下幾點上達成共識:

  • CPU是用來計算的!
  • 多核CPU,意味著可以有多個核并行完成計算,所以多核提升的是計算性能;
  • 每個CPU一旦遇到I/O阻塞,仍然需要等待,所以多核對I/O操作沒什么用處。

當然,對于一個程序來說,不會是純計算或者純I/O,我們只能相對的去看一個程序到底是計算密集型,還是I/O密集型。從而進一步分析Python的多線程有無用武之地。

分析:

我們有四個任務需要處理,處理訪求肯定是要有并發的效果,解決方案可以是:

  • 方案一:開啟四個進程;
  • 方案二:一個進程下,開啟四個進程。

單核情況下,分析結果:

  • 如果四個任務是計算密集型,沒有多核來并行計算,方案一徒增了創建進程的開銷,方案二勝;
  • 如果四個任務是I/O密集型,方案一創建進程的開銷大,且進程的切換速度遠不如線程,方案二勝。

多核情況下,分析結果:

  • 如果四個任務是密集型,多核意味著并行 計算,在python中一個進程中同一時刻只有一個線程執行用不上多核,方案一勝;
  • 如果四個任務是I/O密集型,再多的核 也解決不了I/O問題,方案二勝。

結論:現在的計算機基本上都是多核,python對于計算密集型的任務開多線程的效率并不能帶來多大性能上的提升,甚至 不如串行(沒有大量切換),但是,對于I/O密集型的任務效率還是有顯著提升的。

代碼實現對比

計算密集型:

#計算密集型
from threading import Thread
from multiprocessing import Process
import os
import time
def work():
    res=0
    for i in range(1000000):
        res+=i

if __name__ == '__main__':
    t_l=[]
    start_time=time.time()
    for i in range(100):
        # t=Thread(target=work) #我的機器4核cpu,多線程大概15秒
        t=Process(target=work) #我的機器4核cpu,多進程大概10秒
        t_l.append(t)
        t.start()

    for i in t_l:
        i.join()
    stop_time=time.time()
    print('run time is %s' %(stop_time-start_time))
    print('主線程')

I/O密集型:

#I/O密集型
from threading import Thread
from multiprocessing import Process
import time
import os
def work():
    time.sleep(2) #模擬I/O操作,可以打開一個文件來測試I/O,與sleep是一個效果
    print(os.getpid())

if __name__ == '__main__':
    t_l=[]
    start_time=time.time()
    for i in range(500):
        # t=Thread(target=work) #run time is 2.195
        t=Process(target=work) #耗時大概為37秒,創建進程的開銷遠高于線程,而且對于I/O密集型,多cpu根本不管用
        t_l.append(t)
        t.start()

    for t in t_l:
        t.join()
    stop_time=time.time()
    print('run time is %s' %(stop_time-start_time))

總結:
應用場景:
多線程用于I/O密集型,如socket、爬蟲、web
多進程用于計算密集型,如金融分析

四、鎖

4.1 同步鎖

需求:對一個全局變量,開啟100個線程,每個線程都對該全局變量做減1操作;

不加鎖,代碼如下:

import time
import threading

num = 100  #設定一個共享變量
def addNum():
    global num #在每個線程中都獲取這個全局變量
    #num-=1

    temp=num
    time.sleep(0.1)
    num =temp-1  # 對此公共變量進行-1操作

thread_list = []

for i in range(100):
    t = threading.Thread(target=addNum)
    t.start()
    thread_list.append(t)

for t in thread_list: #等待所有線程執行完畢
    t.join()

print('Result: ', num)

分析:以上程序開啟100線程并不能把全局變量num減為0,第一個線程執行addNum遇到I/O阻塞后迅速切換到下一個線程執行addNum,由于CPU執行切換的速度非常快,在0.1秒內就切換完成了,這就造成了第一個線程在拿到num變量后,在time.sleep(0.1)時,其他的線程也都拿到了num變量,所有線程拿到的num值都是100,所以最后減1操作后,就是99。加鎖實現。

加鎖,代碼如下:

import time
import threading

num = 100   #設定一個共享變量
def addNum():
    with lock:
        global num
        temp = num
        time.sleep(0.1)
        num = temp-1    #對此公共變量進行-1操作

thread_list = []

if __name__ == '__main__':
    lock = threading.Lock()   #由于同一個進程內的線程共享此進程的資源,所以不需要給每個線程傳這把鎖就可以直接用。
    for i in range(100):
        t = threading.Thread(target=addNum)
        t.start()
        thread_list.append(t)

    for t in thread_list:  #等待所有線程執行完畢
        t.join()

    print("result: ",num)

加鎖后,第一個線程拿到鎖后開始操作,第二個線程必須等待第一個線程操作完成后將鎖釋放后,再與其它線程競爭鎖,拿到鎖的線程才有權操作。這樣就保障了數據的安全,但是拖慢了執行速度。
注意:with locklock.acquire()(加鎖)與lock.release()(釋放鎖)的簡寫。

import threading

R=threading.Lock()

R.acquire()
'''
對公共數據的操作
'''
R.release()

GIL vs Lock

機智的同學可能會問到這個問題,就是既然你之前說過了,Python已經有一個GIL來保證同一時間只能有一個線程來執行了,為什么這里還需要lock? 

首先我們需要達成共識:鎖的目的是為了保護共享的數據,同一時間只能有一個線程來修改共享的數據

然后,我們可以得出結論:保護不同的數據就應該加不同的鎖。

最后,問題就很明朗了,GIL 與Lock是兩把鎖,保護的數據不一樣,前者是解釋器級別的(當然保護的就是解釋器級別的數據,比如垃圾回收的數據),后者是保護用戶自己開發的應用程序的數據,很明顯GIL不負責這件事,只能用戶自定義加鎖處理,即Lock

詳細的:

因為Python解釋器幫你自動定期進行內存回收,你可以理解為python解釋器里有一個獨立的線程,每過一段時間它起wake up做一次全局輪詢看看哪些內存數據是可以被清空的,此時你自己的程序 里的線程和 py解釋器自己的線程是并發運行的,假設你的線程刪除了一個變量,py解釋器的垃圾回收線程在清空這個變量的過程中的clearing時刻,可能一個其它線程正好又重新給這個還沒來及得清空的內存空間賦值了,結果就有可能新賦值的數據被刪除了,為了解決類似的問題,python解釋器簡單粗暴的加了鎖,即當一個線程運行時,其它人都不能動,這樣就解決了上述的問題, 這可以說是Python早期版本的遺留問題。

4.2 死鎖與遞歸鎖

所謂死鎖:是指兩個或兩個以上的進程或線程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處于死鎖狀態,或系統產生了死鎖。這此永遠在互相等待的進程稱死鎖進程

如下代碼,就會產生死鎖:

from threading import Thread,Lock
import time
mutexA=Lock()
mutexB=Lock()

class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutexA.acquire()
        print('\033[41m%s 拿到A鎖\033[0m' %self.name)

        mutexB.acquire()
        print('\033[42m%s 拿到B鎖\033[0m' %self.name)
        mutexB.release()

        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('\033[43m%s 拿到B鎖\033[0m' %self.name)
        time.sleep(2)

        mutexA.acquire()
        print('\033[44m%s 拿到A鎖\033[0m' %self.name)
        mutexA.release()

        mutexB.release()

if __name__ == '__main__':
    for i in range(10):
        t=MyThread()
        t.start()

'''
Thread-1 拿到A鎖
Thread-1 拿到B鎖
Thread-1 拿到B鎖
Thread-2 拿到A鎖
然后就卡住,死鎖了
'''

解決死鎖的方法

避免產生死鎖的方法就是用遞歸鎖,在python中為了支持在同一線程中多次請求同一資源,python提供了可重入鎖RLock

這個RLock內部維護著一個Lock和一個counter變量,counter記錄了acquire(獲得鎖)的次數,從而使得資源可以被多次require。直到一個線程所有的acquire都被release(釋放)后,其他的線程才能獲得資源。上面的例子如果使用RLock代替Lock,就不會發生死鎖的現象了。

mutexA=mutexB=threading.RLock() #一個線程拿到鎖,counter加1,該線程內又碰到加鎖的情況,則counter繼續加1,這期間所有其他線程都只能等待,等待該線程釋放所有鎖,即counter遞減到0為止。

4.3 信號量Semaphore

同進程的信號量一樣。
用一個粗俗的例子來說,鎖相當于獨立衛生間,只有一個坑,同一時刻只能有一個人獲取鎖,進去使用;而信號量相當于公共衛生間,例如有5個坑,同一時刻可以有5個人獲取鎖,并使用。

Semaphore管理一個內置的計數器,每當調用acquire()時,內置計數器-1;調用release()時,內置計數器+1;計數器不能小于0,當計數器為0時,acquire()將阻塞線程,直到其他線程調用release()

實例:
同時只有5個線程可以獲得Semaphore,即可以限制最大連接數為5:

import threading
import time

sem = threading.Semaphore(5)
def func():
    if sem.acquire():   #也可以用with進行上下文管理
        print(threading.current_thread().getName()+"get semaphore")
        time.sleep(2)
        sem.release()

for i in range(20):
    t1 = threading.Thread(target=func)
    t1.start()

利用with進行上下文管理:

import threading
import time

sem = threading.Semaphore(5)

def func():
    with sem:   
        print(threading.current_thread().getName()+"get semaphore")
        time.sleep(2)

for i in range(20):
    t1 = threading.Thread(target=func)
    t1.start()

注:信號量與進程池是完全不同一的概念,進程池Pool(4)最大只能產生4個進程,而且從頭到尾都只是這4個進程,不會產生新的,而信號量是產生一堆線程/進程。

4.4 事件Event

同進程的一樣

線程的一個關鍵特性是每個線程都是獨立運行且狀態不可預測。如果程序中的其他線程通過判斷某個線程的狀態來確定自己下一步的操作,這時線程同步問題就會變得非常棘手,為了解決這些問題我們使用threading庫中的Event對象。

Event對象包含一個可由線程設置的信號標志,它允許線程等待某些事件的發生。在初始情況下,Event對象中的信號標志被設置為假。如果有線程等待一個Event對象,而這個Event對象的標志為假,那么這個線程將會被 一直阻塞直至該 標志為真。一個線程如果將一個Event對象的信號標志設置為真,它將喚醒所有等待這個Event對象的線程。如果一個線程等待一個已經被 設置 為真的Event對象,那么它將忽略這個事件,繼續執行。

Event對象具有一些方法:
event = threading.Event() #產生一個事件對象

  • event.isSet():返回event狀態值;
  • event.wait():如果event.isSet() == False,將阻塞線程;
  • event.set():設置event的狀態值為True,所有阻塞池的線程進入就緒狀態,等待操作系統高度;
  • event.clear():恢復event的狀態值False。

應用場景:

例如,我們有多個線程需要連接數據庫,我們想要在啟動時確保Mysql服務正常,才讓那些工作線程去連接Mysql服務器,那么我們就可以采用threading.Event()機制來協調各個工作線程的連接操作,主線程中會去嘗試連接Mysql服務,如果正常的話,觸發事件,各工作線程會嘗試連接Mysql服務。

from threading import Thread,Event
import threading
import time,random
def conn_mysql():
    print('\033[42m%s 等待連接mysql。。。\033[0m' %threading.current_thread().getName())
    event.wait()  #默認event狀態為False,等待
    print('\033[42mMysql初始化成功,%s開始連接。。。\033[0m' %threading.current_thread().getName())


def check_mysql():
    print('\033[41m正在檢查mysql。。。\033[0m')
    time.sleep(random.randint(1,3))
    event.set()   #設置event狀態為True
    time.sleep(random.randint(1,3))

if __name__ == '__main__':
    event=Event()
    t1=Thread(target=conn_mysql) #等待連接mysql
    t2=Thread(target=conn_mysql) #等待連接myqsl
    t3=Thread(target=check_mysql) #檢查mysql

    t1.start()
    t2.start()
    t3.start()


'''
輸出如下:
Thread-1 等待連接mysql。。。
Thread-2 等待連接mysql。。。
正在檢查mysql。。。
Mysql初始化成功,Thread-1開始連接。。。
Mysql初始化成功,Thread-2開始連接。。。
'''

注:threading.Eventwait方法還可以接受一個超時參數,默認情況下,如果事件一直沒有發生,wait方法會一直阻塞下去,而加入這個超時參數之后,如果阻塞時間超過這個參數設定的值之后,wait方法會返回。對應于上面的應用場景,如果mysql服務器一直沒有啟動,我們希望子線程能夠打印一些日志來不斷提醒我們當前沒有一個可以連接的mysql服務,我們就可以設置這個超時參數來達成這樣的目的:

上例代碼修改后如下:

from threading import Thread,Event
import threading
import time,random
def conn_mysql():
    count = 1
    while not event.is_set():
        print("\033[42m%s 第 <%s> 次嘗試連接。。。"%(threading.current_thread().getName(),count))
        event.wait(0.2)
        count+=1
    print("\033[45mMysql初始化成功,%s 開始連接。。。\033[0m"%(threading.current_thread().getName()))

def check_mysql():
    print('\033[41m正在檢查mysql。。。\033[0m')
    time.sleep(random.randint(1,3))
    event.set()
    time.sleep(random.randint(1,3))

if __name__ == '__main__':
    event=Event()
    t1=Thread(target=conn_mysql) #等待連接mysql
    t2=Thread(target=conn_mysql) #等待連接mysql
    t3=Thread(target=check_mysql) #檢查mysql

    t1.start()
    t2.start()
    t3.start()

這樣,我們就可以在等待Mysql服務啟動的同時,看到工作線程里正在等待的情況。應用:連接池。

4.5 定時器timer

定時器,指定n秒后執行某操作。

from threading import Timer
 
def hello():
    print("hello, world")
 
t = Timer(1, hello)  #1秒后執行任務hello
t.start()   # after 1 seconds, "hello, world" will be printed

4.6 線程隊列queue

queue隊列:使用import queue,用法與進程Queue一樣。

queue下有三種隊列:

  • queue.Queue(maxsize) 先進先出,先放進隊列的數據,先被取出來;
  • queue.LifoQueue(maxsize) 后進先出,(Lifo 意為last in first out),后放進隊列的數據,先被取出來
  • queue.PriorityQueue(maxsize) 優先級隊列,優先級越高優先取出來。

舉例:
先進先出:

import queue

q=queue.Queue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
'''
結果(先進先出):
first
second
third
'''

后進先出:

import queue

q=queue.LifoQueue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
'''
結果(后進先出):
third
second
first
'''

優先級隊列:

import queue

q=queue.PriorityQueue()
#put進入一個元組,元組的第一個元素是優先級(通常是數字,也可以是非數字之間的比較),數字越小優先級越高
q.put((20,'a'))
q.put((10,'b'))
q.put((30,'c'))

print(q.get())
print(q.get())
print(q.get())
'''
結果(數字越小優先級越高,優先級高的優先出隊):
(10, 'b')
(20, 'a')
(30, 'c')
'''

五、協程

協程:是單線程下的并發,又稱微線程、纖程,英文名:Coroutine協程是一種用戶態的輕量級線程,協程是由用戶程序自己控制調度的。

需要強調的是:

1. python的線程屬于內核級別的,即由操作系統控制調度(如單線程一旦遇到io就被迫交出cpu執行權限,切換其他線程運行)

2. 單線程內開啟協程,一旦遇到io,從應用程序級別(而非操作系統)控制切換

對比操作系統控制線程的切換,用戶在單線程內控制協程的切換,優點如下:

1. 協程的切換開銷更小,屬于程序級別的切換,操作系統完全感知不到,因而更加輕量級

2. 單線程內就可以實現并發的效果,最大限度地利用cpu。

要實現協程,關鍵在于用戶程序自己控制程序切換,切換之前必須由用戶程序自己保存協程上一次調用時的狀態,如此,每次重新調用時,能夠從上次的位置繼續執行

(詳細的:協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧)

5.1 yield實現協程

我們之前已經學習過一種在單線程下可以保存程序運行狀態的方法,即yield,我們來簡單復習一下:

  • yiled可以保存狀態,yield的狀態保存與操作系統的保存線程狀態很像,但是yield是代碼級別控制的,更輕量級
  • send可以把一個函數的結果傳給另外一個函數,以此實現單線程內程序之間的切換 。
#不用yield:每次函數調用,都需要重復開辟內存空間,即重復創建名稱空間,因而開銷很大
import time
def consumer(item):
    # print('拿到包子%s' %item)
    x=11111111111
    x1=12111111111
    x3=13111111111
    x4=14111111111
    y=22222222222
    z=33333333333

    pass
def producer(target,seq):
    for item in seq:
        target(item) #每次調用函數,會臨時產生名稱空間,調用結束則釋放,循環100000000次,則重復這么多次的創建和釋放,開銷非常大

start_time=time.time()
producer(consumer,range(100000000))
stop_time=time.time()
print('run time is:%s' %(stop_time-start_time)) #30.132838010787964


#使用yield:無需重復開辟內存空間,即重復創建名稱空間,因而開銷小
import time
def init(func):
    def wrapper(*args,**kwargs):
        g=func(*args,**kwargs)
        next(g)
        return g
    return wrapper

init
def consumer():
    x=11111111111
    x1=12111111111
    x3=13111111111
    x4=14111111111
    y=22222222222
    z=33333333333
    while True:
        item=yield
        # print('拿到包子%s' %item)
        pass
def producer(target,seq):
    for item in seq:
        target.send(item) #無需重新創建名稱空間,從上一次暫停的位置繼續,相比上例,開銷小

start_time=time.time()
producer(consumer(),range(100000000))
stop_time=time.time()
print('run time is:%s' %(stop_time-start_time)) #21.882073879241943

缺點:
協程的本質是單線程下,無法利用多核,可以是一個程序開啟多個進程,每個進程內開啟多個線程,每個線程內開啟協程。
協程指的是單個線程,因而一旦協程出現阻塞,將會阻塞整個線程。

協程的定義(滿足1,2,3就可以稱為協程):

  1. 必須在只有一個單線程里實現并發
  2. 修改共享數據不需加鎖
  3. 用戶程序里自己保存多個控制流的上下文棧
  4. 附加:一個協程遇到IO操作自動切換到其它協程(如何實現檢測IO,yield、greenlet都無法實現,就用到了gevent模塊(select機制))

注意:yield切換在沒有io的情況下或者沒有重復開辟內存空間的操作,對效率沒有什么提升,甚至更慢,為此,可以用greenlet來為大家演示這種切換。

5.2 greenlet實現協程

greenlet是一個用C實現的協程模塊,相比與python自帶的yield,它可以使你在任意函數之間隨意切換,而不需把這個函數先聲明為generator。

安裝greenlet模塊
pip install greenlet

from greenlet import greenlet
import time

def t1():
    print("test1,first")
    gr2.switch()
    time.sleep(5)
    print("test1,second")
    gr2.switch()

def t2():
    print("test2,first")
    gr1.switch()
    print("test2,second")

gr1 = greenlet(t1)
gr2 = greenlet(t2)
gr1.switch()


'''
輸出結果:
test1,first
test2,first   #等待5秒
test1,second
test2,second
'''

可以在第一次switch時傳入參數

from greenlet import greenlet
import time
def eat(name):
    print("%s eat food 1"%name)
    gr2.switch(name="alex")
    time.sleep(5)
    print("%s eat food 2"%name)
    gr2.switch()

def play_phone(name):
    print("%s play phone 1"%name)
    gr1.switch()
    print("%s play phone 1" % name)

gr1 = greenlet(eat)
gr2 = greenlet(play_phone)
gr1.switch(name="egon")  #可以在第一次switch時傳入參數,以后都不需要

注意:greenlet只是提供了一種比generator更加便捷的切換方式,仍然沒有解決遇到I/O自動切換的問題,而單純的切換,反而會降低程序的執行速度。這就需要用到gevent模塊了。

5.3 gevent實現協程

gevent是一個第三方庫,可以輕松通過gevent實現并發同步或異步編程,在gevent中用到的主要是Greenlet,它是以C擴展模塊形式接入Python的輕量級協程。greenlet全部運行在主程操作系統進程的內部,但它們被協作式地調試。遇到I/O阻塞時會自動切換任務。

注意:gevent有自己的I/O阻塞,如:gevent.sleep()和gevent.socket();但是gevent不能直接識別除自身之外的I/O阻塞,如:time.sleep(2),socket等,要想識別這些I/O阻塞,必須打一個補丁:from gevent import monkey;monkey.patch_all()

  • 需要先安裝gevent模塊
    pip install gevent

  • 創建一個協程對象g1
    g1 =gevent.spawn()
    spawn括號內第一個參數是函數名,如eat,后面可以有多個參數,可以是位置實參或關鍵字實參,都是傳給第一個參數(函數)eat的。

from gevent import monkey;monkey.patch_all()
import gevent

def eat():
    print("點菜。。。")
    gevent.sleep(3)   #等待上菜
    print("吃菜。。。")

def play():
    print("玩手機。。。")
    gevent.sleep(5)  #網卡了
    print("看NBA...")

# gevent.spawn(eat)
# gevent.spawn(play)
# print('主') # 直接結束

#因而也需要join方法,進程或現場的jion方法只能join一個,而gevent的joinall方法可以join多個
g1=gevent.spawn(eat)
g2=gevent.spawn(play)
gevent.joinall([g1,g2])  #傳一個gevent對象列表。
print("主線程")

"""
輸出結果:
點菜。。。
玩手機。。。    
##等待大概3秒       此行沒打印
吃菜。。。
##等待大概2秒          此行沒打印
看NBA...
主線程
"""

注:上例中的gevent.sleep(3)是模擬的I/O阻塞。跟time.sleep(3)功能一樣。

同步/異步

import gevent
def task(pid):
    """
    Some non-deterministic task
    """
    gevent.sleep(0.5)
    print('Task %s done' % pid)

def synchronous():  #同步執行
    for i in range(1, 10):
        task(i)

def asynchronous(): #異步執行
    threads = [gevent.spawn(task, i) for i in range(10)]
    gevent.joinall(threads)

print('Synchronous:')
synchronous()   #執行后,會順序打印結果

print('Asynchronous:')
asynchronous()  #執行后,會異步同時打印結果,無序的。

爬蟲應用

#協程的爬蟲應用

from gevent import monkey;monkey.patch_all()
import gevent
import time
import requests

def get_page(url):
    print("GET: %s"%url)
    res = requests.get(url)
    if res.status_code == 200:
        print("%d bytes received from %s"%(len(res.text),url))

start_time = time.time()
g1 = gevent.spawn(get_page,"https://www.python.org")
g2 = gevent.spawn(get_page,"https://www.yahoo.com")
g3 = gevent.spawn(get_page,"https://www.github.com")
gevent.joinall([g1,g2,g3])
stop_time = time.time()
print("run time is %s"%(stop_time-start_time))

上以代碼輸出結果:

GET: https://www.python.org
GET: https://www.yahoo.com
GET: https://www.github.com
47714 bytes received from https://www.python.org
472773 bytes received from https://www.yahoo.com
98677 bytes received from https://www.github.com
run time is 2.501142978668213

應用:
通過gevent實現單線程下的socket并發,注意:from gevent import monkey;monkey.patch_all()一定要放到導入socket模塊之前,否則gevent無法識別socket的阻塞。

服務端代碼:

from gevent import monkey;monkey.patch_all()
import gevent
from socket import *

class server:
    def __init__(self,ip,port):
        self.ip = ip
        self.port = port


    def conn_cycle(self):   #連接循環
        tcpsock = socket(AF_INET,SOCK_STREAM)
        tcpsock.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
        tcpsock.bind((self.ip,self.port))
        tcpsock.listen(5)
        while True:
            conn,addr = tcpsock.accept()
            gevent.spawn(self.comm_cycle,conn,addr)

    def comm_cycle(self,conn,addr):   #通信循環
        try:
            while True:
                data = conn.recv(1024)
                if not data:break
                print(addr)
                print(data.decode("utf-8"))
                conn.send(data.upper())
        except Exception as e:
            print(e)
        finally:
            conn.close()

s1 = server("127.0.0.1",60000)
print(s1)
s1.conn_cycle()

客戶端代碼 :

from socket import *

tcpsock = socket(AF_INET,SOCK_STREAM)
tcpsock.connect(("127.0.0.1",60000))

while True:
    msg = input(">>: ").strip()
    if not msg:continue
    tcpsock.send(msg.encode("utf-8"))
    data = tcpsock.recv(1024)
    print(data.decode("utf-8"))

通過gevent實現并發多個socket客戶端去連接服務端

from gevent import monkey;monkey.patch_all()
import gevent
from socket import *

def client(server_ip,port):
    try:
        c = socket(AF_INET,SOCK_STREAM)
        c.connect((server_ip,port))
        count = 0
        while True:
            c.send(("say hello %s"%count).encode("utf-8"))
            msg = c.recv(1024)
            print(msg.decode("utf-8"))
            count+=1
    except Exception as e:
        print(e)
    finally:
        c.close()

# g_l = []
# for i in range(500):
#     g = gevent.spawn(client,'127.0.0.1',60000)
#     g_l.append(g)
# gevent.joinall(g_l)

#上面注釋代碼可簡寫為下面代碼這樣。

threads = [gevent.spawn(client,"127.0.0.1",60000) for i in range(500)]
gevent.joinall(threads)

六、IO多路復用

通過IO多路復用實現同時監聽多個端口的服務端

示例一:

# 示例一:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author : Cai Guangyin

from socket import socket
import select

sock_1 = socket()
sock_1.bind(("127.0.0.1",60000))
sock_1.listen(5)

sock_2 = socket()
sock_2.bind(("127.0.0.1",60001))
sock_2.listen(5)

inputs = [sock_1,sock_2]

while True:
    # IO多路復用
    # -- select方法,內部進行循環操作,哪個socket對象有變化(連接),就賦值給r;監聽socket文件句柄有個數限制(1024個)
    # -- poll方法,也是內部進行循環操作,沒有監聽個數限制
    # -- epoll方法,通過異步回調,哪個socket文件句柄有變化,就會自動告訴epoll,它有變化,然后將它賦值給r;
    # windows下沒有epoll方法,只有Unix下有,windows下只有select方法
    r,w,e=select.select(inputs,[],[],0.2)  #0.2是超時時間
        #當有人連接sock_1時,返回的r,就是[sock_1,];是個列表
        #當有人連接sock_2時,返回的r,就是[sock_2,];是個列表
        #當有多人同時連接sock_1和sock_2時,返回的r,就是[sock_1,sock_2,];是個列表
        #0.2是超時時間,如果這段時間內沒有連接進來,那么r就等于一個空列表;
    for obj in r:
        if obj in [sock_1,sock_2]:

            conn, addr = obj.accept()
            inputs.append(conn)
            print("新連接來了:",obj)

        else:
            print("有連接用戶發送消息來了:",obj)
            data = obj.recv(1024)
            if not data:break
            obj.sendall(data)

客戶端:

# -*- coding:utf-8 -*-
#!/usr/bin/python
# Author : Cai Guangyin

from socket import *

tcpsock = socket(AF_INET,SOCK_STREAM)   #創建一個tcp套接字
tcpsock.connect(("127.0.0.1",60001))     #根據地址連接服務器

while True:   #客戶端通信循環
    msg = input(">>: ").strip()   #輸入消息
    if not msg:continue           #判斷輸入是否為空
        #如果客戶端發空,會卡住,加此判斷,限制用戶不能發空
    if msg == 'exit':break       #退出
    tcpsock.send(msg.encode("utf-8"))   #socket只能發送二進制數據
    data = tcpsock.recv(1024)    #接收消息
    print(data.decode("utf-8"))

tcpsock.close()

以上服務端運行時,如果有客戶端斷開連接則會拋出如下異常:

異常

改進版如下

收集異常并將接收數據和發送數據分開處理
示例二:

# 示例二
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author : Cai Guangyin

from socket import *
import select

sk1 = socket(AF_INET,SOCK_STREAM)
sk1.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
sk1.bind(("127.0.0.1",60000))
sk1.listen(5)

sk2 = socket(AF_INET,SOCK_STREAM)
sk2.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
sk2.bind(("127.0.0.1",60001))
sk2.listen(5)


inputs = [sk1,sk2]
w_inputs = []

while True:
    r,w,e = select.select(inputs,w_inputs,inputs,0.1)
    for obj in r:
        if obj in [sk1,sk2]:
            print("新連接:",obj.getsockname())
            conn,addr = obj.accept()
            inputs.append(conn)

        else:
            try:
                # 如果客戶端斷開連接,將獲取異常,并將收取數據data置為空
                data = obj.recv(1024).decode('utf-8')
                print(data)
            except Exception as e:
                data = ""

            if data:
                # 如果obj能正常接收數據,則認為它是一個可寫的對象,然后將它加入w_inputs列表
                w_inputs.append(obj)
            else:
                # 如果數據data為空,則從inputs列表中移除此連接對象obj
                print("空消息")
                obj.close()
                inputs.remove(obj)


        print("分割線".center(60,"-"))

    # 遍歷可寫的對象列表,
    for obj in w:
        obj.send(b'ok')
        # 發送數據后刪除w_inputs中的此obj對象,否則客戶端斷開連接時,會拋出”ConnectionResetError“異常
        w_inputs.remove(obj)

七、socketserver實現并發

基于TCP的套接字,關鍵就是兩個循環,一個連接循環,一個通信循環。

SocketServer內部使用 IO多路復用 以及 “多線程” 和 “多進程” ,從而實現并發處理多個客戶端請求的Socket服務端。即:每個客戶端請求連接到服務器時,Socket服務端都會在服務器是創建一個“線程”或者“進程” 專門負責處理當前客戶端的所有請求。

socketserver模塊中的類分為兩大類:server類(解決鏈接問題)和request類(解決通信問題)

server類:

server類

request類:

request類

線程server類的繼承關系:

線程server類的繼承關系

進程server類的繼承關系:

進程server類的繼承關系

request類的繼承關系:

request類的繼承關系

以下述代碼為例,分析socketserver源碼:

ftpserver=socketserver.ThreadingTCPServer(('127.0.0.1',8080),FtpServer)
ftpserver.serve_forever()

查找屬性的順序:ThreadingTCPServer --> ThreadingMixIn --> TCPServer->BaseServer

  1. 實例化得到ftpserver,先找類ThreadingTCPServer__init__,在TCPServer中找到,進而執行server_bind,server_active
  2. ftpserver下的serve_forever,在BaseServer中找到,進而執行self._handle_request_noblock(),該方法同樣是在BaseServer
  3. 執行self._handle_request_noblock()進而執行request, client_address = self.get_request()(就是TCPServer中的self.socket.accept()),然后執行self.process_request(request, client_address)
  4. ThreadingMixIn中找到process_request,開啟多線程應對并發,進而執行process_request_thread,執行self.finish_request(request, client_address)
  5. 上述四部分完成了鏈接循環,本部分開始進入處理通訊部分,在BaseServer中找到finish_request,觸發我們自己定義的類的實例化,去找__init__方法,而我們自己定義的類沒有該方法,則去它的父類也就是BaseRequestHandler中找....

源碼分析總結:
基于tcp的socketserver我們自己定義的類中的

  • self.server 即套接字對象
  • self.request 即一個鏈接
  • self.client_address 即客戶端地址

基于udp的socketserver我們自己定義的類中的

  • self.request是一個元組(第一個元素是客戶端發來的數據,第二部分是服務端的udp套接字對象),如(b'adsf', <socket.socket fd=200, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8080)>)
  • self.client_address即客戶端地址。

6.1 ThreadingTCPServer

ThreadingTCPServer實現的Soket服務器內部會為每個client創建一個 “線程”,該線程用來和客戶端進行交互。

使用ThreadingTCPServer:

  • 創建一個繼承自 SocketServer.BaseRequestHandler 的類
  • 類中必須定義一個名稱為 handle 的方法
  • 啟動ThreadingTCPServer。
  • 啟動serve_forever() 鏈接循環

服務端:

import socketserver

class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        conn = self.request
        # print(addr)
        conn.sendall("歡迎致電10086,請輸入1XXX,0轉人工服務。".encode("utf-8"))
        Flag = True
        while Flag:
            data = conn.recv(1024).decode("utf-8")
            if data == "exit":
                Flag = False
            elif data == '0':
                conn.sendall("您的通話可能會被錄音。。。".encode("utf-8"))
            else:
                conn.sendall("請重新輸入。".encode('utf-8'))

if __name__ == '__main__':
    server = socketserver.ThreadingTCPServer(("127.0.0.1",60000),MyServer)
    server.serve_forever()  #內部實現while循環監聽是否有客戶端請求到達。

客戶端:

import socket

ip_port = ('127.0.0.1',60000)
sk = socket.socket()
sk.connect(ip_port)
sk.settimeout(5)

while True:
    data = sk.recv(1024).decode("utf-8")
    print('receive:',data)
    inp = input('please input:')
    sk.sendall(inp.encode('utf-8'))
    if inp == 'exit':
        break
sk.close()

七、基于UDP的套接字

  • recvfrom(buffersize[, flags])接收消息,buffersize是一次接收多少個字節的數據。
  • sendto(data[, flags], address) 發送消息,data是要發送的二進制數據,address是要發送的地址,元組形式,包含IP和端口

服務端:

from socket import *
s=socket(AF_INET,SOCK_DGRAM)  #創建一個基于UDP的服務端套接字,注意使用SOCK_DGRAM類型
s.bind(('127.0.0.1',8080))  #綁定地址和端口,元組形式

while True:    #通信循環
    client_msg,client_addr=s.recvfrom(1024) #接收消息
    print(client_msg)
    s.sendto(client_msg.upper(),client_addr) #發送消息

客戶端:

from socket import *
c=socket(AF_INET,SOCK_DGRAM)   #創建客戶端套接字

while True:
    msg=input('>>: ').strip()
    c.sendto(msg.encode('utf-8'),('127.0.0.1',8080)) #發送消息
    server_msg,server_addr=c.recvfrom(1024) #接收消息
    print('from server:%s msg:%s' %(server_addr,server_msg))

模擬即時聊天
由于UDP無連接,所以可以同時多個客戶端去跟服務端通信

服務端:

from socket import *

server_address = ("127.0.0.1",60000)
udp_server_sock = socket(AF_INET,SOCK_DGRAM)
udp_server_sock.bind(server_address)

while True:
    qq_msg,addr = udp_server_sock.recvfrom(1024)
    print("來自[%s:%s]的一條消息:\033[32m%s\033[0m"%(addr[0],addr[1],qq_msg.decode("utf-8")))
    back_msg = input("回復消息:").strip()
    udp_server_sock.sendto(back_msg.encode("utf-8"),addr)

udp_server_sock.close()

客戶端:

from socket import *

BUFSIZE = 1024
udp_client_sock = socket(AF_INET,SOCK_DGRAM)
qq_name_dic = {
    "alex":("127.0.0.1",60000),
    "egon":("127.0.0.1",60000),
    "seven":("127.0.0.1",60000),
    "yuan":("127.0.0.1",60000),
}

while True:
    qq_name = input("請選擇聊天對象:").strip()
    while True:
        msg = input("請輸入消息,回車發送:").strip()
        if msg == "quit":break
        if not msg or not qq_name or qq_name not in qq_name_dic:continue
        print(msg,qq_name_dic[qq_name])
        udp_client_sock.sendto(msg.encode("utf-8"),qq_name_dic[qq_name])

        back_msg,addr = udp_client_sock.recvfrom(BUFSIZE)
        print("來自[%s:%s]的一條消息:\033[32m%s\033[0m" %(addr[0],addr[1],back_msg.decode("utf-8")))
udp_client_sock.close()

注意:
1.你單獨運行上面的udp的客戶端,你發現并不會報錯,相反tcp卻會報錯,因為udp協議只負責把包發出去,對方收不收,我根本不管,而tcp是基于鏈接的,必須有一個服務端先運行著,客戶端去跟服務端建立鏈接然后依托于鏈接才能傳遞消息,任何一方試圖把鏈接摧毀都會導致對方程序的崩潰。

2.上面的udp程序,你注釋任何一條客戶端的sendinto,服務端都會卡住,為什么?因為服務端有幾個recvfrom就要對應幾個sendinto,哪怕是sendinto(b'')那也要有。

3.recvfrom(buffersize)如果設置每次接收數據的字節數,小于對方發送的數據字節數,如果運行Linux環境下,則只會接收到recvfrom()所設置的字節數的數據;而如果運行windows環境下,則會報錯。

基于socketserver實現多線程的UDP服務端:

import socketserver

class MyUDPhandler(socketserver.BaseRequestHandler):
    def handle(self):
        client_msg,s=self.request
        s.sendto(client_msg.upper(),self.client_address)

if __name__ == '__main__':
    s=socketserver.ThreadingUDPServer(('127.0.0.1',60000),MyUDPhandler)
    s.serve_forever()
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容