Python多線程和多進程

一、簡介

什么是線程?
線程也叫輕量級進程,是操作系統能夠進行運算調度的最小單位,它被包涵在進程之中,是進程中的實際運作單位。
線程自己不擁有系統資源,只擁有一點兒在運行中必不可少的資源,但它可與同屬一個進程的其他線程共享進程所擁有的全部資源。一個線程可以創建和撤銷另一個線程,同一個進程中的多個線程之間可以并發執行。

為什么要使用多線程?
線程在程序中是獨立的、并發的執行流。與分隔的進程相比,進程中線程之間的隔離程度要小,它們共享內存、文件句柄和其他進程應有的狀態。
因為線程的劃分尺度小于進程,使得多線程程序的并發性高。進程在執行過程之中擁有獨立的內存單元,而多個線程共享內存,從而極大的提升了程序的運行效率。
線程比進程具有更高的性能,這是由于同一個進程中的線程都有共性,多個線程共享一個進程的虛擬空間。線程的共享環境包括進程代碼段、進程的共有數據等,利用這些共享的數據,線程之間很容易實現通信。
操作系統在創建進程時,必須為改進程分配獨立的內存空間,并分配大量的相關資源,但創建線程則簡單得多。因此,使用多線程來實現并發比使用多進程的性能高得要多。

總結起來,使用多線程編程具有如下幾個優點:
進程之間不能共享內存,但線程之間共享內存非常容易。
操作系統在創建進程時,需要為該進程重新分配系統資源,但創建線程的代價則小得多。因此使用多線程來實現多任務并發執行比使用多進程的效率高。python語言內置了多線程功能支持,而不是單純地作為底層操作系統的調度方式,從而簡化了python的多線程編程。


二、創建多線程和多進程

2.1 普通方式創建

2.1.1 多線程

import threading
from threading import Lock,Thread
import time

def run(n):
    print('task', n)
    time.sleep(1)
    print('2s')
    time.sleep(1)
    print('1s')
    time.sleep(1)
    print('0s')
    time.sleep(1)


if __name__ == '__main__':
    t1 = threading.Thread(target=run, args=('t1',))  # target是要執行的函數名(不是函數),args是函數對應的參數,以元組的形式存在
    t2 = threading.Thread(target=run, args=('t2',))
    t1.start()
    t2.start()

2.1.2 多進程

相比較于threading模塊用于創建python多線程,python提供multiprocessing用于創建多進程。先看一下創建進程的兩種方式。

import time
import multiprocessing

def run(n):
    print('task', n)
    time.sleep(1)
    print('2s')
    time.sleep(1)
    print('1s')
    time.sleep(1)
    print('0s')
    time.sleep(1)

if __name__ == '__main__':
    t1 = multiprocessing.Process(target=run, args=('t1',))
    t2 = multiprocessing.Process(target=run, args=('t2',))
    t1.start()
    t2.start()

2.2 自定義對象方式創建

2.2.1 多線程

繼承threading.Thread來定義線程類,其本質是重構Thread類中的run方法

import threading
import time


class MyThread(threading.Thread):
    def __init__(self, n):
        super().__init__()
        self.n = n

    def run(self) -> None:
        print('task', self.n)
        time.sleep(1)
        print('2s')
        time.sleep(1)
        print('1s')
        time.sleep(1)
        print('0s')
        time.sleep(1)

if __name__ == '__main__':
    t1 = MyThread('t1')
    t2 = MyThread('t2')
    t1.start()
    t2.start()

2.2.2 多進程

改變父類為multiprocessing.Process即可。

import time
import multiprocessing

# class MyThread(multiprocessing.Process):
    def __init__(self, n):
        super().__init__()
        self.n = n

    def run(self) -> None:
        print('task', self.n)
        time.sleep(1)
        print('2s')
        time.sleep(1)
        print('1s')
        time.sleep(1)
        print('0s')
        time.sleep(1)


if __name__ == '__main__':
    t1 = MyThread('t1')
    t2 = MyThread('t2')
    t1.start()
    t2.start()

2.3 守護線程

