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(['notepad'])

>>> 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密集型代码比较友好。

你可能感兴趣的:(Python多线程和多进程)