第16章 协程-补充内容

<!-- TOC -->

<!-- /TOC -->

多进程VS多线程

多进程

多进程就是利用 CPU 的多核优势,在同一时间并行地执行多个任务,可以大大提高执行效率。

multiprocessing 模块

例子:

from multiprocessing import Process


def fun1(name):
    print('测试%s多进程' % name)


if __name__ == '__main__':

    process_list = []
    for i in range(5):  # 开启5个子进程执行fun1函数
        p = Process(target=fun1, args=('Python', ))  # 实例化进程对象
        p.start()
        process_list.append(p)

    for i in process_list:
        p.join()

    print('结束测试')

返回结果:

测试Python多进程
测试Python多进程
测试Python多进程
测试Python多进程
测试Python多进程
结束测试


类继承方式:

from multiprocessing import Process


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

    def run(self):
        print('测试%s多进程' % self.name)


if __name__ == '__main__':
    process_list = []
    for i in range(5):  # 开启5个子进程执行fun1函数
        p = MyProcess('Python')  # 实例化进程对象
        p.start()
        process_list.append(p)

    for i in process_list:
        p.join()

    print('结束测试')
多进程通讯共享数据

多进程之间可以通过管道,队列,Managers来实现通讯和共享数据

队列
from multiprocessing import Process, Queue


def fun1(q, i):
    print('子进程%s 开始put数据' % i)
    q.put('我是%s 通过Queue通信' % i)


if __name__ == '__main__':
    q = Queue()

    process_list = []
    for i in range(3):
         # 注意args里面要把q对象传给我们要执行的方法,这样子进程才能和主进程用Queue来通信
        p = Process(target=fun1, args=(
            q,
            i,
        ))
        p.start()
        process_list.append(p)

    for i in process_list:
        p.join()

    print('主进程获取Queue数据')
    print(q.get())
    print(q.get())
    print(q.get())
    print('结束测试')

运行结果:

子进程0 开始put数据
子进程1 开始put数据
子进程2 开始put数据
主进程获取Queue数据
我是0 通过Queue通信
我是1 通过Queue通信
我是2 通过Queue通信
结束测试
管道
from multiprocessing import Process, Pipe


def fun1(conn):
    print('子进程发送消息:')
    conn.send('你好主进程')
    print('子进程接受消息:')
    print(conn.recv())
    conn.close()


if __name__ == '__main__':
    conn1, conn2 = Pipe()  # 关键点,pipe实例化生成一个双向管
    p = Process(target=fun1, args=(conn2, ))  # conn2传给子进程
    p.start()
    print('主进程接受消息:')
    print(conn1.recv())
    print('主进程发送消息:')
    conn1.send("你好子进程")
    p.join()
    print('结束测试')

返回结果:

主进程接受消息:
子进程发送消息:
子进程接受消息:
你好主进程
主进程发送消息:
你好子进程
结束测试
Managers

Queue和Pipe只是实现了数据交互,并没实现数据共享,即一个进程去更改另一个进程的数据。那么要用到Managers

from multiprocessing import Process, Manager


def fun1(dic, lis, index):

    dic[index] = 'a'
    dic['2'] = 'b'
    lis.append(index)  # [0,1,2,3,4,0,1,2,3,4,5,6,7,8,9]
    #print(l)


if __name__ == '__main__':
    with Manager() as manager:
        dic = manager.dict()  # 注意字典的声明方式,不能直接通过{}来定义
        l = manager.list(range(5))  # [0,1,2,3,4]

        process_list = []
        for i in range(10):
            p = Process(target=fun1, args=(dic, l, i))
            p.start()
            process_list.append(p)

        for res in process_list:
            res.join()
        print(dic)
        print(l)

返回结果:

{0: 'a', '2': 'b', 2: 'a', 1: 'a', 3: 'a', 5: 'a', 4: 'a', 9: 'a', 6: 'a', 8: 'a', 7: 'a'}
[0, 1, 2, 3, 4, 0, 2, 1, 3, 5, 4, 9, 6, 8, 7]

可以看到主进程定义了一个字典和一个列表,在子进程中,可以添加和修改字典的内容,在列表中插入新的数据,实现进程间的数据共享,即可以共同修改同一份数据

进程池

进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。就是固定有几个进程可以使用。

进程池中有两个方法:
apply:同步,一般不使用
apply_async:异步

异步:

from multiprocessing import Process, Pool
import os, time, random


