線程與進程


一、線程介紹

線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位,一條線程指的是進程中一個單一順序的控制流,一個進程中可以並發多個線程,每條線程並行執行不同的任務。

在同一個進程內的線程的數據是可以進行互相訪問的。

線程的切換使用過上下文來實現的,比如有一本書,有a和b這兩個人(兩個線程)看,a看完之后記錄當前看到那一頁哪一行,然后交給b看,b看完之后記錄當前看到了那一頁哪一行,此時a又要看了,那么a就通過上次記錄的值(上下文)直接找到上次看到了哪里,然后繼續往下看。

線程中的5種狀態:

 

各狀態說明:

 1.新建狀態(New): 
        使用threading.threading創建實例時候,線程還沒有開始運行,此時線程處在新建狀態。 當一個線程處於新生狀態時,程序還沒有開始運行線程中的代碼。

 2.就緒狀態(Runnable)

        一個新創建的線程並不自動開始運行,要執行線程,必須調用線程的start()方法。當線程對象調用start()方法即啟動了線程,start()方法創建線程運行的系統資源,並調度線程運行run()方法。當start()方法返回后,線程就處於就緒狀態。

        處於就緒狀態的線程並不一定立即運行run()方法,線程還必須同其他線程競爭CPU時間,只有獲得CPU時間才可以運行線程。因為在單CPU的計算機系統中,不可能同時運行多個線程,一個時刻僅有一個線程處於運行狀態。因此此時可能有多個線程處於就緒狀態。

 3.運行狀態(Running)

        當線程獲得CPU時間后,它才進入運行狀態,真正開始執行run()方法.

 4. 阻塞狀態(Blocked)

        線程運行過程中,可能由於各種原因進入阻塞狀態:
        1>線程通過調用sleep方法進入睡眠狀態;
        2>線程調用一個在I/O上被阻塞的操作,即該操作在輸入輸出操作完成之前不會返回到它的調用者;
        3>線程試圖得到一個鎖,而該鎖正被其他線程持有;
        4>線程在等待某個觸發條件;
        ......           

        所謂阻塞狀態是正在運行的線程沒有運行結束,暫時讓出CPU,這時其他處於就緒狀態的線程就可以獲得CPU時間,進入運行狀態。

  5. 死亡狀態(Dead)

        有兩個原因會導致線程死亡:
        1) run方法正常退出而自然死亡,
        2) 一個未捕獲的異常終止了run方法而使線程猝死。
       為了確定線程在當前是否存活着(就是要么是可運行的,要么是被阻塞了),需要使用isAlive方法。如果是可運行或被阻塞,這個方法返回true,如果線程仍舊是new狀態且不是可運行的, 或者線程死亡了,則返回false.

 

python中的多線程:

Python通過兩個標准庫thread和threading提供對線程的支持。thread提供了低級別的、原始的線程以及一個簡單的鎖,threading則彌補了其缺陷,所以線程模塊使用threading就可以了。

多線程在Python內實則就是一個假象,為什么這么說呢,因為CPU的處理速度是很快的,所以我們看起來以一個線程在執行多個任務,每個任務的執行速度是非常之快的,利用上下文切換來快速的切換任務,以至於我們根本感覺不到。

但是頻繁的使用上下文切換也是要耗費一定的資源,因為單線程在每次切換任務的時候需要保存當前任務的上下文。

什么時候用到多線程?

首先IO操作是不占用CPU的,只有計算的時候才會占用CPU(譬如1+1=2),Python中的多線程不適合CPU密集型的任務,適合IO密集型的任務(sockt server).

IO密集型(I/O bound):頻繁網絡傳輸、讀取硬盤及其他IO設備稱之為IO密集型,最簡單的就是硬盤存取數據,IO操作並不會涉及到CPU。

計算密集型(CPU bound):程序大部分在做計算、邏輯判斷、循環導致cpu占用率很高的情況,稱之為計算密集型,比如說python程序中執行了一段代碼1+1,這就是在計算1+1的值

 

線程創建方法:

1.普通方法