下面這個例子,這里使用setDaemon(True)把所有的子線程都變成了主線程的守護線程,因此當主線程結束后,子線程也會隨之結束,所以當主線程結束后,整個程序就退出了。
所謂’線程守護’,就是主線程不管該線程的執行情況,只要是其他子線程結束且主線程執行完畢,主線程都會關閉。也就是說: 主線程不等待該守護線程的執行完再去關閉。

import time
import threading

def run(n):
    print('task', n)
    time.sleep(1)
    print('3s')
    time.sleep(1)
    print('2s')
    time.sleep(1)
    print('1s')

if __name__ == '__main__':
    t = threading.Thread(target=run, args=('t1',))
    t.setDaemon(True)
    t.start()
    print('end')

通過執行結果可以看出,設置守護線程之后,當主線程結束時,子線程也將立即結束,不再執行。
為了讓守護線程執行結束之后,主線程再結束,我們可以使用join方法,讓主線程等待子線程執行

def run(n):
    print('task', n)
    time.sleep(2)
    print('5s')
    time.sleep(2)
    print('3s')
    time.sleep(2)
    print('1s')


if __name__ == '__main__':
    t = threading.Thread(target=run, args=('t1',))
    t.setDaemon(True)  # 把子線程設置為守護線程,必須在start()之前設置
    t.start()
    t.join()  # 設置主線程等待子線程結束
    print('end')

2.4 資源池

2.4.1 線程池

2.4.1.1 threadpool模塊

threadpool是一個比較老的模塊了,逐漸被其他模塊取代

import threadpool
import time


def sayhello(a):
    print("hello: " + a)
    time.sleep(2)


if __name__ == '__main__':
    global result
    seed = ["a", "b", "c"]
    # 定義一個線程池
    task_pool = threadpool.ThreadPool(5)
    # 創建了要開啟多線程的函數,函數相關參數和回調函數
    requests = threadpool.makeRequests(sayhello, seed)
    # 將所有要運行的請求放到線程池中(參數數量決定任務數量)
    for req in requests:
        task_pool.putRequest(req)
    # 等待所有線程完成后退出
    task_pool.wait()

2.4.1.2 concurrent.futures模塊

concurrent.futures模塊是python3中自帶的模塊,python2.7以上版本也可以安裝使用。
線程池優秀的設計理念在于:他返回的結果并不是執行完畢后的結果,而是futures的對象,這個對象會在未來存儲線程執行完畢的結果,這一點也是異步編程的核心。python為了提高與統一可維護性,多線程多進程和協程的異步編程都是采取同樣的方式。

from concurrent.futures import ThreadPoolExecutor
import time

def sleeper(secs):
    time.sleep(secs)
    print('I slept for {} seconds'.format(secs))
    return secs

with ThreadPoolExecutor(max_workers=3) as executor:
    times = [4, 1, 2]
    start_t = time.time()

    futs = [executor.submit(sleeper, secs) for secs in times]
    for fut in futs:
        print(fut.result())

    print(time.time() - start_t)

# 結果如下
I slept for 1 seconds
I slept for 2 seconds
I slept for 4 seconds
4
1
2

上述例子是直接遍歷future任務來獲取返回結果,可以發現for循環會按順序遍歷所有的future任務,如果有一個任務未執行完會一直堵塞,直到該任務完成后才會繼續遍歷。
可以通過as_completed方法來避免情況發生,提高效率

from concurrent.futures import ThreadPoolExecutor, as_completed
import time

def sleeper(secs):
    time.sleep(secs)
    print('I slept for {} seconds'.format(secs))
    return secs

with ThreadPoolExecutor(max_workers=3) as executor:
    times = [4, 1, 2]
    start_t = time.time()

    futs = [executor.submit(sleeper, secs) for secs in times]
    for fut in as_completed(futs):
        print(fut.result())

    print(time.time() - start_t)


# 結果如下
I slept for 1 seconds
1
I slept for 2 seconds
2
I slept for 4 seconds
4

可以發現,as_completed對集合進行了重新排序,將執行完成的任務放到集合的前面,避免了已經執行完成的任務被前面的任務堵塞,導致效率降低。

