前言:为什么有人说 Python 的多线程是鸡肋,不是真正意义上的多线程?

看到这里,也许你会疑惑。这很正常,所以让我们带着问题来阅读本文章吧。问题:

1、Python多线程为什么耗时更长?

2、为什么在Python里面推荐使用多进程而不是多线程?

1 基础知识

现在的PC都是多核的,使用多线程能充分利用CPU来提供程序的执行效率。

1.1 线程

线程是一个基本的CPU执行单元。它必须依托于进程存活。一个线程是一个execution context(执行上下文),即一个CPU执行时所需要的一串指令。

1.2 进程

进程是指一个程序在给定数据集合上的一次执行过程,是系统进行资源分配和运行调用的独立单位。可以简单地理解为操作系统中正在执行的程序。也就说,每个应用程序都有一个自己的进程。

每一个进程启动时都会最先产生一个线程,即主线程。然后主线程会再创建其他的子线程。

1.3 两者的区别

  • 线程必须在某个进程中执行。
  • 一个进程可包含多个线程,其中有且只有一个主线程。
  • 多线程共享同个地址空间、打开的文件以及其他资源。
  • 多进程共享物理内存、磁盘、打印机以及其他资源。

1.4 线程的类型

线程的因作用可以划分为不同的类型。大致可分为:

  • 主线程
  • 子线程
  • 守护线程(后台线程)
  • 前台线程

2 Python 多线程

2.1 GIL

其他语言,CPU是多核时是支持多个线程同时执行。但在Python中,无论是单核还是多核,同时只能由一个线程在执行。其根源是GIL的存在。GIL的全称是Global Interpreter Lock(全局解释器锁),来源是Python设计之初的考虑,为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个Python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。

而目前Python的解释器有多种,例如:

  • CPython:CPython是用C语言实现的Python解释器。 作为官方实现,它是最广泛使用的Python解释器。

  • PyPy:PyPy是用RPython实现的解释器。RPython是Python的子集, 具有静态类型。这个解释器的特点是即时编译,支持多重后端(C, CLI, JVM)。PyPy旨在提高性能,同时保持最大兼容性(参考CPython的实现)。

  • Jython:Jython是一个将Python代码编译成Java字节码的实现,运行在JVM (Java Virtual Machine) 上。另外,它可以像是用Python模块一样,导入并使用任何Java类。

  • IronPython:IronPython是一个针对 .NET 框架的Python实现。它可以用Python和 .NET framework的库,也能将Python代码暴露给 .NET框架中的其他语言。

GIL只在CPython中才有,而在PyPy和Jython中是没有GIL的。

每次释放GIL锁,线程进行锁竞争、切换线程,会消耗资源。这就导致打印线程执行时长,会发现耗时更长的原因。

并且由于GIL锁存在,Python里一个进程永远只能同时执行一个线程(拿到GIL的线程才能执行),这就是为什么在多核CPU上,Python 的多线程效率并不高的根本原因。

2.2 创建多线程

Python提供两个模块进行多线程的操作,分别是threadthreading,前者是比较低级的模块,用于更底层的操作,一般应用级别的开发不常用。

  • 方法1:直接使用threading.Thread()
import threading# 这个函数名可随便定义
def run(n):print("current task:", n)if __name__ == "__main__":t1 = threading.Thread(target=run, args=("thread 1",))t2 = threading.Thread(target=run, args=("thread 2",))t1.start()t2.start()
  • 方法2:继承threading.Thread来自定义线程类,重写run方法
import threadingclass MyThread(threading.Thread):def __init__(self, n):super(MyThread, self).__init__()  # 重构run函数必须要写self.n = ndef run(self):print("current task:", n)if __name__ == "__main__":t1 = MyThread("thread 1")t2 = MyThread("thread 2")t1.start()t2.start()

2.3 线程合并

join函数执行顺序是逐个执行每个线程,执行完毕后继续往下执行。主线程结束后,子线程还在运行,join函数使得主线程等到子线程结束时才退出。

