Python 多进程(上)

前言

在网上看到一篇帖子,讲的是 “为什么在Python里推荐使用多进程而不是多线程?”,大致内容待细细道来,背景是这样的,

1. GIL是什么?

GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。

2. 每个CPU在同一时间只能执行一个线程

在单核CPU下的多线程其实都只是并发,不是并行,并发和并行从宏观上来讲都是同时处理多路请求的概念。但并发和并行又有区别,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。

3、在Python多线程下,每个线程的执行方式:

  1. 获取GIL
  2. 执行代码直到sleep或者是python虚拟机将其挂起。
  3. 释放GIL

可见,某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是”通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。

在Python2.x里,GIL的释放逻辑是当前线程遇见IO操作或者ticks计数达到100(ticks可以看作是Python自身的一个计数器,专门做用于GIL,每次释放后归零,这个计数可以通过 sys.setcheckinterval 来调整),进行释放。
而每次释放GIL锁,线程进行锁竞争、切换线程,会消耗资源。并且由于GIL锁存在,python里一个进程永远只能同时执行一个线程(拿到GIL的线程才能执行),这就是为什么在多核CPU上,python的多线程效率并不高。

而在python3.x中,GIL不使用ticks计数,改为使用计时器(执行时间达到阈值后,当前线程释放GIL),这样对CPU密集型程序更加友好,但依然没有解决GIL导致的同一时间只能执行一个线程的问题,所以效率依然不尽如人意。

请注意:多核多线程比单核多线程更差,原因是单核下多线程,每次释放GIL,唤醒的那个线程都能获取到GIL锁,所以能够无缝执行,但多核下,CPU0释放GIL后,其他CPU上的线程都会进行竞争,但GIL可能会马上又被CPU0拿到,导致其他几个CPU上被唤醒后的线程会醒着等待到切换时间后又进入待调度状态,这样会造成线程颠簸(thrashing),导致效率更低。

每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,所以在python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。所以在这里得出的结论是:多核下,想做并行提升效率,比较通用的方法是使用多进程,能够有效提高执行效率。

那么,什么是多进程和多线程呢?

[En]

So, what is multiprocess and multithreading?

一、多进程

1.1 multiprocessing模块

在 Windows 上要实现跨平台的多进程,我们大多使用 multiprocessing 模块。

p = multiprocessing.Process(target=, args=)'''target   指定的是当进程执行时,需要执行的函数args     是当进程执行时,需要给函数传入的参数注意: args必须是一个tuple, 特别是当函数需要传入一个参数时 (1,)p 代表的是一个多进程,p.is_alive()     判断进程是否存活p.run()          启动进程p.start()        启动进程,他会自动调用run方法,推荐使用startp.join(timeout)  等待子进程结束或者到超时时间p.terminate()    强制子进程退出p.name           进程的名字p.pid            进程的pid'''

1.2 创建子进程

multiprocessing 模块提供了一个 Process 类来代表一个进程对象,下面用一个例子展示如何启动一个子进程并等待其结束:

#!/usr/bin/env python# -*- coding:utf-8 -*-# @Time    : 2018/5/19 20:09# @Author  : zhouyuyao# @File    : 2018-05-19.pyimport multiprocessingimport os# 子进程要执行的代码def run_proc(name):    print('Run child process {0} ({1})...'.format(name, os.getpid()))def parent_process():    print('Parent process {0}.'.format(os.getpid()))    p = multiprocessing.Process(target=run_proc, args=('test',))    print('Child process will start.')    p.start()    p.join()    print('Child process end.')if __name__=='__main__':    parent_process()

结果如下,每次运行都会产生不同的进程ID

Parent process 5232.Child process will start.Run child process test (17912)...Child process end.

创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用 start() 方法启动,这样创建进程比 fork() 还要简单。

join() 方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。

1.3 启动多个进程