#!/usr/bin/env python3
#_*_ coding:utf-8 _*_
#Author:wd
import threading


def task(n):
    print('run task ',n)

for i in range(50):#啟動50個線程
    t=threading.Thread(target=task,args=(i,))#args參數是一個tuple
    t.start()#啟動線程

 2.類繼承方法

#!/usr/bin/env python3
#_*_ coding:utf-8 _*_
#Author:wd
import threading
class Mythreading(threading.Thread):
    def __init__(self,fun,args):
        self._fun=fun
        self._agrs=args
        super(Mythreading,self).__init__()


    def run(self):#啟動線程時候會允許run方法,這里重寫父類run方法,可以自定義需要運行的task
        print('start running ....')
        self._fun(self._agrs)


def func(n):
    print(n)

t=Mythreading(func,1)
t.start()#啟動運行線程
結果:
start running ....
1

 threading模塊提供方法:

  • start            線程准備就緒,等待CPU調度
  • setName      為線程設置名稱
  • getName      獲取線程名稱
  • setDaemon   設置為守護線程(在start之前),默認為前台線程,設置為守護線程以后,如果主線程退出,守護線程無論執行完畢都會退出
  • join              等待線程執行結果,逐個執行每個線程,執行完畢后繼續往下執行,該方法使得多線程變得無意義
  • run              線程被cpu調度后自動執行線程對象的run方法
  • isAlive      判斷線程是否活躍
  • threading.active_count 返回當前活躍的線程數量
  • threading.current_thread 獲取當前線程對象

使用list主線程阻塞,子現在並行執行demo:

#!/usr/bin/env python3
#_*_ coding:utf-8 _*_
#Author:wd
import threading
import time
def fun(n):
    print('start running',n)
    time.sleep(2)
    print('end runing ',n)
thread_list=[]
for i in range(10):
    t=threading.Thread(target=fun,args=(i,))
    t.start()
    thread_list.append(t)

for r in thread_list:#循環每個線程,等待其結果,好處是,這樣做所有線程啟動之后一起join
    r.join()

這樣的好處在於,在啟動線程后統一join,縮短了程序運行時間,並且提高運行效率。

關於python的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。

Python GIL其實是功能和性能之間權衡后的產物,它尤其存在的合理性,也有較難改變的客觀因素,無論你啟多少個線程,你有多少個cpu, Python在執行的時候在同一時刻只允許一個線程運行。

 

線程鎖(互斥鎖Mutex)

一個進程下可以啟動多個線程,多個線程共享父進程的內存空間,也就意味着每個線程可以訪問同一份數據,此時,如果2個線程同時要修改同一份數據,會出現什么狀況?

import threading
import time
NUM=5

def fun():
    global NUM
    NUM-=1
    time.sleep(2)
    print(NUM)
for i in range(5):
    t=threading.Thread(target=fun)
    t.start()
結果:
0
0
0
0
0
上述結果並不是我們想要的,去掉了sleep結果才是我們想要的,若是不去掉sleep呢,該怎么辦?,此時我們可以加鎖實現。
import threading
import time
NUM=5#共享數據

def fun():
    global NUM
    lock.acquire()#獲取鎖
    NUM-=1
    time.sleep(2)
    print(NUM)
    lock.release()#釋放鎖
lock=threading.Lock()
for i in range(5):
    t=threading.Thread(target=fun)
    t.start()

 

RLock(遞歸鎖)

遞歸鎖,通俗來講就是大鎖里面再加小鎖,有人可能會問,那我使用Lock不就完了嗎,其實不然,想象一下,現在有兩道門,一把鎖對應一把鑰匙,如果使用Lock,進去第一個門獲取一把鎖,在進去第二個人門又獲取一把鎖,然后要出來開鎖時候(釋放鎖)程序還是用第一個門進來的鑰匙,此時就會一直阻塞,那么RLock就解決了這樣的問題場景。

demo

import threading
import time
l=threading.RLock()#實例化
def indoor(name):
    print('%s across second door'%name)
    l.acquire()
    print('%s do somethind',name)
    time.sleep(2)
    l.release()