import threadingdef count(n):while n > 0:n -= 1if __name__ == "__main__":t1 = threading.Thread(target=count, args=("100000",))t2 = threading.Thread(target=count, args=("100000",))t1.start()t2.start()# 将 t1 和 t2 加入到主线程中t1.join()t2.join()

2.4 线程同步与互斥锁

线程之间数据共享的。当多个线程对某一个共享数据进行操作时,就需要考虑到线程安全问题。threading模块中定义了Lock 类,提供了互斥锁的功能来保证多线程情况下数据的正确性。

用法的基本步骤:

#创建锁
mutex = threading.Lock()
#锁定
mutex.acquire([timeout])
#释放
mutex.release()

其中,锁定方法acquire可以有一个超时时间的可选参数timeout。如果设定了timeout,则在超时后通过返回值可以判断是否得到了锁,从而可以进行一些其他的处理。具体用法见示例代码:

import threading
import timenum = 0
mutex = threading.Lock()class MyThread(threading.Thread):def run(self):global num time.sleep(1)if mutex.acquire(1):  num = num + 1msg = self.name + ': num value is ' + str(num)print(msg)mutex.release()if __name__ == '__main__':for i in range(5):t = MyThread()t.start()

2.5 可重入锁(递归锁)

为了满足在同一线程中多次请求同一资源的需求,Python提供了可重入锁(RLock)。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire 的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。具体用法如下:

#创建 RLock
mutex = threading.RLock()class MyThread(threading.Thread):def run(self):if mutex.acquire(1):print("thread " + self.name + " get mutex")time.sleep(1)mutex.acquire()mutex.release()mutex.release()

2.6 守护线程

如果希望主线程执行完毕之后,不管子线程是否执行完毕都随着主线程一起结束。我们可以使用setDaemon(bool)函数,它跟join函数是相反的。它的作用是设置子线程是否随主线程一起结束,必须在start() 之前调用,默认为False

2.7 定时器

如果需要规定函数在多少秒后执行某个操作,需要用到Timer类。具体用法如下:

from threading import Timerdef show():print("Pyhton")# 指定一秒钟之后执行 show 函数
t = Timer(1, hello)
t.start()

3 Python 多进程

3.1 创建多进程

Python要进行多进程操作,需要用到muiltprocessing库,其中的Process类跟threading模块的Thread类很相似。所以直接看代码熟悉多进程。

  • 方法1:直接使用Process, 代码如下:
from multiprocessing import Process  def show(name):print("Process name is " + name)if __name__ == "__main__": proc = Process(target=show, args=('subprocess',))  proc.start()  proc.join()
  • 方法2:继承Process来自定义进程类,重写run方法, 代码如下:
from multiprocessing import Process
import timeclass MyProcess(Process):def __init__(self, name):super(MyProcess, self).__init__()self.name = namedef run(self):print('process name :' + str(self.name))time.sleep(1)if __name__ == '__main__':for i in range(3):p = MyProcess(i)p.start()for i in range(3):p.join()

3.2 多进程通信

进程之间不共享数据的。如果进程之间需要进行通信,则要用到Queue模块或者Pipe模块来实现。

  • Queue

Queue是多进程安全的队列,可以实现多进程之间的数据传递。它主要有两个函数putget

put() 用以插入数据到队列中,put还有两个可选参数:blocked 和timeout。如果blocked为 True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出 Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。

get()可以从队列读取并且删除一个元素。同样get有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且 timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常。

具体用法如下:

from multiprocessing import Process, Queuedef put(queue):queue.put('Queue 用法')if __name__ == '__main__':queue = Queue()pro = Process(target=put, args=(queue,))pro.start()print(queue.get())   pro.join()
  • Pipe

Pipe的本质是进程之间的用管道数据传递,而不是数据共享,这和socket有点像。pipe() 返回两个连接对象分别表示管道的两端,每端都有send()和recv()函数。如果两个进程试图在同一时间的同一端进行读取和写入那么,这可能会损坏管道中的数据,具体用法如下:

from multiprocessing import Process, Pipedef show(conn):conn.send('Pipe 用法')conn.close()if __name__ == '__main__':parent_conn, child_conn = Pipe() pro = Process(target=show, args=(child_conn,))pro.start()print(parent_conn.recv())   pro.join()

3.3 进程池

创建多个进程,我们不用傻傻地一个个去创建。我们可以使用Pool模块来搞定。Pool 常用的方法如下:

方法 含义
apply() 同步执行(串行)
apply_async() 异步执行(并行)
terminate() 立刻关闭进程池
join() 主进程等待所有子进程执行完毕。必须在close或terminate()之后使用
close() 等待所有进程结束后,才关闭进程池

具体用法见示例代码:

#coding: utf-8
import multiprocessing
import timedef func(msg):print("msg:", msg)time.sleep(3)print("end")if __name__ == "__main__":# 维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去pool = multiprocessing.Pool(processes = 3)for i in range(5):msg = "hello %d" %(i)# 非阻塞式,子进程不影响主进程的执行,会直接运行到 pool.join()pool.apply_async(func, (msg, ))   # 阻塞式,先执行完子进程,再执行主进程# pool.apply(func, (msg, ))   print("Mark~ Mark~ Mark~~~~~~~~~~~~~~~~~~~~~~")# 调用join之前,先调用close函数,否则会出错。pool.close()# 执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束pool.join()   print("Sub-process(es) done.")
  • 如上,进程池Pool被创建出来后,即使实际需要创建的进程数远远大于进程池的最大上限,p.apply_async(test)代码依旧会不停的执行,并不会停下等待;相当于向进程池提交了10个请求,会被放到一个队列中;
  • 当执行完p1 = Pool(5)这条代码后,5条进程已经被创建出来了,只是还没有为他们各自分配任务,也就是说,无论有多少任务,实际的进程数只有5条,计算机每次最多5条进程并行。
  • 当Pool中有进程任务执行完毕后,这条进程资源会被释放,pool会按先进先出的原则取出一个新的请求给空闲的进程继续执行;
  • 当Pool所有的进程任务完成后,会产生5个僵尸进程,如果主线程不结束,系统不会自动回收资源,需要调用join函数去回收。
  • join函数是主进程等待子进程结束回收系统资源的,如果没有join,主程序退出后不管子进程有没有结束都会被强制杀死;
  • 创建Pool池时,如果不指定进程最大数量,默认创建的进程数为系统的内核数量.

4 选择多线程还是多进程?

在这个问题上,首先要看下你的程序是属于哪种类型的。一般分为两种:CPU密集型和I/O密集型。

  • CPU 密集型:程序比较偏重于计算,需要经常使用CPU来运算。例如科学计算的程序,机器学习的程序等。

  • I/O 密集型:顾名思义就是程序需要频繁进行输入输出操作。爬虫程序就是典型的I/O密集型程序。

如果程序是属于CPU密集型,建议使用多进程。而多线程就更适合应用于I/O密集型程序。

Python之多进程与多线程相关推荐

  1. Python编程——多进程与多线程编程(附实例)

    进程与线程的概念 进程,是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竟争计算机系统资源的基本单位.每一个进程都有一个自己的地址空间,即进程空间或(虚空间).进程空间的大小只与 ...

  2. Python之多进程和多线程详解

    1.进程的概念 一个CPU的时候运行,轮询调度实现并发执行 多CPU运行机制: 计算机程序:存储在磁盘上的可执行二进制(或其他类型)文件. 只有把它们加载到内存中,并被操作系统调用它们才会拥有其自己的 ...

  3. python中多进程和多线程

    简介 任务可以理解为程序,多个程序同时执行 比如:边听歌,边看小说 边写代码,边听听歌 单核电脑实现多任务: 调度算法: 时间片轮转 并发: 3个任务,2个cpu,轮番调度并行: 4个cpu,3个任务 ...

  4. python计算密集型任务_Python多进程和多线程测试比高低,只为证明谁是最快的“仔”

    目的 前面分别详细介绍了python的多进程和多线程,如果还没看前面文章的,请先看下之前的文章详解内容.有任何疑问请留言.那这里就不再对多线程和多进程的实现和用法再赘述了.那各位同学学习了python ...

  5. Python多进程、多线程编程

    文章目录 1. 进程.线程.协程 2. Python多线程 GIL全局解释器锁 CPython科普 3. Python:多进程 or 多线程 计算密集型.I/O密集型科普 4. 编程实战 1. 进程. ...

  6. python廖雪峰_【Python】python中实现多进程与多线程

    进程与线程 进程(process)就是任务,是计算机系统进行资源分配和调度的基本单位[1].比如,打开一个word文件就是启动了一个word进程. 线程(thread)是进程内的子任务.比如word中 ...

  7. python并发与并行_python多进程,多线程分别是并行还是并发

    匿名用户 1级 2017-09-30 回答 展开全部 并发和并行 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行. 你吃饭吃到一半,电话来了,你停了下来接了电话, ...

  8. async python两个_【Python】python中实现多进程与多线程

    进程与线程 进程(process)就是任务,是计算机系统进行资源分配和调度的基本单位[1].比如,打开一个word文件就是启动了一个word进程. 线程(thread)是进程内的子任务.比如word中 ...

  9. 一文看懂Python多进程与多线程编程(工作学习面试必读)

    进程(process)和线程(thread)是非常抽象的概念, 也是程序员必需掌握的核心知识.多进程和多线程编程对于代码的并发执行,提升代码效率和缩短运行时间至关重要.小编我今天就来尝试下用一文总结下 ...

最新文章

  1. Java Review - 并发编程_伪共享
  2. (25)2-9-9-12分页(下)
  3. war 发布后页面不更新_一文看懂tomcat8如何配置web页面管理
  4. 原生JAVA的TCP/UDP编程
  5. 前端学习(686):for循环
  6. Kali Linux 从入门到精通(十)-漏洞挖掘之缓冲区溢出
  7. 方舟编译器开源技术沙龙北京站首秀:让开源激活软件开发的潜力
  8. 【DP】【单调队列】【NOI2005】瑰丽华尔兹
  9. 归一化函数mapminmax的讨论
  10. 力扣题目——143. 重排链表
  11. Effective JAVA 创建和销毁对象 遇到多参构造器考虑使用构建器
  12. 2.4G信道跳频-LFSR-C代码实现
  13. FPGA零基础学习:按键控制LED
  14. 数据可视化大屏能为物联网项目带来什么
  15. macOS 安卓模拟器 Nox夜神模拟器 共享目录
  16. 信息系统项目管理师2018年上半年下午案例分析题及答案
  17. 谁抢走你的棒棒糖?精彩的创意让你得到的不仅仅是震撼 值得一看(图)
  18. 我的世界服务器怎么显示腐竹来了,我的世界服务器主人可用指令一览 我的世界腐竹常用指令介绍_游侠手游...
  19. nodejs无法下载puppeteer附带的chromium解决方案
  20. 基于yolov4作者最新力作yolov7目标检测模型实现火点烟雾检测

热门文章

  1. 独家|【云+端】战略发布,助力快速上云
  2. IBM X3650 M3服务器上RAID配置
  3. Linux常用命令及使用技巧
  4. 将windows系统主机上的文件拷贝到Linux系统中;将Linux系统中的文件粘贴到Windows主机中
  5. 通信算法之七十五:无人机通信-接收信号解析破解
  6. 轻时代来临 资深架构师分享手游五大设计要点
  7. Echarts使用心得总结——异步数据加载
  8. EasyTips v0.02 Beta发布了
  9. python爬取论坛图片_[python爬虫] Selenium定向爬取虎扑篮球海量精美图片
  10. 局域网中两台笔记本之间传输文件