還可以使用回調函數來進行后序處理,使用的是同一個線程

from concurrent.futures import ThreadPoolExecutor, as_completed
import time
import threading

def sleeper(secs):
    print("threadPool:" + str(threading.currentThread()))
    time.sleep(secs)
    print('I slept for {} seconds'.format(secs))
    return secs

def call_back(arg):
    print("callback")

with ThreadPoolExecutor(max_workers=3) as executor:
    print("Main:" + str(threading.currentThread()))
    times = [4, 1, 2]
    start_t = time.time()

    futs = [executor.submit(sleeper, secs) for secs in times]
    for fut in as_completed(futs):
        fut.add_done_callback(call_back)
        print(fut.result())

    print(time.time() - start_t)

# 結果如下
I slept for 1 seconds
callback
1
I slept for 2 seconds
callback
2
I slept for 4 seconds
callback
4

2.4.1.3 自定義線程池

import threading
import Queue
import hashlib
import logging
from utils.progress import PrintProgress
from utils.save import SaveToSqlite


class ThreadPool(object):
    def __init__(self, thread_num, args):

        self.args = args
        self.work_queue = Queue.Queue()
        self.save_queue = Queue.Queue()
        self.threads = []
        self.running = 0
        self.failure = 0
        self.success = 0
        self.tasks = {}
        self.thread_name = threading.current_thread().getName()
        self.__init_thread_pool(thread_num)

    # 線程池初始化
    def __init_thread_pool(self, thread_num):
        # 下載線程
        for i in range(thread_num):
            self.threads.append(WorkThread(self))
        # 打印進度信息線程
        self.threads.append(PrintProgress(self))
        # 保存線程
        self.threads.append(SaveToSqlite(self, self.args.dbfile))

    # 添加下載任務
    def add_task(self, func, url, deep):
        # 記錄任務,判斷是否已經下載過
        url_hash = hashlib.new('md5', url.encode("utf8")).hexdigest()
        if not url_hash in self.tasks:
            self.tasks[url_hash] = url
            self.work_queue.put((func, url, deep))
            logging.info("{0} add task {1}".format(self.thread_name, url.encode("utf8")))

    # 獲取下載任務
    def get_task(self):
        # 從隊列里取元素,如果block=True,則一直阻塞到有可用元素為止。
        task = self.work_queue.get(block=False)

        return task

    def task_done(self):
        # 表示隊列中的某個元素已經執行完畢。
        self.work_queue.task_done()

    # 開始任務
    def start_task(self):
        for item in self.threads:
            item.start()

        logging.debug("Work start")

    def increase_success(self):
        self.success += 1

    def increase_failure(self):
        self.failure += 1

    def increase_running(self):
        self.running += 1

    def decrease_running(self):
        self.running -= 1

    def get_running(self):
        return self.running

    # 打印執行信息
    def get_progress_info(self):
        progress_info = {}
        progress_info['work_queue_number'] = self.work_queue.qsize()
        progress_info['tasks_number'] = len(self.tasks)
        progress_info['save_queue_number'] = self.save_queue.qsize()
        progress_info['success'] = self.success
        progress_info['failure'] = self.failure

        return progress_info

    def add_save_task(self, url, html):
        self.save_queue.put((url, html))

    def get_save_task(self):
        save_task = self.save_queue.get(block=False)

        return save_task

    def wait_all_complete(self):
        for item in self.threads:
            if item.isAlive():
                # join函數的意義,只有當前執行join函數的線程結束,程序才能接著執行下去
                item.join()