def outdoor(name):
    print('%s across first door'%name)
    l.acquire()
    indoor(name)
    print('%s do somethind'% name)
    time.sleep(1)
    l.release()

outdoor('wd')
#上述代碼,若將l=threading.RLock()改為l=threading.Lock(),程序將一直阻塞。

 

Semaphore&BoundedSemaphore(信號量)

前面已經介紹過了互斥鎖, 互斥鎖同時只允許一個線程更改數據,而Semaphore是同時允許一定數量的線程更改數據 ,比如廁所有3個坑,那最多只允許3個人上廁所,后面的人只能等里面有人出來了才能再進去.

1.Semaphore和BoundedSemaphore使用方法一致

方法:

  • acquire(blocking=True,timeout=None)
  • release()

demo:

import threading
import time

def door(n):
    sp.acquire()#獲取一把鎖,可設置超時時間
    print('%d in the door'% n)
    time.sleep(1)
    print('%d out the door'% n)
    sp.release()#釋放鎖
sp=threading.Semaphore(3)#最多允許3個線程同時運行(獲取到信號量)
for i in range(5):
    t=threading.Thread(target=door,args=(i,))
    t.start()#啟動線程
結果:
0 in the door
1 in the door
2 in the door
1 out the door
0 out the door
3 in the door
4 in the door
2 out the door
4 out the door
3 out the door

Events 

Event是線程間通信最間的機制之一:一個線程發送一個event信號,其他的線程則等待這個信號。用於主線程控制其他線程的執行。 Events 維護着一個flag,這個flag可以使用set()設置成True或者使用clear()重置為False,flag默認為False,而當flag為false時候,wait(timeout=s)則阻塞。

常用方法:

  • Event.set():將標志設置為True
  • Event.clear():清空標志位,設置為False
  • Event.wait(timeout=s):等待(阻塞),直到標志位變成True
  • Event.is_set():判斷標志位是否被設置

通過Event來實現兩個或多個線程間的交互,例如紅綠燈,即起動一個線程做交通指揮燈,生成幾個線程做車輛,車輛行駛按紅燈停,綠燈行的規則。

demo:

import threading,time
import random
def light():
    if not event.isSet():
        event.set() #wait就不阻塞 #綠燈狀態
    count = 0
    while True:
        if count < 10:
            print('\033[42;1m--green light on---\033[0m')
        elif count <13:
            print('\033[43;1m--yellow light on---\033[0m')
        elif count <20:
            if event.isSet():
                event.clear()
            print('\033[41;1m--red light on---\033[0m')
        else:
            count = 0
            event.set() #打開綠燈
        time.sleep(1)
        count +=1
def car(n):
    while 1:
        time.sleep(random.randrange(10))

        if  event.isSet(): #綠燈
            print("car [%s] is running.." % n)
        else:
            print("car [%s] is waiting for the red light.." %n)
        #event.wait()
if __name__ == '__main__':
    event = threading.Event()
    Light = threading.Thread(target=light)
    Light.start()
    for i in range(3):
        t = threading.Thread(target=car,args=(i,))
        t.start()

Timer(定時器)

Timer用來定時執行某個線程,若取消運行,則使用cancel方法。

demo:

import  threading
def show_name(name):
    print("my name is ",name)

t=threading.Timer(5,show_name,args=('wd',))#設置5秒后運行該線程
t.start()#啟動線程
print('要開始運行線程了')
t.cancel()#取消運行的線程
結果:
要開始運行線程了

 線程池:

啟動一個線程消耗的資源非常少,所以對線程的使用官方並沒有給出標准的線程池模塊,第三方模塊(Threadpool),下面我們自己定義簡單線程池。

簡單demo:

import threading
import queue
import time
class MyThread:
    def __init__(self,max_num=10):
        self.queue = queue.Queue()
        for n in range(max_num):
            self.queue.put(threading.Thread)
    def get_thread(self):
        return self.queue.get()
    def put_thread(self):
        self.queue.put(threading.Thread)