#!/usr/bin/env python# -*- coding:utf-8 -*-# @Time    : 2018/5/20 19:51# @Author  : zhouyuyao# @File    : demon2.pyimport multiprocessingimport timeimport osdef worker(args, interval):           # interval 表示间隔    print("start worker {0}({1})".format(args, os.getpid()))    time.sleep(interval)    print("end worker {0}({1})".format(args, os.getpid()))def main():    print("start main")    print('Parent process {0}.'.format(os.getpid()))    p1 = multiprocessing.Process(target=worker, args=(1,1))    print('Parent process {0}.'.format(os.getpid()))    p2 = multiprocessing.Process(target=worker, args=(2,2))    print('Parent process {0}.'.format(os.getpid()))    p3 = multiprocessing.Process(target=worker, args=(3,3))    p1.start()    p2.start()    p3.start()    print("end main")if __name__ == '__main__':    main()''' 结果start mainParent process 6180.                 # 此处得到的进程ID一样,原因暂不明Parent process 6180.Parent process 6180.end mainstart worker 1(17596)start worker 2(5872)start worker 3(19228)end worker 1(17596)end worker 2(5872)end worker 3(19228)'''

1.4 判断子进程是否存在

我们也可以获取运行代码机器的CPU核数,以及查看哪个进程的子进程正在运行,如下所示

#!/usr/bin/env python# -*- coding:utf-8 -*-# @Time    : 2018/5/20 19:51# @Author  : zhouyuyao# @File    : demon2.pyimport multiprocessingimport timeimport osdef worker(args, interval):           # interval 表示间隔    print("start worker {0}({1})".format(args, os.getpid()))    time.sleep(interval)    print("end worker {0}({1})".format(args, os.getpid()))def main():    print("start main")    print('Parent process {0}.'.format(os.getpid()))    p1 = multiprocessing.Process(target=worker, args=(1,1))    print('Parent process {0}.'.format(os.getpid()))    p2 = multiprocessing.Process(target=worker, args=(2,2))    print('Parent process {0}.'.format(os.getpid()))    p3 = multiprocessing.Process(target=worker, args=(3,3))    p1.start()    p1.join(timeout=0.5)    p2.start()    p3.start()    print("the number of CPU is: {0}".format(multiprocessing.cpu_count()))    '''cpu_count(self):Returns the number of CPUs in the system'''    for p in multiprocessing.active_children():       """active_children :rtype: list[multiprocessing.Process]"""       print("The name of active children is: {0}, pid is: {1} is alive".format(p.name, p.pid))    print("end main")if __name__ == '__main__':    main()

运行后,我们得到了以下结果

[En]

After running, we get the following results

start mainParent process 22560.Parent process 22560.Parent process 22560.start worker 1(20788)the number of CPU is: 4The name of active children is: Process-3, pid is: 23508 is aliveThe name of active children is: Process-2, pid is: 6440 is aliveThe name of active children is: Process-1, pid is: 20788 is aliveend mainstart worker 2(6440)start worker 3(23508)end worker 1(20788)end worker 2(6440)end worker 3(23508)

1.5 进程同步(进程锁Lock)

对于一些互斥的资源,需要进程间互斥才能访问。否则,对资源的访问将被阻止,或者最终结果将被混淆。对于标准输出资源,如果多个资源属于相同的输出信息,则可能会混淆输出信息。因此,需要锁定以避免对资源的互斥访问。

[En]

For some mutually exclusive resources, inter-process mutual exclusion is required to access. Otherwise, access to resources will be blocked, or the final result will be confused. For standard output resources, if multiple resources belong to the same output information, the output information may be confused. So locks are needed to avoid mutually exclusive access to resources.

当多个进程需要访问共享资源的时候,Lock可以用来避免访问的冲突。

