天天看點

Python 淺析線程(threading子產品)和程序(process)

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

程序與線程

什麼是線程(threading)?

A thread is an execution context, which is all the information a CPU needs to execute a stream of instructions.

Suppose you're reading a book, and you want to take a break right now, but you want to be able to come back and resume reading from the exact point where you stopped. One way to achieve that is by jotting down the page number, line number, and word number. So your execution context for reading a book is these 3 numbers.

If you have a roommate, and she's using the same technique, she can take the book while you're not using it, and resume reading from where she stopped. Then you can take it back, and resume it from where you were.

Threads work in the same way. A CPU is giving you the illusion that it's doing multiple computations at the same time. It does that by spending a bit of time on each computation. It can do that because it has an execution context for each computation. Just like you can share a book with your friend, many tasks can share a CPU.

On a more technical level, an execution context (therefore a thread) consists of the values of the CPU's registers.

Last: threads are different from processes. A thread is a context of execution, while a process is a bunch of resources associated with a computation. A process can have one or many threads.

Clarification: the resources associated with a process include memory pages (all the threads in a process have the same view of the memory), file descriptors (e.g., open sockets), and security credentials (e.g., the ID of the user who started the process).

    線程的出現是為了降低上下文切換的消耗,提高系統的并發性,并突破一個程序隻能幹一樣事的缺陷,使到程序内并發成為可能。

      假設,一個文本程式,需要接受鍵盤輸入,将内容顯示在螢幕上,還需要儲存資訊到硬碟中。若隻有一個程序,勢必造成同一時間隻能幹一樣事的尴尬(當儲存時,就不能通過鍵盤輸入内容)。若有多個程序,每個程序負責一個任務,程序A負責接收鍵盤輸入的任務,程序B負責将内容顯示在螢幕上的任務,程序C負責儲存内容到硬碟中的任務。這裡程序A,B,C間的協作涉及到了程序通信問題,而且有共同都需要擁有的東西——-文本内容,不停的切換造成性能上的損失。若有一種機制,可以使任務A,B,C共享資源,這樣上下文切換所需要儲存和恢複的内容就少了,同時又可以減少通信所帶來的性能損耗,那就好了。是的,這種機制就是線程。

        線程也叫輕量級程序,它是一個基本的CPU執行單元,也是程式執行過程中的最小單元,由線程ID、程式計數器、寄存器集合和堆棧共同組成。線程的引入減小了程式并發執行時的開銷,提高了作業系統的并發性能。線程沒有自己的系統資源。

什麼是程序(process)?

An executing instance of a program is called a process.

Each process provides the resources needed to execute a program. A process has a virtual address space, executable code, open handles to system objects, a security context, a unique process identifier, environment variables, a priority class, minimum and maximum working set sizes, and at least one thread of execution. Each process is started with a single thread, often called the primary thread, but can create additional threads from any of its threads.

      程序就是一個程式在一個資料集上的一次動态執行過程。程序一般由程式、資料集、程序控制塊三部分組成。我們編寫的程式用來描述程序要完成哪些功能以及如何完成;資料集則是程式在執行過程中所需要使用的資源;程序控制塊用來記錄程序的外部特征,描述程序的執行變化過程,系統可以利用它來控制和管理程序,它是系統感覺程序存在的唯一标志。

舉一例說明程序:

想象一位有一手好廚藝的計算機科學家正在為他的女兒烘制生日蛋糕。他有做生日蛋糕的食譜,廚房裡有所需的原料:面粉、雞蛋、糖、香草汁等。在這個比喻中,做蛋糕的食譜就是程式(即用适當形式描述的算法)計算機科學家就是處理器(cpu),而做蛋糕的各種原料就是輸入資料。程序就是廚師閱讀食譜、取來各種原料以及烘制蛋糕等一系列動作的總和。現在假設計算機科學家的兒子哭着跑了進來,說他的頭被一隻蜜蜂蟄了。計算機科學家就記錄下他照着食譜做到哪兒了(儲存程序的目前狀态),然後拿出一本急救手冊,按照其中的訓示處理蟄傷。這裡,我們看到處理機從一個程序(做蛋糕)切換到另一個高優先級的程序(實施醫療救治),每個程序擁有各自的程式(食譜和急救手冊)。當蜜蜂蟄傷處理完之後,這位計算機科學家又回來做蛋糕,從他離開時的那一步繼續做下去。