# WorkThread 繼承自threading.Thread
class WorkThread(threading.Thread):
    # 這里的thread_pool就是上面的ThreadPool類
    def __init__(self, thread_pool):
        threading.Thread.__init__(self)
        self.thread_pool = thread_pool

    #定義線程功能方法,即,當thread_1,...,thread_n,調用start()之后,執行的操作。
    def run(self):
        print (threading.current_thread().getName())
        while True:
            try:
                # get_task()獲取從工作隊列里獲取當前正在下載的線程,格式為func,url,deep
                do, url, deep = self.thread_pool.get_task()
                self.thread_pool.increase_running()

                # 判斷deep,是否獲取新的鏈接
                flag_get_new_link = True
                if deep >= self.thread_pool.args.deep:
                    flag_get_new_link = False

                # 此處do為工作隊列傳過來的func,返回值為一個頁面內容和這個頁面上所有的新鏈接
                html, new_link = do(url, self.thread_pool.args, flag_get_new_link)

                if html == '':
                    self.thread_pool.increase_failure()
                else:
                    self.thread_pool.increase_success()
                    # html添加到待保存隊列
                    self.thread_pool.add_save_task(url, html)

                # 添加新任務,即,將新頁面上的不重復的鏈接加入工作隊列。
                if new_link:
                    for url in new_link:
                        self.thread_pool.add_task(do, url, deep + 1)

                self.thread_pool.decrease_running()
                # self.thread_pool.task_done()
            except Queue.Empty:
                if self.thread_pool.get_running() <= 0:
                    break
            except Exception, e:
                self.thread_pool.decrease_running()
                # print str(e)
                break

2.4.2 進程池

進程池同樣使用multiprocessing模塊

from multiprocessing import Pool
import time,os

def worker(arg):
    print("子進程{}執行中, 父進程{}".format(os.getpid(),os.getppid()))
    time.sleep(2)
    print("子進程{}終止".format(os.getpid()))

if __name__ == "__main__":
    print("本機為",os.cpu_count(),"核 CPU")
    print("主進程{}執行中, 開始時間={}".format(os.getpid(), time.strftime('%Y-%m-%d %H:%M:%S')))
    start = time.time()

    l = Pool(processes=5)
    # 創建子進程實例
    for i in range(10):
        # l.apply(worker,args=(i,))      # 同步執行(Python官方建議廢棄)
        l.apply_async(worker,args=(i,))  # 異步執行

    # 關閉進程池,停止接受其它進程
    l.close()
    # 阻塞進程
    l.join()
    
    stop = time.time()
    print("主進程終止,結束時間={}".format(time.strftime('%Y-%m-%d %H:%M:%S')))
    print("總耗時 %s 秒" % (stop - start))

可以通過異步回調來進行后序操作

from multiprocessing import Process,Pool
import os
import time
import random
 
#子進程任務
def download(f):
    print('__進程池中的進程——pid=%d,ppid=%d'%(os.getpid(),os.getppid()))
    for i in range(3):
        print(f,'--文件--%d'%i)
        time.sleep(random.randint(1, 9))
        # time.sleep(1)
    return {"result": 1, "info": '下載完成!'}
 
#主進程調用回調函數
def alterUser(msg):
    print("----callback func --pid=%d"%os.getpid())
    print("get result:", msg["info"])
 
if __name__ == "__main__":
    p = Pool(3)
    p.apply_async(func=download, args=(1111,), callback=alterUser)
    p.apply_async(func=download, args=(2222,), callback=alterUser)
    p.apply_async(func=download, args=(3333,), callback=alterUser)
    #當func執行完畢后,return的東西會給到回調函數callback
    print("---start----")
    p.close()#關閉進程池,關閉后,p不再接收新的請求。
    p.join()
    print("---end-----")

2.5 Subprocess模塊

python提供了Sunprocess模塊可以在程序執行過程中,調用外部的程序。
如我們可以在python程序中打開記事本,打開cmd,或者在某個時間點關機:

>>> import subprocess
>>> subprocess.Popen(['cmd'])
<subprocess.Popen object at 0x0339F550>
>>> subprocess.Popen(['notepad'])
<subprocess.Popen object at 0x03262B70>
>>> subprocess.Popen(['shutdown', '-p'])


三、資源和鎖(多線程)

線程時進程的執行單元,進程時系統分配資源的最小執行單位,所以在同一個進程中的多線程是共享資源的。
由于線程之間是進行隨機調度,并且每個線程可能只執行n條執行之后,當多個線程同時修改同一條數據時可能會出現臟數據,所以出現了線程鎖,即同一時刻允許一個線程執行操作。線程鎖用于鎖定資源,可以定義多個鎖,像下面的代碼,當需要獨占某一個資源時,任何一個鎖都可以鎖定這個資源,就好比你用不同的鎖都可以把這個相同的門鎖住一樣。
由于線程之間是進行隨機調度的,如果有多個線程同時操作一個對象,如果沒有很好地保護該對象,會造成程序結果的不可預期,我們因此也稱為“線程不安全”。
為了防止上面情況的發生,就出現了鎖(Lock)。

3.1 互斥鎖

此處有一個公共資源,就是n。如果是單線程執行,那么最終n會被扣除到0,但是多線程下資源不安全,最終結果不是0;所以我們需要添加鎖來保證資源安全。

def work():
    global n
    lock.acquire()   # 注銷掉鎖會導致線程不安全
    temp = n
    time.sleep(0.1)
    n = temp-1
    lock.release()  # 注銷掉鎖會導致線程不安全


if __name__ == '__main__':
    lock = threading.Lock()
    n = 100
    l = []
    for i in range(100):
        p = threading.Thread(target=work)
        l.append(p)
        p.start()
    for p in l:
        p.join()

    print(n)

3.2 遞歸鎖(可重入鎖)

RLcok類的用法和Lock類一模一樣,但它支持嵌套,在多個鎖沒有釋放的時候一般會使用RLock類。

import threading
import time

def func1():
    global gl_num
    rlock.acquire()
    gl_num += 1
    time.sleep(1)
    print(gl_num)
    print("enter func1")
    func2()
    rlock.release()  # 直到RLock所有鎖釋放后,其他線程才能獲取鎖

def func2():
    rlock.acquire()  # 線程獲取鎖后可以再次獲取相同的鎖
    print("enter func2")
    rlock.release()

if __name__ == '__main__':
    gl_num = 0
    rlock = threading.RLock()   # 此處換成Lock鎖,則會造成死鎖
    for i in range(5):
        t = threading.Thread(target=func1)
        t.start()

3.3 信號量(BoundedSemaphore類)

互斥鎖同時只允許一個線程更改數據,而Semaphore是同時允許一定數量的線程更改數據,比如加油站有5個油箱,那最多只允許5輛車停放加油,后面的車只能等里面有車出來了才能再進去。

如下程序,有20輛車和5個油箱,每輛車停放3秒完成加油后開走,下一輛再停入加油,直到所有車都完成加油開走。

import threading
import time

def run(n):
    semaphore.acquire()   #加鎖
    print(f'車輛 {n} 停放加油')
    time.sleep(5)
    print(f'車輛 {n} 離開')
    semaphore.release()    #釋放


if __name__== '__main__':
    num=0
    semaphore = threading.BoundedSemaphore(5)   #最多允許5個線程同時運行
    for i in range(20):
        t = threading.Thread(target=run, args={i})
        time.sleep(0.5)
        t.start()
    while threading.active_count() > 1:
        pass
    else:
        print('----------所有車輛完成加油-----------')

3.4 主線程控制其他線程的執行

python線程的事件用于主線程控制其他線程的執行,事件是一個簡單的線程同步對象,其主要提供以下的幾個方法:

  • clear:將flag設置為 False
  • set:將flag設置為 True
  • is_set:判斷是否設置了flag

wait會一直監聽flag,如果沒有檢測到flag就一直處于阻塞狀態。
事件處理的機制:全局定義了一個Flag,當Flag的值為False,那么event.wait()就會阻塞,當flag值為True,那么event.wait()便不再阻塞.

import threading
import time

def lighter():
    count = 0
    event.set()  # 初始者為綠燈
    while True:
        if count % 20 > 10:
            event.clear()  # 紅燈,清除標志位
            print('紅燈亮,停止通行')
        else:
            event.set()  # 綠燈,設置標志位
            print('綠燈亮,可以通行')

        time.sleep(1)
        count += 1


def car(name):
    while True:
        if event.is_set():  # 判斷是否設置了標志位
            print(f'{name} 通行')
            time.sleep(1)
        else:
            print(f'{name} 停止通行')
            event.wait()  # 阻塞直到標志位被設置為True
            print(f'{name} 通行')


if __name__ == "__main__":
    event = threading.Event()
    light = threading.Thread(target=lighter, )
    light.start()

    car = threading.Thread(target=car, args=('司機',))
    car.start()

3.5 線程間的通信

在一個進程中,不同子線程負責不同的任務,t1子線程負責獲取到數據,t2子線程負責把數據保存的本地,那么他們之間的通信使用Queue來完成。因為再一個進程中,數據變量是共享的,即多個子線程可以對同一個全局變量進行操作修改,Queue是加了鎖的安全消息隊列。

import threading
import time
import queue
 
q = queue.Queue(maxsize=5)   #q在t1和t2兩個子線程之間通信共享,一個存入數據,一個使用數據。
def t1(q):
    while 1:
        for i in range(10):
            q.put(i)
def t2(q):
    while not q.empty():
        print('隊列中的數據量:'+str(q.qsize()))
        # q.qsize()是獲取隊列中剩余的數量
        print('取出值:'+str(q.get()))
        # q.get()是一個堵塞的,會等待直到獲取到數據
        print('-----')
        time.sleep(0.1)
t1 = threading.Thread(target=t1,args=(q,))
t2 = threading.Thread(target=t2,args=(q,))
t1.start()
t2.start()


四、相關概念

4.1 GIL 全局解釋器

在非python環境中,如java和c,單核情況下,同時只能有一個任務執行,多核時可以支持多個線程同時執行。但是在python中,無論有多少個核,一個進程中同時只能執行一個線程。究其原因,這就是由于GIL的存在導致的。
GIL的全稱是全局解釋器,來源是python設計之初的考慮,為了數據安全所做的決定。某個線程想要執行,必須先拿到GIL,我們可以把GIL看做是“通行證”,并且在一個python進程之中,GIL只有一個。
拿不到通行證的線程,就不允許進入CPU執行。GIL只在cpython中才有,因為cpython調用的是c語言的原生線程,所以他不能直接操作cpu,而只能利用GIL保證同一時間只能有一個線程拿到數據。而在pypy和jpython中是沒有GIL的。
python在使用多線程的時候,調用的是c語言的原生過程。

示例如下
累加數字到100000000,比較單線程和雙線程的時間。

import threading
import multiprocessing
import time

def tstart(n):
    var = 0
    for i in range(n):
        var += 1

if __name__ == '__main__':
    t1 = threading.Thread(target=tstart, args=(50000000,))
    # t1 = multiprocessing.Process(target=tstart, args=(50000000,))
    t2 = threading.Thread(target=tstart, args=(50000000,))
    # t2 = multiprocessing.Process(target=tstart, args=(50000000,))
    start_time = time.time()
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print("Two thread cost time: %s" % (time.time() - start_time))
    start_time = time.time()
    tstart(100000000)
    print("Main thread cost time: %s" % (time.time() - start_time))

# 結果如下:
# Two thread cost time: 5.507142066955566
# Main thread cost time: 5.363916635513306

這里多線程耗時比單線程耗時要多,原因就是GIL鎖導致的即使多線程也只有一個核在進行運算,線程間的切換導致了耗時增加,起到了反作用。

多進程耗時如下

import threading
import multiprocessing
import time

def tstart(n):
    var = 0
    for i in range(n):
        var += 1

if __name__ == '__main__':
    # t1 = threading.Thread(target=tstart, args=(50000000,))
    t1 = multiprocessing.Process(target=tstart, args=(50000000,))
    # t2 = threading.Thread(target=tstart, args=(50000000,))
    t2 = multiprocessing.Process(target=tstart, args=(50000000,))
    start_time = time.time()
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print("Two thread cost time: %s" % (time.time() - start_time))
    start_time = time.time()
    tstart(100000000)
    print("Main thread cost time: %s" % (time.time() - start_time))

# 結果如下:
# Two thread cost time: 2.793893337249756
# Main thread cost time: 5.109605073928833

可以看到多線程將執行時間減少了將近一半。

也印證了CPU密集型的任務不能用多線程執行,而應該用多進程執行。而IO密集型的任務在CPU可以使用多線程進行操作,效果比較理想

4.2 多線程和多進程

每個進程都包含至少一個線程:主線程,每個主線程可以開啟多個子線程,由于GIL鎖機制的存在,每個進程里的若干個線程同一時間只能有一個被執行;但是使用多進程就可以保證多個線程被多個CPU同時執行。
python編寫多進程能更充分地利用多核CPU的性能,大大提升程序的運行速度。

多進程必須注意的是,要加上

if __name__ == '__main__':
    pass

python多線程和多進程不存在優劣之分,兩者都有著各自的應用環境。

  • 線程幾乎不占資源,系統開銷少,切換速度快,而且同一個進程的多個線程之間能很容易地實現數據共享
  • 而創建進程需要為它分配單獨的資源,系統開銷大,切換速度慢,而且不同進程之間的數據默認是不可共享的。

掌握了兩者各自的特點,才能在實際編程中根據任務需求采取更加適合的方案。

  • 多進程:消耗CPU操作,CPU密集計算。
  • 多線程:適合大量的IO操作。

4.3 線程與進程區別

下面簡單的比較一下線程與進程

  • 進程是資源分配的基本單位,線程是CPU執行和調度的基本單位;
  • 通信/同步方式:
    • 進程
      通信方式:管道,FIFO,消息隊列,信號,共享內存,socket,stream流;
      同步方式:PV信號量,管程
    • 線程
      同步方式:互斥鎖,遞歸鎖,條件變量,信號量
      通信方式:位于同一進程的線程共享進程資源,因此線程間沒有類似于進程間用于數據傳遞的通信方式,線程間的通信主要是用于線程同步。
  • CPU上真正執行的是線程,線程比進程輕量,其切換和調度代價比進程要小;
  • 線程間對于共享的進程數據需要考慮線程安全問題,由于進程之間是隔離的,擁有獨立的內存空間資源,相對比較安全,只能通過上面列出的IPC(Inter-Process Communication)進行數據傳輸;
  • 系統有一個個進程組成,每個進程包含代碼段、數據段、堆空間和棧空間,以及操作系統共享部分 ,有等待,就緒和運行三種狀態;
  • 一個進程可以包含多個線程,線程之間共享進程的資源(文件描述符、全局變量、堆空間等),寄存器變量和棧空間等是線程私有的;
  • 操作系統中一個進程掛掉不會影響其他進程,如果一個進程中的某個線程掛掉而且OS對線程的支持是多對一模型,那么會導致當前進程掛掉;
  • 如果CPU和系統支持多線程與多進程,多個進程并行執行的同時,每個進程中的線程也可以并行執行,這樣才能最大限度的榨取硬件的性能;

4.4 線程和進程的上下文切換

進程切換過程切換牽涉到非常多的東西,寄存器內容保存到任務狀態段TSS,切換頁表,堆棧等。簡單來說可以分為下面兩步:

  1. 頁全局目錄切換,使CPU到新進程的線性地址空間尋址;
  2. 切換內核態堆棧和硬件上下文,硬件上下文包含CPU寄存器的內容,存放在TSS中;

線程運行于進程地址空間,切換過程不涉及到空間的變換,只牽涉到第二步;

4.5 密集型任務類型

python針對不同類型的代碼執行效率也是不同的,任務可以分為I/O密集型和計算密集型,而多線程在切換中又分為I/O切換和時間切換。

  • CPU密集型任務(各種循環處理、計算等),在這種情況下,由于計算工作多,ticks技術很快就會達到閥值,然后出發GIL的釋放與再競爭(多個線程來回切換當然是需要消耗資源的),所以python下的多線程對CPU密集型代碼并不友好。
  • IO密集型任務(文件處理、網絡爬蟲等設計文件讀寫操作),多線程能夠有效提升效率(單線程下有IO操作會進行IO等待,造成不必要的時間浪費,而開啟多線程能在線程A等待時,自動切換到線程B,可以不浪費CPU的資源,從而能提升程序的執行效率)。所以python的多線程對IO密集型代碼比較友好。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容