#!/usr/bin/env python# -*- coding:utf-8 -*-# @Time    : 2018/5/20 21:31# @Author  : zhouyuyao# @File    : demon4.pyimport multiprocessingdef worker_with(lock, f):    with lock:        with open('file.txt',"a+") as fs:            fs.write('Lock acquired via with\n')        # fs.close()def worker_no_with(lock, f):    lock.acquire()    '''    This method blocks until the lock is unlocked, then sets it to            locked and returns True.    '''    try:        # fs = open(f, "a+")        with open('file.txt',"a+") as fs:            fs.write('Lock acquired directly\n')    finally:        lock.release()        """Release a lock.        When the lock is locked, reset it to unlocked, and return.        If any other coroutines are blocked waiting for the lock to become        unlocked, allow exactly one of them to proceed.        When invoked on an unlocked lock, a RuntimeError is raised.        There is no return value.        """def main():    f = "file.txt"    lock = multiprocessing.Lock()    w = multiprocessing.Process(target=worker_with, args=(lock, f))    nw = multiprocessing.Process(target=worker_no_with, args=(lock, f))    w.start()     # p.start() 启动进程,他会自动调用run方法,推荐使用start    nw.start()    w.join()      # p.join(timeout) 等待子进程结束或者到超时时间    '''    join()代表启动多进程,但是阻塞并发运行,一个进程执行结束后再执行第二个进程。    可以给其设置一个timeout值比如 join(5)代表5秒后无论当前进程是否结果都继续并发执行第二个进程。    '''    nw.join()if __name__ == "__main__":    main()

结果会产生一个file.txt文件,内容如下(以下为运行多次的结果)

Python 多进程(上)

1.6 进程间共享数据

几个进程之间的都拥有自己独立的命名空间和地址空间,无法通过一些全局变量来实现,multiprocessing提供了一些特殊的函数来实现共享变量。

进程间共享变量(Value、Array、Manager)

Value、Array 是通过共享内存的方式共享数据 ,Manager 则是通过共享进程的方式共享数据

1)Value、Array

#!/usr/bin/env python# -*- coding:utf-8 -*-# @Time    : 2018/5/20 23:10# @Author  : zhouyuyao# @File    : demon7.pyfrom multiprocessing import Process,Value,Arraydef f(n,a):        n.value = 3.1415926        for i in range(len(a)):                a[i] = -a[i]def main():    num = Value('d', 0.0)    arr = Array('i', range(10))    ''' Value() 和 Array() 都有两个参数第一个参数代表存放的值的类型,第二个参数代表其值 '''    p = Process(target=f, args=(num, arr))    p.start()    p.join()    print(num.value)    print(arr[:])if __name__ == "__main__":    main()''' 结果3.1415926[0, -1, -2, -3, -4, -5, -6, -7, -8, -9]'''

2)manager

Manager管理的共享数据类型有:Value、Array、dict、list、Lock、Semaphore 等等,同时 Manager 还可以共享类的实例对象。

这个方式支持的类型更多,灵活性更大,但是速度要慢于Value,Array。

#!/usr/bin/env python# -*- coding:utf-8 -*-# @Time    : 2018/5/20 23:13# @Author  : zhouyuyao# @File    : demon8.pyfrom multiprocessing import Process,Managerdef f(d,l):        d[1] = '1'        d['2'] = 2        d[0.25] = None        l.reverse()          '''reverse() 函数用于反向列表中元素。        该方法没有返回值,但是会对列表的元素进行反向排序'''def main():    manager = Manager()    d = manager.dict()    l = manager.list(range(10))    p = Process(target=f, args=(d, l))    p.start()    p.join()    print(d)    print(l)if __name__ == "__main__":    main()''' 结果{1: '1', '2': 2, 0.25: None}[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]'''

参考资料

  1. ​​ 为什么在 Python 里推荐使用多进程而不是多线程?

  2. ​​

浅谈 python multiprocessing(多进程)下如何共享变量

​​ python之多进程multiprocessing

Original: https://blog.51cto.com/shaoniana/5637802
Author: 归来仍少年
Title: Python 多进程(上)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/500210/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球