面试官:请你讲讲Python多线程多进程

本文详细介绍了Python中的多线程多进程概念,包括并行与并发的区别、线程与进程的特性、GIL全局解释器锁的影响以及Python中多线程和多进程的使用。在多线程中,重点讨论了GIL如何限制了Python真正意义上的并行计算,但适合于I/O密集型任务。多进程可以利用多核优势,通过`multiprocessing`包实现。文中还给出了多线程编程示例,演示了线程锁的使用,以及多进程编程中`Process`和`Pool`的使用。
摘要由CSDN通过智能技术生成

Python多线程多进程

赛道

文章目录

  1. 并行和并发的概念
  2. 线程和进程的概念(来点八股文)
  3. PythonGIL锁相关以及历史
  4. 多线程编程详解
  5. 多进程编程详解(重点)

一、什么是并行和并发?

首先我们来先说一下一个简单的共同点,并行和并发都是完成多任务更加有效率的工具。我们下面用一张图来说明它们的不同点

image-20201115152751957

  • 并发:是指应用能够交替执行不同的任务,其实并发有带你类似于多线程的原理,多线程并非同事执行多个任务,如果你开多个线程执行,就是在你几乎不可察觉的速度不断的去切换这几个线程,以达到“同时执行的效果”
  • 并行:时指应用能够“同时”执行不同的的任务,就像你吃饭的时候可以一边吃饭一边打电话
  • 总结来说就是,并发时交替执行,并行时同时执行

线程和进程的概念

image-20201115155024645

在很多教科书上都有一句话:进程时资源分配的最小单位,线程时CPU调度的最小单位。

线程是程序中一个单一的顺序控制流程,是进程内一个相对独立的、可调度的执行单元,是系统独立调度和分配CPU的基本单位中的调度单位。

进程和线程的区别

进程是资源分配的基本单位。所有与该进程有关的资源,都被记录在进程控制块PCB中。以表示该进程拥有这些资源。另外进程也是抢占处理机的调度单位,它拥有一个完整的虚拟地址空间。但进程发生调度的时候,不同的进程拥有不同的虚拟你地址空间,而同一进程内的不同线程共享同一地址空间。

与进程相对应,线程和资源分配无关,它属于某一进程,并与进程内的其他线程一起共享进程的资源。线程只由相关堆栈寄存器和线程控制表TCB组成。

通常在一个进程中可以包含若干个线程,他们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为资源费配的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小。进本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更搞笑得提高系统内多个程序间得并发执行程度,提高系统资源的利用率。

我们总结下来有以下四项:

  1. 根本的区别为:进程是操作系统资源分配的基本单位,线程是任务调度和执行的基本单位
  2. 开销方面:每个进程都有独立的代码和数据空间,程序之间的切换会有较大的开销。线程可以看作轻量级进程,同一类线程共享代码和数据空间,每一个线程都有自己独立的运行栈,线程之间的切换开销小。
  3. 环境:在操作系统中能同时运行多个进程;在同一个进程中有多个线程同时执行。
  4. 内存分配方面:系统在运行的时候会为每个进程分配不同的空间;而对线程而言,除CPU外,系统不会为线程分配内存
  5. 关系:一个进程拥有多个线程,而一个线程只属于一个进程

多进程和多线程比较

对比维度 多进程 多线程 总结
数据共享、同步 数据共享复杂,同步简单 数据共享简单,同步复杂 各有优劣
内存、CPU 占用内存多,切换复杂,CPU利用率低 占用内存少,切换简单,CPU利用率高 线程占优
创建、销毁、切换 复杂,速度慢 简单,速度快 线程占优
编程、调试 编程简单,调试简单 编程复杂,调试复杂 进程占优
可靠性 进程间不会互相影响 一个线程挂掉将导致整个进程挂掉 进程占优

GIL锁(全局解释器锁)

每次谈到Python的多线程的时候,我们都会说到GIL锁的问题。

在正式的讲GIL锁之前,我们有一点是需要明确的,就是GIL并不是Python的特性,他是在实现Python解析器(CPython)时引入的概念。而在别的解析器中比如,Jython,IronPython等都是没有全局解释锁的,但CPython时大部分环境下的默认Python执行环境,所以在很多人的概念中CPython就是Python,但这个想法是错的。

GIL本质就是一把互斥锁,既然是互斥锁,所有的互斥锁的本质都是一样的,都是会将并发运行变成串行,以此来控制同一时间内数据只能被一个任务修改,进而保证数据安全。保护不同的数据的安全,就应该加不同的锁。

GIL的版本也在不断的改善之中,在python3.2前,GIL的释放逻辑是当前线程遇见IO操作或者ticks计数达到100(ticks可以看作是python自身的一个计数器,专门做用于GIL,每次释放后归零,这个计数可以通过 sys.setcheckinterval 来调整),进行释放。因为计算密集型线程在释放GIL之后又会立即去申请GIL,并且通常在其它线程还没有调度完之前它就已经重新获取到了GIL,就会导致一旦计算密集型线程获得了GIL,那么它在很长一段时间内都将占据GIL,甚至一直到该线程执行结束。

而在Python3.2之后开始使用新的GIL。新的GIL实现中用一个固定的超时时间来指示当前的线程放弃全局锁。在当前线程保持这个锁,且其他线程请求这个锁时,当前线程就会在5毫秒后被强制释放该锁。该改进在单核的情况下,对于单个线程长期占用GIL的情况有所好转。

总结以下:

有了GIL的存在,Python有这两个特点:

  • 进程可以利用多核,但是开销大。
  • 多线程开销小,却无法利用多核优势

也就是说,在Python中的多线程是假的多线程,Python解释器虽然可以开启多个线程,但在同一时间只有一个线程在解释器中运行,而做到这一点的正式由于GIL锁的存在,它的存在使得CPU的资源统一时间只会给一个线程使用,而由于开启线程的开销小,所以多线程才能有一篇用武之地。

但Python的多线程还是由用处的,从上面的分析我们可以知道:

  • 如果是I/O密集型任务,再多核也没用,即能开再多进程也没用,所以我们利用Python的多线程。
  • 如果是计算密集型任务,多进程则更好了。

多线程编程

在Python的多线程编程中,我们主要的是借助于threading模块,而在threading模块中最核心的内容是Thread这个类。我们要创建Thread对象,然后让他们运行,每一个Thread对象代表一个线程,在每一个线程中我们可以让程序处理不同的任务。这就是多线程编程。

创建Thread对象,有两种手段。

  1. 直接创建 Thread ,将一个 callable 对象从类的构造器传递进去,这个 callable 就是回调函数,用来处理任务。
  2. 编写一个自定义类继承 Thread,然后复写 run() 方法,在 run() 方法中编写任务处理代码,然后创建这个 Thread 的子类。(类似Java)

我们主要介绍第一种方式

class threading.Thread(group=None, target=None, name=None, args=(), kwargs={
   }, *, daemon=None)
  • group 应该为 None;为了日后扩展 ThreadGroup 类实现而保留。
  • target 是用于 run()方法调用的可调用对象。默认是 None,表示不需要调用任何方法。
  • name 是线程名称。默认情况下,由 “Thread-N” 格式构成一个唯一的名称,其中 N 是小的十进制数。
  • args 是用于调用目标函数的参数元组。默认是 ()
  • kwargs 是用于调用目标函数的关键字参数字典。默认是 {}
  • daemon 参数将显式地设置该线程是否为守护模式。 如果是 None (默认值),线程将继承当前线程的守护模式属性。

在Thread中有以下方法

  • start()开始线程活动。
  • run()代表线程活动的方法。
  • join(timeout=None)等待,直到线程终结。这会阻塞调用这个方法的线程,直到被调用 join() 的线程终结 – 不管是正常终结还是抛出未处理异常 – 或者直到发生超时,超时选项是可选的。
  • name只用于识别的字符串。它没有语义。多个线程可以赋予相同的名称。 初始名称由构造函数设置。
  • is_alive()返回线程是否存活。
  • daemon一个表示这个线程是(True)否(False)守护线程的布尔值。

我们列一下简单的代码实例:

import threading
import time

def test():

    for i in range(5):
        print(threading.current_thread().name+' test ',i)  # 获取子线程的名字
        time.sleep(0.5)


thread = threading.Thread(target=test,name='TestThread') # 实例化线程
thread.start()  # 开始线程
thread.join()   # 添加阻塞,可以改变线程的运行顺序

for i in range(5):
    print(threading.current_thread().name+' main ', i)
    print(thread.name+' is alive ', thread.isAlive()) # 判断线程是狗准确
    time.sleep(1)

结果

TestThread test  0
TestThread test  1
TestThread test  2
TestThread test  3
TestThread test  4
MainThread main  0
TestThread is alive  False
MainThread main  1
TestThread is alive  False
MainThread main  2
TestThread is alive  False
MainThread main  3
TestThread is alive  False
MainThread main  4
TestThread is alive  False

如果我门想要主进程结束的时候,子进程也要跟着结果,要怎么做呢,这里就可以用到daemon了。

我们可以设置主线程的执行时间比子线程慢,来检测一下

#-*-coding:utf-8-*-
import threading
import time

def test():

    for i in range(10):
        print(threading.current_thread().name+' test '
  • 1
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值