Python 多线程、多进程 (三)之 线程进程对比、多进程
Python 多线程、多进程 (一)之 源码执行流程、GIL
Python 多线程、多进程 (二)之 多线程、同步、通信
Python 多线程、多进程 (三)之 线程进程对比、多线程
一、多线程与多进程的对比
在之前简单的提过,CPython中的GIL使得同一时刻只能有一个线程运行,即并发执行。并且即使是多核CPU,GIL使得同一个进程中的多个线程也无法映射到多个CPU上运行,这么做最初是为了安全着想,慢慢的也成为了限制CPython性能的问题。
一个线程想要执行,就必须得到GIL,否则就不能拿到CPU资源。但是也不是说一个线程在拿到CPU资源后就一劳永逸,在执行的过程中GIL可能会释放并被其他线程获取,所以说其它的线程会与本线程竞争CPU资源,线程是抢占式执行的。具体可在 understand GIL中看到,[传送门]。
多线程在python2中:当一个线程进行I/O的时候会释放锁,另外当ticks计数达到100(ticks可以看作是Python自身的一个计数器,也可对比着字节码指令理解,专门做用于GIL,每次释放后归零,这个计数可以通过 sys.setcheckinterval 来调整)。锁释放之后,就涉及到线程的调度,线程的锁进行,线程的切换。这是会消耗CPU资源,因此会造成程序性能问题和等待时延。另外由于线程共享内存的问题,没有进程安全性高。
但是对于多进程,GIL就无法限制,多个进程可以再多个CPU上运行,充分利用多核优势。事情往往是相对的,虽然可以充分利用多核优势,但是进程之的创建和调度却比线程的代价更高。
所以选择多线程还是多进程,主要还是看怎样权衡代价,什么样的情况。
1、CPU密集代码
下面来利用斐波那契数列模拟CPU密集运算。
def fib(n):
# 求斐波那契数列的第n个值
if n<=2:
return 1
return fib(n-1)+fib(n-2)
<1>、多进程
打印第25到35个斐波那契数,并计算程序运行时间
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from concurrent.futures import ProcessPoolExecutor
def fib(n):
if n<=2:
return 1
return fib(n-1)+fib(n-2)
if __name__ == "__main__":
with ProcessPoolExecutor(3) as executor: # 使用进程池控制 每次执行3个进程
all_task = [executor.submit(fib, (num)) for num in range(25,35)]
start_time = time.time()
for future in as_completed(all_task):
data = future.result()
print("exe result: {}".format(data))
print("last time is: {}".format(time.time()-start_time))
# 输出
exe result: 75025
exe result: 121393
exe result: 196418
exe result: 317811
exe result: 514229
exe result: 832040
exe result: 1346269
exe result: 2178309
exe result: 3524578
exe result: 5702887
last time is: 4.457437038421631
输出结果,每次打印三个exe result,总重打印十个结果,多进程运行时间为4.45秒
<2>、多线程
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from concurrent.futures import ProcessPoolExecutor
def fib(n):
if n<=2:
return 1
return fib(n-1)+fib(n-2)
if __name__ == "__main__":
with ThreadPoolExecutor(3) as executor: # 使用线程池控制 每次执行3个线程
all_task = [executor.submit(fib, (num)) for num in range(25,35)]
start_time = time.time()
for future in as_completed(all_task):
data = future.result()
print("exe result: {}".format(data))
print("last time is: {}".format(time.time()-start_time))
# 输出
exe result: 121393
exe result: 75025
exe result: 196418
exe result: 317811
exe result: 514229
exe result: 832040
exe result: 1346269
exe result: 2178309
exe result: 3524578
exe result: 5702887
last time is: 7.3467772006988525
最终程序运行时间为7.34秒
程序的执行之间与计算机的性能有关,每天计算机的执行时间都会有差异。从上述结果中看显然多线程比多进程要耗费时间。这就是因为对于密集代码(密集运算,循环语句等),tick计数很快达到100,GIL来回的释放竞争,线程之间频繁切换,所以对于密集代码的执行中,多线程性能不如对进程。
2、I/O密集代码
一个线程在I/O阻塞的时候,会释放GIL,挂起,然后其他的线程会竞争CPU资源,涉及到线程的切换,但是这种代价与较高时延的I/O来说是不足为道的。
下面用sleep函数模拟密集I/O
def random_sleep(n):
time.sleep(n)
return n
<1>、 多进程
def random_sleep(n):
time.sleep(n)
return n
if __name__ == "__main__":
with ProcessPoolExecutor(5) as executor:
all_task = [executor.submit(random_sleep, (num)) for num in [2]*30]
start_time = time.time()
for future in as_completed(all_task):
data = future.result()
print("exe result: {}".format(data))
print("last time is: {}".format(time.time()-start_time))
# 输出
exe result: 2
exe result: 2
......(30个)
exe result: 2
exe result: 2
last time is: 12.412866353988647
每次打印5个结果,总共二十个打印结果,多进程运行时间为12.41秒
<2>、多线程
def random_sleep(n):
time.sleep(n)
return n
if __name__ == "__main__":
with ThreadPoolExecutor(5) as executor:
all_task = [executor.submit(random_sleep, (num)) for num in [2]*30]
start_time = time.time()
for future in as_completed(all_task):
data = future.result()
print("exe result: {}".format(data))
print("last time is: {}".format(time.time()-start_time))
# 输出
exe result: 2
exe result: 2
......(30个)
exe result: 2
exe result: 2
last time is: 12.004231214523315
I/O密集多线程情况下,程序的性能较多进程有了略微的提高。IO密集型代码(文件处理、网络爬虫等),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO密集型代码比较友好。
3、线程进程对比
- CPU密集型代码(各种循环处理、计数等等),多线程性能不如多进程。
- I/O密集型代码(文件处理、网络爬虫等),多进程不如多线程。
二、多进程
在python 进程、线程 (一)已经有简单的进程介绍。
不过与多线程编程相比,最需要注意的是这里多进程由并发执行变成了真正意义上的并行执行。
1、fork()调用
Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程,但是还是要有Unix/Linux系统支持,windows没有系统调用fork(),可以在本地虚拟机或者云服务器尝试,默认liunx发行版中是有python2.X的。
情况一:
import os
print("Lanyu") # 只打印一次
pid = os.fork()
if pid == 0:
print('子进程 {} ,父进程是: {}.' .format(os.getpid(), os.getppid()))
else:
print('我是父进程:{}.'.format(os.getpid())
# 输出
Lanyu
我是父进程:2993
子进程2994,父进程2993
fork()调用复制了一个进程,然后程序中就有两个进程,父进程的pid不为0,所以先打印子进程2994,父进程2993。然后子进程pid=0,打印我是父进程:2993。这里的Lanyu打印一次
情况二:
import os
pid = os.fork()
print("Lanyu") # 这里打印两次
if pid == 0:
print('子进程 {} ,父进程是: {}.' .format(os.getpid(), os.getppid()))
else:
print('我是父进程:{}.'.format(os.getpid())
# 输出
Lanyu
我是父进程:2993
Lanyu
子进程2994,父进程2993
这里的Lanyu打印两次是因为,由于fork()函数调用之后,程序立即成成一个子进程,主进程打印一次,子进程再打印一次。因此这里的Lanyu打印两次。
情况三:
还记得操作系统专业课的时候,老师讲的一道考研题
int main{
fork();
fork();
fork():
printf('process')
return 0;
}
三次fork(),问此程序最终打印几个次process,关键在于fork()函数的用途,每一次都会复制一次进程,则最终,一个父进程被复制成8个进程,打印8次。
2、python多进程
虽然python中没有提供直接的进程调用函数,但是标准库中的模块提供能更多更方便的选择。 ProcessPoolExecutor进程池,与 multiprocessing标准的多进程模块。其实ProcessPoolExecutor也是对multiprocessing的封装调用,并且与ThreadPoolExecutor线程池提供的接口类似。而multiprocessing则更加底层。
<1>、进程编程
import time
import multiprocessing
def get_html(n):
time.sleep(n)
print("sub_progress success")
return n
if __name__ == "__main__":
progress = multiprocessing.Process(target=get_html, args=(2,))
print(progress.pid) # 打印结果为None,因为这个时候进程还未开启
progress.start() # 进程开启
print(progress.pid)
progress.join()
print("main progress end")
# 输出
None
5056
sub_progress success
main progress end
<2>、使用进程池
import time
import multiprocessing
def get_html(n):
time.sleep(n)
print("sub_progress success")
return n
if __name__ == "__main__":
#使用进程池
pool = multiprocessing.Pool(multiprocessing.cpu_count()) # 可以指明进程数,默认等于CPU数
result = pool.apply_async(get_html, args=(3,))
#等待所有任务完成
pool.close()
pool.join()
print(result.get())
# 输出
sub_progress success
3
<3>、imap 接口
实例一:
import time
import multiprocessing
def get_html(n):
time.sleep(n)
print("sub_progress success")
return n
if __name__ == "__main__":
# imap
for result in pool.imap(get_html, [1,5,3]):
print("{} sleep success".format(result))
# 输出
sub_progress success
1 sleep success
sub_progress success
sub_progress success
5 sleep success
3 sleep success
imap有点像python提供的内置函数map,讲[1,5,3]这个列表中的值一个一个传递给get_html函数对象,并按照传值的先后顺序,一一执行输出进程结果。
实例二:
import multiprocessing
import time
def get_html(n):
time.sleep(n)
print("sub_progress success")
return n
if __name__ == "__main__":
pool = multiprocessing.Pool(multiprocessing.cpu_count()) # 可以进程数,不过最好是等于CPU数,这里也是进程数
for result in pool.imap_unordered(get_html, [1,5,3]):
print("{} sleep success".format(result))
# 输出
sub_progress success
1 sleep success
sub_progress success
3 sleep success
sub_progress success
5 sleep success
与imap方法不同的是imap_unordered方法,imap_unordered是按照进程的执行完成的先后顺序,打印进程执行结果,而不是依照列表中的先后顺序。可以依照需要调用。
划重点 多进程编程中,需要在__name__ == "main"下编写
更多API参考:[传送门]
3、进程通信
<1>、共享变量通信
类比线程之间的通信,首先想到的就是共享变量通信。但是在多进程中,一个进程都有自的隔离区,导致变量不能共享。
情况一:
def producer(a):
a += 100
time.sleep(2)
def consumer(a):
time.sleep(2)
print(a)
if __name__ == "__main__":
a = 1
my_producer = Process(target=producer, args=(a,))
my_consumer = Process(target=consumer, args=(a,))
my_producer.start()
my_consumer.start()
my_producer.join()
my_consumer.join()
# 输出
1
结果进程没有共享变量。
但是Python的标准模块提供了Manager()在内存中划出一块单独的内存区,供所有的进程使用,共享变量。
情况二:
from multiprocessing import Process, Manager
def add_data(p_dict, key, value):
p_dict[key] = value
if __name__ == "__main__":
progress_dict = Manager().dict()
first_progress = Process(target=add_data, args=(progress_dict, "666", 666)) # 更新progress_dict
second_progress = Process(target=add_data, args=(progress_dict, "999", 999)) # 更新progress_dict
first_progress.start()
second_progress.start()
first_progress.join()
second_progress.join()
print(progress_dict)
# 打印结果
{'666': 666, '999': 999} # 实现了变量的共享
在Manager中还可以有其它的数据结构,例如列表数组等可共享使用。
因此,在使用多进程编程的时候,如果像情况二共享全局变量,就仍旧需要加锁实现进程同步。
<2>、Queue队列通信
在multiprocessing模块中有Queue类安全的队列,也可以实现通信,不过在这种情况下无法联通线程池。
import time
from multiprocessing import Process, Queue, Pool, Manager
def producer(queue):
queue.put("a")
time.sleep(2)
def consumer(queue):
time.sleep(2)
data = queue.get()
print(data)
if __name__ == "__main__":
queue = Queue(10) # 使用普通的Queue
pool = Pool(2)
pool.apply_async(producer, args=(queue,))
pool.apply_async(consumer, args=(queue,))
pool.close()
pool.join()
# 无输出
想要使用进程池又实现消息队列通信就需要用到Manager管理者
import time
from multiprocessing import Process, Queue, Pool, Manager
def producer(queue):
queue.put("a")
time.sleep(2)
def consumer(queue):
time.sleep(2)
data = queue.get()
print(data)
if __name__ == "__main__":
queue = Manager().Queue(10) # 在使用Manger的时候需要先将Manager实例化在调用Queue
pool = Pool(2)
pool.apply_async(producer, args=(queue,))
pool.apply_async(consumer, args=(queue,))
pool.close()
pool.join()
# 输出
正常打印字符a
<3>、pipe管道通信
pipe也用于进程通信,从功能上说,提供的接口应该是queue的子集。但是queue为了更好的控制,所以内部加了很多的锁,而pipe在两个进程通信的时候性能会比queue更好一些。
def producer(pipe):
pipe.send("Lanyu")
def consumer(pipe):
print(pipe.recv())
if __name__ == "__main__":
recevie_pipe, send_pipe = Pipe()
#pipe只能适用于两个进程
my_producer= Process(target=producer, args=(send_pipe, ))
my_consumer = Process(target=consumer, args=(recevie_pipe,))
my_producer.start()
my_consumer.start()
my_producer.join()
my_consumer.join()
# 输出
Lanyu
三、总结
最开始为了引出GIL,简单输了python源码的执行流程,也是先编译成字节码再执行。在CPython中,为了数据完整性和状态同步才有GIL,GIL同样使得多线程不能利用CPU多核优势,所以性能低部分是因为GIL。
线程需要加上GIL才能获取CPU资源,才能执行。线程通信的时候,可以用消息队列Queue和全局变量,但是对于全局变量这种通信方式,在执行字节码一定数量之后,会释放GIL,线程抢占式执行同样导致变量的混乱,所以我们加上了用户级别的互斥锁Lock,或者迭代锁Rlock保证了线程的状态同步。condition帮我们实现了线程的复杂通信,而semaphore信号量,使得我们在多个线程的情况下,控制并发线程的数量。线程池进一步的封装,提供了对线程的状态,异步控制等操作。
对于多进程,可以利用多核CPU优势,但是使用多线程和多进程还需要进一步根据密集I/O和密集运算型代码等具体情况。多进程标准模块中提供的接口与多线程类似,可相互参照。
陆陆续续总结关于这篇博文也有一个多星期了,但是还是感觉有说不清楚的地方逻辑不通,希望读者能在评论区指出。期间参阅了很多的文档,博客,教程。
印象最深刻的还是Understand GIL: [传送门]这篇关于GIL的解释,虽然是英文文档,但是作者总是能以最精炼的句子表达最清晰的观点。
上一篇:Python 多线程、多进程 (二)之 多线程、同步、通信
Python 多线程、多进程 (三)之 线程进程对比、多进程的更多相关文章
- Python并发编程04 /多线程、生产消费者模型、线程进程对比、线程的方法、线程join、守护线程、线程互斥锁
Python并发编程04 /多线程.生产消费者模型.线程进程对比.线程的方法.线程join.守护线程.线程互斥锁 目录 Python并发编程04 /多线程.生产消费者模型.线程进程对比.线程的方法.线 ...
- python多线程学习三
本文希望达到的目标: 1.服务器端与线程池 (实例demo) 2.并发插入db与线程池(实例demo) 3.线程池使用说明 4.线程池源码解析 一.基于socket的服务器与线程池连接. 1.在i7 ...
- java多线程(三)线程的安全问题
1.1. 什么是线程安全 如果有多个线程同时运行同一个实现了Runnable接口的类,程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的:反之,则是线程不 ...
- Python多线程笔记(三),queue模块
尽管在Python中可以使用各种锁和同步原语的组合编写非常传统的多线程程序,但有一种首推的编程方式要优于其他所有编程方式即将多线程程序组织为多个独立人物的集合,这些任务之间通过消息队列进行通信 que ...
- Python多线程(2)——线程同步机制
本文介绍Python中的线程同步对象,主要涉及 thread 和 threading 模块. threading 模块提供的线程同步原语包括:Lock.RLock.Condition.Event.Se ...
- Java 多线程(三)—— 线程的生命周期及方法
这篇博客介绍线程的生命周期. 线程是一个动态执行的过程,它也有从创建到死亡的过程. 线程的几种状态 在 Thread 类中,有一个枚举内部类: 上面的信息以图片表示如下: 第一张图: 第二张图:把等待 ...
- UNIX环境编程学习笔记(28)——多线程编程(三):线程的取消
lienhua342014-11-24 1 取消线程 pthread 提供了pthread_cancel 函数用于请求取消同一进程中的其他线程. #include <pthread.h> ...
- python之路 IO多路复用 线程进程初步了解
一.IO多路复用 1.客户端 #!/usr/bin/env python #-*-coding:utf-8-*- import socket sk=socket.socket() sk.connect ...
- Java线程和多线程(三)——线程安全和同步
线程安全在Java中是一个很重要的课题.Java提供的多线程环境支持使用Java线程.我们都知道多线程共享一些对象实例的话,可能会在读取和更新共享数据的事后产生数据不一致问题. 线程安全 之所以会产生 ...
随机推荐
- Spring MVC 原理探秘 - 一个请求的旅行过程
1.简介 在前面的文章中,我较为详细的分析了 Spring IOC 和 AOP 部分的源码,并写成了文章.为了让我的 Spring 源码分析系列文章更为丰富一些,所以从本篇文章开始,我将来向大家介绍一 ...
- cad.net 利用win32api实现不重复打开dwg路径的文件夹(资源管理器)
这里地址的方法也是可用的,但是net3.5不能使用 为此我选择使用win32api的方式来遍历当前桌面所有资源管理器 /// <summary> /// 不重复打开dwg路径的资源管理器 ...
- Python编程语言基础
今天给大家讲解python语言基础~~ 01.python核心数据类型 整型数 int:整数是不带有小数部分的数字 浮点型数 float:浮点数是带有小数部分的数字(小数部分可以是0) 复数 co ...
- 机器学习基石笔记:10 Logistic Regression
线性分类中的是非题------>概率题, 设置概率阈值后,大于等于该值的为O,小于改值的为X.------>逻辑回归. O为1,X为0: 逻辑回归假设: 逻辑函数/S型函数:光滑,单调, ...
- 从一个集合中查找最大最小的N个元素——Python heapq 堆数据结构
Top N问题在搜索引擎.推荐系统领域应用很广, 如果用我们较为常见的语言,如C.C++.Java等,代码量至少也得五行,但是用Python的话,只用一个函数就能搞定,只需引入heapq(堆队列)这个 ...
- kafka之consumer参数auto.offset.reset 0.10+
https://blog.csdn.net/dingding_ting/article/details/84862776 https://blog.csdn.net/xianpanjia4616/ar ...
- Jquery 动态追加控件并获取值
先展示通过动态添加控件的结果: 实现步骤: 1.引用js文件 <script src="Script/jquery-1.5.1.min.js" type="text ...
- Excel设置excel打印每页都有表头标题
Excel设置excel打印每页都有表头标题
- 课程四(Convolutional Neural Networks),第三 周(Object detection) —— 0.Learning Goals
Learning Goals: Understand the challenges of Object Localization, Object Detection and Landmark Find ...
- Linux学习笔记之十————Linux常用服务器构建之ftp服务器
p服务器介绍 FTP 是File Transfer Protocol(文件传输协议)的英文简称,而中文简称为“文传协议”. 用于Internet上的控制文件的双向传输. 同时,它也是一个应用程序(Ap ...