pool = MyThread(5)
def RunThread(arg,pool):
    print(arg)
    time.sleep(2)
    pool.put_thread()
for n in range(10):
    Thread=pool.get_thread()
    t=Thread(target=RunThread, args=(n,pool,))
    t.start()

 

二、進程介紹

一個進程至少要包含一個線程,每個進程在啟動的時候就會自動的啟動一個線程,進程里面的第一個線程就是主線程,每次在進程內創建的子線程都是由主線程進程創建和銷毀,子線程也可以由主線程創建出來的線程創建和銷毀線程。

進程是對各種資源管理的集合,比如要調用內存、CPU、網卡、聲卡等,進程要操作上述的硬件之前都必須要創建一個線程,進程里面可以包含多個線程,QQ就是一個進程。

繼續拿QQ來說,比如我現在打卡了QQ的聊天窗口、個人信息窗口、設置窗口等,那么每一個打開的窗口都是一個線程,他們都在執行不同的任務,比如聊天窗口這個線程可以和好友進行互動,聊天,視頻等,個人信息窗口我可以查看、修改自己的資料。

為了進程安全起見,所以兩個進程之間的數據是不能夠互相訪問的(默認情況下),比如自己寫了一個應用程序,然后讓別人運行起來,那么我的這個程序就可以訪問用戶啟動的其他應用,我可以通過我自己的程序去訪問QQ,然后拿到一些聊天記錄等比較隱秘的信息,那么這個時候就不安全了,所以說進程與進程之間的數據是不可以互相訪問的,而且每一個進程的內存是獨立的。

多進程

多進程的資源是獨立的,不可以互相訪問,如果想多個進程之間實現數據交互就必須通過中間件實現。

啟動一個進程方法與啟動一個線程類似

demo:

#!/usr/bin/env python3
#_*_ coding:utf-8 _*_
#Author:wd
from multiprocessing import Process
def show(n):
    print('runing',n)


if __name__ == '__main__':

    p=Process(target=show,args=(1,))
    p.start()
    p.join()

在進程中啟動線程:

#!/usr/bin/env python3
#_*_ coding:utf-8 _*_
#Author:wd
from multiprocessing import Process
import threading
import os
def fun(n):
    print("run threading ",n)
    print('當前進程id ',os.getpid())
def show(n):
    print("子進程id:{} 父進程id:{} ".format(os.getpid(),os.getppid()))#ppid指父進程pid,pid當前進程pid
    print('runing',n)
    t=threading.Thread(target=fun,args=(2,))#啟動一個線程
    t.start()
    t.join()


if __name__ == '__main__':
    print("主進程id",os.getpid())
    p=Process(target=show,args=(1,))
    p.start()
    p.join()
結果:
主進程id 8092
子進程id:9100父進程id:8092 
runing 1
run threading  2
當前進程id  9100

進程間通信方法(Queue、Pipes、Mangers)

前面已經提到,進程間通信是需要中間件來實現的,下面介紹幾個實現進程間通信的中間件。

1.進程Queue:建立一個共享的隊列(其實並不是共享的,實際是克隆的,內部維護着數據的共享),多個進程可以向隊列里存/取數據。

demo:

#!/usr/bin/env python3
#_*_ coding:utf-8 _*_
#Author:wd
from multiprocessing import Queue,Process
def fun(q):
    q.put([1,2,3])

if __name__ == '__main__':
    Q=Queue(5)#設置進程隊列長度
    for i in range(2):#啟動兩個進程,想隊列里put數據
        process=Process(target=fun,args=(Q,))#創建一個進程,將Q傳入,實際上是克隆了Q
        process.start()
        process.join()
    print(Q.get())#在主進程中獲取元素
    print(Q.get())
結果:
[1, 2, 3]
[1, 2, 3]

2.Pipes(管道)

正如其名,進程間的管道內部機制通過啟動socket連接來維護兩個進程間的通訊。 

demo:

from multiprocessing import Process,Pipe
def son_process(con):
    con.send('hello wd')#向管道另一頭發起
    print("from father:",con.recv())#子進程收取數據,如果沒收到會阻塞