def fun1(name):
    print('Run task %s (%s)...' % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()
    print('Task %s runs %0.2f seconds.' % (name, (end - start)))


if __name__ == '__main__':
    pool = Pool(5)  # 创建一个5个进程的进程池

    for i in range(10):
        pool.apply_async(func=fun1, args=(i, ))

    pool.close()
    pool.join()
    print('结束测试')

运行结果是:


Run task 0 (13892)...
Run task 1 (15796)...
Run task 2 (6648)...
Run task 3 (9260)...
Run task 4 (1872)...
Task 3 runs 0.79 seconds.
Run task 5 (9260)...
Task 5 runs 0.10 seconds.
Run task 6 (9260)...
Task 2 runs 1.43 seconds.
Run task 7 (6648)...
Task 4 runs 2.44 seconds.
Task 1 runs 2.44 seconds.
Run task 8 (1872)...
Run task 9 (15796)...
Task 0 runs 2.58 seconds.
Task 9 runs 0.14 seconds.
Task 8 runs 0.88 seconds.
Task 6 runs 2.55 seconds.
Task 7 runs 2.07 seconds.
结束测试

对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。

另外一个例子:

from multiprocessing import Manager, Pool
import os, time


def reader(q):
    print("reader 启动(%s),父进程为(%s)" % (os.getpid(), os.getpid()))
    for i in range(q.qsize()):
        print("reader 从Queue获取到消息:%s" % q.get(True))


def writer(q):
    print("writer 启动(%s),父进程为(%s)" % (os.getpid(), os.getpid()))
    for i in "itcast":
        q.put(i)


if __name__ == "__main__":
    print("(%s)start" % os.getpid())

    q = Manager().Queue()  # 使用Manager中的Queue
    po = Pool()

    po.apply_async(writer, (q, ))
    time.sleep(1)
    po.apply_async(reader, (q, ))

    po.close()
    po.join()

    print("(%s)End" % os.getpid())

运行结果是:

(9516)start
writer 启动(10852),父进程为(10852)
reader 启动(13392),父进程为(13392)
reader 从Queue获取到消息:i
reader 从Queue获取到消息:t
reader 从Queue获取到消息:c
reader 从Queue获取到消息:a
reader 从Queue获取到消息:s
reader 从Queue获取到消息:t
(9516)End

同步:

from multiprocessing import Process, Pool
import os, time, random


def fun1(name):
    print('Run task %s (%s)...' % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()
    print('Task %s runs %0.2f seconds.' % (name, (end - start)))


if __name__ == '__main__':
    pool = Pool(5)  # 创建一个5个进程的进程池

    for i in range(10):
        pool.apply(func=fun1, args=(i, ))

    pool.close()
    pool.join()
    print('结束测试')

运行结果:

Run task 0 (12996)...
Task 0 runs 0.96 seconds.
Run task 1 (5704)...
Task 1 runs 2.68 seconds.
Run task 2 (6808)...
Task 2 runs 1.31 seconds.
Run task 3 (3020)...
Task 3 runs 0.85 seconds.
Run task 4 (16980)...
Task 4 runs 2.35 seconds.
Run task 5 (12996)...
Task 5 runs 1.25 seconds.
Run task 6 (5704)...
Task 6 runs 2.43 seconds.
Run task 7 (6808)...
Task 7 runs 1.79 seconds.
Run task 8 (3020)...
Task 8 runs 0.72 seconds.
Run task 9 (16980)...
Task 9 runs 0.77 seconds.
结束测试

多线程 GIL

GIL是什么

GIL 是python的全局解释器锁,同一进程中假如有多个线程运行,一个线程在运行python程序的时候会霸占python解释器(加了一把锁即GIL),使该进程内的其他线程无法运行,等该线程运行完后其他线程才能运行。如果线程运行过程中遇到耗时操作,则解释器锁解开,使其他线程运行。所以在多线程中,线程的运行仍是有先后顺序的,并不是同时进行。

多进程中因为每个进程都能被系统分配资源,相当于每个进程有了一个python解释器,所以多进程可以实现多个进程的同时运行,缺点是进程系统资源开销大

GIL例子
import time


def decrement(n):
    while n > 0:
        n -= 1


start = time.time()
decrement(100000000)
cost = time.time() - start
print(cost)  # 4.968633651733398

使用threading多线程模块:

import time
import threading


def decrement(n):
    while n > 0:
        n -= 1


start = time.time()

t1 = threading.Thread(target=decrement, args=[50000000])
t2 = threading.Thread(target=decrement, args=[50000000])

t1.start()  # 启动线程,执行任务
t2.start()  # 同上

t1.join()  # 主线程阻塞,直到t1执行完成,主线程继续往后执行
t2.join()  # 同上

cost = time.time() - start
print(cost)  # 4.755946159362793

按理来说,两个线程同时并行地运行在两个 CPU 之上,时间应该减半才对,现在不减反增。

是什么原因导致多线程不快反慢的呢?

原因就在于 GIL ,在 Cpython 解释器(Python语言的主流解释器)中,有一把全局解释锁(Global Interpreter Lock)

在解释器解释执行 Python 代码时,先要得到这把锁,意味着,任何时候只可能有一个线程在执行代码,其它线程要想获得 CPU 执行代码指令,

就必须先获得这把锁,如果锁被其它线程占用了,那么该线程就只能等待,直到占有该锁的线程释放锁才有执行代码指令的可能。

因此,这也就是为什么两个线程一起执行反而更加慢的原因,因为同一时刻,只有一个线程在运行,其它线程只能等待,即使是多核CPU,

也没办法让多个线程「并行」地同时执行代码,只能是交替执行,因为多线程涉及到上线文切换、锁机制处理(获取锁,释放锁等),

所以,多线程执行不快反慢。

GIL释放

什么时候 GIL 被释放呢?

当一个线程遇到 I/O 任务时,将释放GIL。

计算密集型(CPU-bound)线程执行 100 次解释器的计步(ticks)时(计步可粗略看作 Python 虚拟机的指令),也会释放 GIL。

可以通过 sys.setcheckinterval()设置计步长度,sys.getcheckinterval() 查看计步长度。相比单线程,这些多是多线程带来的额外开销

为什么这样设计

CPython 解释器为什么要这样设计?

多线程是为了适应现代计算机硬件高速发展充分利用多核处理器的产物,通过多线程使得 CPU 资源可以被高效利用起来,

但是多线程有个问题,怎么解决共享数据的同步、一致性问题,因为,对于多个线程访问共享数据时,可能有两个线程同时修改一个数据情况,

如果没有合适的机制保证数据的一致性,那么程序最终导致异常,

所以,Python之父就搞了个全局的线程锁,不管你数据有没有同步问题,反正一刀切,上个全局锁,保证数据安全。

这也就是多线程鸡肋的原因,因为它没有细粒度的控制数据的安全,而是用一种简单粗暴的方式来解决。

能不能去掉GIL

那么把 GIL 去掉可行吗?

去掉GIL的 Python 在单线程条件下执行效率将近慢了2倍。

threading模块
import threading
from time import sleep


class Mythead(threading.Thread):
    def run(self):
        for i in range(3):
            sleep(1)  # 挂起1s
            # self.name是该线程的名字,默认会分配一个形如“Thread-N”的名字,其中 N 是一个十进制数
            print(u'线程:%s,索引%s' % (self.name, i))


if __name__ == '__main__':
    for i in range(3):
        t = Mythead()
        t.start()

运行结果:

线程:Thread-2,索引0
线程:Thread-3,索引0
线程:Thread-4,索引0
线程:Thread-2,索引1
线程:Thread-3,索引1
线程:Thread-4,索引1
线程:Thread-2,索引2
线程:Thread-3,索引2
线程:Thread-4,索引2

从上面的运行结果来看,多线程程序的执行顺序是确定的。

import threading
import logging
from time import sleep

#配置logging,以“模式名/线程名字(10个字符)/信息”方式输出。
logging.basicConfig(level=logging.DEBUG,
                    format='[%(levelname)s](%(threadName)-10s) %(message)s')


def foo():
    logging.debug('Starting...')
    sleep(2)
    logging.debug('Exiting...')


threading.Thread(name='mythread', target=foo).start()
threading.Thread(target=foo).start()

返回结果是:

[DEBUG](mythread  ) Starting...
[DEBUG](Thread-2  ) Starting...
[DEBUG](mythread  ) Exiting...
[DEBUG](Thread-2  ) Exiting...

比较有趣的是, multiprocessing中提供了一个dummy module, 以multiprocessing API的方式提供了对threading模块的封装,

这就意味着使用如下代码时:

multiprocessing.dummy 模块

multiprocessing.dummy 模块与 multiprocessing 模块的区别:
dummy 模块是多线程,而 multiprocessing 是多进程, api 都是通用的。

from multiprocessing.dummy import Pool, Process
Pool和Process的底层其实都是使用threading的实现(即ThreadPool和Thread),

# from multiprocessing import Pool
from multiprocessing.dummy import Pool as ThreadPool
import time
from urllib.request import urlopen

urls = [
    'http://www.baidu.com',
    'http://home.baidu.com/',
    'http://tieba.baidu.com/',
]
start = time.time()
results = map(urlopen, urls)
print('Normal:', time.time() - start)

start2 = time.time()  # 开8个 worker,没有参数时默认是 cpu 的核心数
pool = ThreadPool(processes=8)
results2 = pool.map(urlopen, urls)
pool.close()
pool.join()

print('Thread Pool:', time.time() - start2)

两者关系

多进程适合在CPU密集操作(cpu操作指令比较多,如位多的的浮点运算)。
多线程适合在IO密性型操作(读写数据操作比多的的,比如爬虫)

多进程分析详解

深入Python多进程编程基础
深入Python多进程通信原理与实战


王知晓
37 声望19 粉丝

知晓知晓,上知天文下晓地理