程序與線程之間的差別?

          程序是計算機中的程式關于某資料集合上的一次運作活動,是系統進行資源配置設定和排程的基本機關,是作業系統結構的基礎。或者說程序是具有一定獨立功能的程式關于某個資料集合上的一次運作活動,程序是系統進行資源配置設定和排程的一個獨立機關。

       線程則是程序的一個實體,是CPU排程和分派的基本機關,它是比程序更小的能獨立運作的基本機關。

Python 淺析線程(threading子產品)和程式(process)

程序和線程的關系:

(1)一個線程隻能屬于一個程序,而一個程序可以有多個線程,但至少有一個線程。

(2)資源配置設定給程序,同一程序的所有線程共享該程序的所有資源。

(3)CPU分給線程,即真正在CPU上運作的是線程。

并行與并發

并行處理(Parallel Processing)是計算機系統中能同時執行兩個或者更多個處理的一種計算方法。并行處理可同時工作于同一程式的不同方面,并行處理的主要目的是節省大型和複雜問題的解決時間。

并發處理(concurrency Processing)是指一個時間段中有幾個程式都處于已經啟動運作到運作完畢之間,而且這幾個程式都是在同一處理機(CPU)上運作,但任意時刻點上隻有一個程式在處理機(CPU)上運作

并發的關鍵在于你有處理多個任務的能力,不一定同時,并行的關鍵是你有同時處理多個任務的能力,是以說,并行是并發的子集

Python 淺析線程(threading子產品)和程式(process)

同步與異步

       在計算機領域,同步就是指一個程序在執行某個請求的時候,若該請求需要一段時間才能傳回資訊,那麼這個程序将會一直等待下去,直到收到傳回資訊才繼續執行下去;異步是指程序不需要一直等下去,而是繼續執行下面的操作,不管其他程序的狀态。當有消息傳回時系統會通知程序進行處理,這樣可以提高執行的效率。舉個例子,打電話時就是同步通信,發短息時就是異步通信。

 線程(Thread)類的執行個體方法

 join和Daemon

join():在子線程完成運作之前,這個子線程的父線程将一直被阻塞。

setDaemon(True):

      将線程聲明為守護線程,必須在start() 方法調用之前設定, 如果不設定為守護線程程式會被無限挂起。這個方法基本和join是相反的。當我們 在程式運作中,執行一個主線程,如果主線程又建立一個子線程,主線程和子線程 就分兵兩路,分别運作,那麼當主線程完成想退出時,會檢驗子線程是否完成。如 果子線程未完成,則主線程會等待子線程完成後再退出。但是有時候我們需要的是 隻要主線程完成了,不管子線程是否完成,都要和主線程一起退出,這時就可以 用setDaemon方法啦 

# join():在子線程完成運作之前,這個子線程的父線程将一直被阻塞。

# setDaemon(True):
        '''
         将線程聲明為守護線程,必須在start() 方法調用之前設定,如果不設定為守護線程程式會被無限挂起。

         當我們在程式運作中,執行一個主線程,如果主線程又建立一個子線程,主線程和子線程 就分兵兩路,分别運作,那麼當主線程完成

         想退出時,會檢驗子線程是否完成。如果子線程未完成,則主線程會等待子線程完成後再退出。但是有時候我們需要的是隻要主線程

         完成了,不管子線程是否完成,都要和主線程一起退出,這時就可以 用setDaemon方法啦'''


import threading
from time import ctime,sleep
import time

def Music(name):

        print ("Begin listening to {name}. {time}".format(name=name,time=ctime()))
        sleep(3)
        print("end listening {time}".format(time=ctime()))

def Blog(title):

        print ("Begin recording the {title}. {time}".format(title=title,time=ctime()))
        sleep(5)
        print('end recording {time}'.format(time=ctime()))


threads = []


t1 = threading.Thread(target=Music,args=('FILL ME',))
t2 = threading.Thread(target=Blog,args=('',))

threads.append(t1)
threads.append(t2)

if __name__ == '__main__':

    #t2.setDaemon(True)

    for t in threads:

        #t.setDaemon(True) #注意:一定在start之前設定
        t.start()

        #t.join()

    #t1.join()
    #t2.join()    #  考慮這三種join位置下的結果?

    print ("all over %s" %ctime())
      

  

daemon
A boolean value indicating whether this thread is a daemon thread (True) or not (False). This must be set before start() is called, otherwise RuntimeError is raised. Its initial value is inherited from the creating thread; the main thread is not a daemon thread and therefore all threads created in the main thread default to daemon = False.

The entire Python program exits when no alive non-daemon threads are left.

當daemon被設定為True時,如果主線程退出,那麼子線程也将跟着退出,

反之,子線程将繼續運作,直到正常退出。

daemon
      

 其他方法

Thread執行個體對象的方法
  # isAlive(): 傳回線程是否活動的。
  # getName(): 傳回線程名。
  # setName(): 設定線程名。

threading子產品提供的一些方法:
  # threading.currentThread(): 傳回目前的線程變量。
  # threading.enumerate(): 傳回一個包含正在運作的線程的list。正在運作指線程啟動後、結束前,不包括啟動前和終止後的線程。
  # threading.activeCount(): 傳回正在運作的線程數量,與len(threading.enumerate())有相同的結果。
      

python GIL(global  Interper  Lack)是什麼?

GIL的概念

GIL全稱是全局解釋器鎖,來源是python設計之初的考慮,為了資料安全所做的決定

每個CPU在同一時期隻能執行一個線程(在單核CPU下的多線程其實都隻是并發的,不是并行,并行和并發從宏觀上來講都是同時處理多路請求的概念。但是并發和并行又有差別,并行是指兩個或者多個事件在同一時刻發生;而并發是指兩個或者多個事件在同一時間間隔内發生)

CPython implementation detail: In CPython, due to the Global Interpreter Lock, only one thread can execute Python code at once (even though certain performance-oriented libraries might overcome this limitation). If you want your application to make better use of the computational resources of multi-core machines, you are advised to use multiprocessing. However, threading is still an appropriate model if you want to run multiple I/O-bound tasks simultaneously.

GIL的早期設計

          Python支援多線程,而解決多線程之間資料完整性和狀态同步的最簡單方法自然就是加鎖。 于是有了GIL這把超級大鎖,而當越來越多的代碼庫開發者接受了這種設定後,他們開始大量依賴這種特性(即預設python内部對象是thread-safe的,無需在實作時考慮額外的記憶體鎖和同步操作)。慢慢的這種實作方式被發現是蛋疼且低效的。但當大家試圖去拆分和去除GIL的時候,發現大量庫代碼開發者已經重度依賴GIL而非常難以去除了。有多難?做個類比,像MySQL這樣的“小項目”為了把Buffer Pool Mutex這把大鎖拆分成各個小鎖也花了從5.5到5.6再到5.7多個大版為期近5年的時間,并且仍在繼續。MySQL這個背後有公司支援且有固定開發團隊的産品走的如此艱難,那又更何況Python這樣核心開發和代碼貢獻者高度社群化的團隊呢?

GIL的影響

在python多線程下,每個線程的執行方式:

(1)擷取GIL

(2)執行代碼直到sleep或者是python虛拟機将其挂起

(3)釋放GIL

   可見,某個線程想要執行,必須先拿到GIL,我們可以将GIL看作是“通行證”,并且在一個python程序中,GIL隻有一個,拿不到通行證的線程,就不允許進入CPU執行。

  而每次釋放GIL鎖,線程進行鎖競争,切換線程,會消耗資源。并且由于GIL鎖存在,python裡一個程序永遠隻能同時執行一個線程(拿到GIL的線程才能執行),這就是為什麼在多核CPU上,python的多線程效率并不高的原因。

那麼是不是python的多線程就完全沒有作用了呢?

在這裡我們分類讨論:

(1):CPU密集型代碼:又稱計算密集型任務,是指CPU計算占主要的任務,CPU一直處于滿負荷的狀态,比如在一個很大的清單中查找元素(當然這不合理),複雜的加減乘除等

(各種循環處理,計數等等),在這種情況下,ticks計數很快會達到阙值,然後觸發GIL的釋放與再競争(多個線程來回切換當然是需要消耗資源的),是以python下的多線程對CPU密集型代碼并不友好。

(2):IO密集型代碼:指磁盤IO,網絡IO占主要的任務,計算量很小。比如請求網頁,讀取檔案等,當然我們可以在python中可以利用sleep達到IO密集型任務的目的

(檔案處理,網絡爬蟲等),多線程能夠有效的提升效率(單線程下有IO操作會進行IO等待,造成不必要的時間浪費,而開啟多線程能線上程A等待的時候,自動切換到線程B,可以不浪費CPU資源,進而能提升程式代碼執行效率),是以python的多線程對IO密集型代碼比較友好。

    在python3.x中GIL不适用ticks計數,改為使用計時器(執行時間達到阙值後,目前線程釋放GIL),這樣對CPU密集型程式更加友好,但是依然沒有解決GIL導緻的同一時間隻能執行一個線程的問題,是以效率依然不盡人意

 多核多線程比單核多線程更差,原因是單核下多線程,每次釋放GIL,喚醒的那個線程都能擷取到GIL鎖,是以能夠無縫執行,但是在多核下導緻其他幾個COU上被喚醒後的線程會醒着等待到切換時間後又進入到待排程狀态,這樣會造成線程颠簸,導緻效率更低。

是以說“python下想要充分使用多核CPU,就要利用多程序”的原因是什麼呢?

每個程序都有各自獨立的GIL,互不幹擾,這樣就可以真正意義上并行執行,是以在python中,多程序的執行效率優于多線程,(僅僅針對多核CPU)

故我們得出結論:多核下,想要并行提升效率,比較通用的方法是使用多程序,能夠有效的提高執行效率。

#coding:utf8
from threading import Thread
import time

def counter():
    i = 0
    for _ in range(50000000):
        i = i + 1

    return True


def main():

    l=[]
    start_time = time.time()

    for i in range(2):

        t = Thread(target=counter)
        t.start()
        l.append(t)
        t.join()

    # for t in l:
    #     t.join()

    end_time = time.time()
    print("Total time: {}".format(end_time - start_time))

if __name__ == '__main__':
    main()


'''
py2.7:
     串行:25.4523348808s
     并發:31.4084379673s
py3.5:
     串行:8.62115597724914s
     并發:8.99609899520874s

'''
      

 

 多程序

 多程序的概念

multiprocessing

 is a package that supports spawning processes using an API similar to the 

threading

 module. The 

multiprocessing

 package offers both local and remote concurrency,effectively side-stepping the Global Interpreter Lock by using subprocesses instead of threads. Due to this, the 

multiprocessing

 module allows the programmer to fully leverage multiple processors on a given machine. It runs on both Unix and Windows.

由于GIL的存在,python中的多線程其實并不是真正的多線程,如果想要充分地使用多核CPU的資源,在python中大部分情況需要使用多程序。Python提供了非常好用的多程序包multiprocessing,隻需要定義一個函數,Python會完成其他所有事情。借助這個包,可以輕松完成從單程序到并發執行的轉換。multiprocessing支援子程序、通信和共享資料、執行不同形式的同步,提供了Process、Queue、Pipe、Lock等元件。

  multiprocessing包是Python中的多程序管理包。與threading.Thread類似,它可以利用multiprocessing.Process對象來建立一個程序。該程序可以運作在Python程式内部編寫的函數。該Process對象與Thread對象的用法相同,也有start(), run(), join()的方法。此外multiprocessing包中也有Lock/Event/Semaphore/Condition類 (這些對象可以像多線程那樣,通過參數傳遞給各個程序),用以同步程序,其用法與threading包中的同名類一緻。是以,multiprocessing的很大一部份與threading使用同一套API,隻不過換到了多程序的情境。

但在使用這些共享API的時候,我們要注意以下幾點:

  • 在UNIX平台上,當某個程序終結之後,該程序需要被其父程序調用wait,否則程序成為僵屍程序(Zombie)。是以,有必要對每個Process對象調用join()方法 (實際上等同于wait)。對于多線程來說,由于隻有一個程序,是以不存在此必要性。
  • multiprocessing提供了threading包中沒有的IPC(比如Pipe和Queue),效率上更高。應優先考慮Pipe和Queue,避免使用Lock/Event/Semaphore/Condition等同步方式 (因為它們占據的不是使用者程序的資源)。
  • 多程序應該避免共享資源。在多線程中,我們可以比較容易地共享資源,比如使用全局變量或者傳遞參數。在多程序情況下,由于每個程序有自己獨立的記憶體空間,以上方法并不合适。此時我們可以通過共享記憶體和Manager的方法來共享資源。但這樣做提高了程式的複雜度,并因為同步的需要而降低了程式的效率。

Process.PID中儲存有PID,如果程序中還沒有start(),則PID為None。

windows系統下,需要注意的是想要啟動一個子程序必須加上

if __name__ ="__main__"
      

 程序相關要寫在上面這句語句下面。

舉個例子:

from multiprocessing import Process
import time
def f(name):
    time.sleep(1)
    print('hello', name,time.ctime())

if __name__ == '__main__':
    p_list=[]
    for i in range(3):
        p = Process(target=f, args=('alvin',))
        p_list.append(p)
        p.start()
    for i in p_list:
        p.join()
    print('end')
      

  類式調用

from multiprocessing import Process
import time

class MyProcess(Process):
    def __init__(self):
        super(MyProcess, self).__init__()
        #self.name = name

    def run(self):
        time.sleep(1)
        print ('hello', self.name,time.ctime())


if __name__ == '__main__':
    p_list=[]
    for i in range(3):
        p = MyProcess()
        p.start()
        p_list.append(p)

    for p in p_list:
        p.join()

    print('end')
      

Process類

構造方法

Process([group [, target [, name [, args [, kwargs]]]]])

  group: 線程組,目前還沒有實作,庫引用中提示必須是None; 

  target: 要執行的方法; 

  name: 程序名; 

  args/kwargs: 要傳入方法的參數。

執行個體方法

is_alive():傳回程序是否在運作。

  join([timeout]):阻塞目前上下文環境的程序程,直到調用此方法的程序終止或到達指定的timeout(可選參數)。

  start():程序準備就緒,等待CPU排程

  run():strat()調用run方法,如果執行個體程序時未制定傳入target,這star執行t預設run()方法。

  terminate():不管任務是否完成,立即停止工作程序

屬性

       authkey

  daemon:和線程的setDeamon功能一樣

  exitcode(程序在運作時為None、如果為–N,表示被信号N結束)

  name:程序名字。

  pid:程序号。

import time
from  multiprocessing import Process

def foo(i):
    time.sleep(1)
    print (p.is_alive(),i,p.pid)
    time.sleep(1)

if __name__ == '__main__':
    p_list=[]
    for i in range(10):
        p = Process(target=foo, args=(i,))
        #p.daemon=True
        p_list.append(p)

    for p in p_list:
        p.start()
    # for p in p_list:
    #     p.join()

    print('main process end')
      

程序間通訊  

不同程序間記憶體是不共享的,要想實作兩個程序間的資料交換,可以用以下方法:

——程序隊列Queues(使用方法跟threading裡的queue類似:)

from multiprocessing import Process, Queue

def f(q,n):
    q.put([42, n, 'hello'])

if __name__ == '__main__':
    q = Queue()
    p_list=[]
    for i in range(3):
        p = Process(target=f, args=(q,i))
        p_list.append(p)
        p.start()
    print(q.get())
    print(q.get())
    print(q.get())
    for i in p_list:
            i.join()
      

——管道Pipe

from multiprocessing import Process, Pipe
 
def f(conn):
    conn.send([42, None, 'hello'])
    conn.close()
 
if __name__ == '__main__':
    parent_conn, child_conn = Pipe()
    p = Process(target=f, args=(child_conn,))
    p.start()
    print(parent_conn.recv())   # prints "[42, None, 'hello']"
    p.join()
      

——managers

from multiprocessing import Process, Manager

def f(d, l,n):
    d[n] = '1'
    d['2'] = 2
    d[0.25] = None
    l.append(n)
    print(l)

if __name__ == '__main__':
    with Manager() as manager:
        d = manager.dict()

        l = manager.list(range(5))
        p_list = []
        for i in range(10):
            p = Process(target=f, args=(d, l,i))
            p.start()
            p_list.append(p)
        for res in p_list:
            res.join()

        print(d)
        print(l)
      

程序同步

from multiprocessing import Process, Lock

def f(l, i):
    l.acquire()
    try:
        print('hello world', i)
    finally:
        l.release()

if __name__ == '__main__':
    lock = Lock()

    for num in range(10):
        Process(target=f, args=(lock, num)).start()
      

程序池

程序池内部維護一個程序式列,當使用時,去程序池中擷取一個程序,如果程序池序列中沒有可供使用的程序,那麼程式就會等待,直到程序池中有可用程序為止。

程序池中有以下幾個主要方法:

  1. apply:從程序池裡取一個程序并執行
  2. apply_async:apply的異步版本
  3. terminate:立刻關閉線程池
  4. join:主程序等待所有子程序執行完畢,必須在close或terminate之後
  5. close:等待所有程序結束後,才關閉線程池
from  multiprocessing import Process,Pool
import time
 
def Foo(i):
    time.sleep(2)
    return i+100
 
def Bar(arg):
    print('-->exec done:',arg)
 
pool = Pool(5)
 
for i in range(10):
    pool.apply_async(func=Foo, args=(i,),callback=Bar)
    #pool.apply(func=Foo, args=(i,))
 
print('end')
pool.close()
pool.join()
      

https://www.cnblogs.com/zingp/p/5878330.html

不經一番徹骨寒 怎得梅花撲鼻香