if __name__ == '__main__':
    son,father=Pipe()#實例化管道,生成socket連接,一個客戶端一個服務端
    P=Process(target=son_process,args=(son,))#啟動一個子進程,將生成的socket對象傳遞進去
    P.start()
    #P.join()
    print(father.recv())#主進程收取子進程信息
    father.send('ok,i know')
結果:
hello wd
from father: ok,i know

3.Manager(數據共享)

Manager實現了多個進程間的數據共享,支持的數據類型有 listdictNamespaceLockRLockSemaphoreBoundedSemaphoreConditionEventBarrierQueueValue and Array

demo:

from multiprocessing import Manager,Process
def fun(l,d):
    l.append(1)
    l.append(2)
    d['name']='wd'
if __name__ == '__main__':

    with Manager() as manager:
        L=manager.list()#定義共享列表
        D=manager.dict()#定義共享字典
        p1=Process(target=fun,args=(L,D))#啟動一個進程,將定義的list和dict傳入
        p1.start()#啟動進程
        p1.join()#等帶結果
        print(L)
        print(D)
結果:
[1, 2]
{'name': 'wd'}

進程鎖(Lock)

 進程鎖和線程鎖使用語法上完全一致。

demo:

from multiprocessing import Process,Lock
import time
def fun(l,i):
    l.acquire()#獲取鎖
    print("running process ",i)
    time.sleep(2)
    l.release()#釋放 if __name__ == '__main__':
    lock=Lock()#生成鎖的實例
    for i in range(5):
        p=Process(target=fun,args=(lock,i))#創建進程
        p.start()#啟動,這里沒有join,看到的效果還是竄行的
結果:
running process  0
running process  1
running process  2
running process  3
running process  4

 

pool(進程池)

進程池內部維護一個進程序列,當使用時,則去進程池中獲取一個進程,如果進程池序列中沒有可供使用的進進程,那么程序就會等待,直到進程池中有可用進程為止。

主要方法:

  • apply  :該方法啟動進程為串行執行
  • apply_async:啟動進程為並行執行

demo:

from multiprocessing import Pool
import os
import time
def fun(i):
    print('runing process ',i)
    time.sleep(2)

def end_fun(i=None):
    print('done')
    print("call back process:",os.getpid())
if __name__ == '__main__':
    print("主進程:",os.getpid())
    pool=Pool(3)#最多運行3個進程同時運行
    for i in range(5):
        pool.apply_async(func=fun,args=(i,),callback=end_fun)#並行執行,callback回調由主程序回調
        #pool.apply(func=fun,args=(i,))串行執行
    pool.close()#關閉進程池
    pool.join()#進程池中進程執行完畢后再關閉,如果注釋,那么程序直接關閉
結果:
主進程: 12396
runing process  0
runing process  1
runing process  2
runing process  3
runing process  4
done
call back process: 12396
done
call back process: 12396
done
call back process: 12396
done
call back process: 12396
done
call back process: 12396

 

三、線程與進程的關系與區別
  1. 線程是執行的指令集,進程是資源的集合;
  2. 線程的啟動速度要比進程的啟動速度要快;
  3. 兩個線程的執行速度是一樣的;
  4. 進程與線程的運行速度是沒有可比性的;
  5. 線程共享創建它的進程的內存空間,進程的內存是獨立的。
  6. 兩個線程共享的數據都是同一份數據,兩個子進程的數據不是共享的,而且數據是獨立的;
  7. 同一個進程的線程之間可以直接交流,同一個主進程的多個子進程之間是不可以進行交流,如果兩個進程之間需要通信,就必須要通過一個中間代理來實現;
  8. 一個新的線程很容易被創建,一個新的進程創建需要對父進程進行一次克隆
  9. 一個線程可以控制和操作同一個進程里的其他線程,線程與線程之間沒有隸屬關系,但是進程只能操作子進程
  10. 改變主線程,有可能會影響到其他線程的行為,但是對於父進程的修改是不會影響子進程;

詳細介紹可參考:https://my.oschina.net/cnyinlinux/blog/422207


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM