百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

浅析Python的进程、线程与协程(python进程线程协程的应用场景)

off999 2024-09-20 22:53 53 浏览 0 评论

进程

进程是指在系统中正在运行的一个应用程序,是CPU的最小工作单元。

进程有就绪、运行、阻塞、创建和退出五种状态。其中,运行中的三种状态:就绪、运行、阻塞。创建和退出是描述产生和释放的状态。

进程的特点

  • 动态性:进程是程序的一次执行过程,动态产生,动态消亡。
  • 独立性:进程是一个能独立运行的基本单元。是系统分配资源与调度的基本单元。
  • 并发性:任何进程都可以与其他进程并发执行。
  • 结构性:进程由程序、数据和进程控制块三部分组成。

缺点

无法即时完成的任务带来大量的上下文切换代价与时间代价。

  • 进程的上下文:当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。
  • 上下文切换:当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够得到切换时的状态并执行下去。

线程

线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。

一个进程可以有一个或多个线程,同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。

但同一进程中的多个线程有各自的调用栈和线程本地存储。

说明:

进程才是CPU的最小作业单元(最小资源管理单元),开更多的线程不会导致本进程得到更多CPU的青睐,而是提高了CPU在本进程使用时间段的利用率。但是,本进程使用时间段的各线程优先级是用户可以自己设置的。

进程与线程的区别

  1. 进程是CPU资源分配的基本单位,线程是独立运行和独立调度的基本单位(CPU上真正运行的是线程)。
  2. 进程拥有自己的资源空间,一个进程包含若干个线程,线程与CPU资源分配无关,多个线程共享同一进程内的资源。
  3. 线程的调度与切换比进程快很多。
  4. CPU密集型代码(各种循环处理、计算等等):使用多进程。IO密集型代码(文件处理、网络爬虫等):使用多线程。

关于IO密集型和CPU密集型,请参见:一分钟明白IO密集型与CPU密集型的区别

虽然线程大幅的提高了CPU的效率,且能够设置一定的优先级,但是线程的资源片分配还是由CPU来管理的。

那么能不能人为管理线程的资源分配(切换)呢?协程在语言层面实现了这一点。

协程

协程(Coroutine,又称微线程,纤程)是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制。

如同一个进程可以有很多线程一样,一个线程可以有很多协程。

但是,协程不是被操作系统所管理的,没有改变CPU最小执行单元是线程,协程是完全由程序所控制的(用户态执行),不会产生上下文切换。

协程具有高并发、高扩展性、低成本的特点,一个CPU支持上万的协程都不是问题。所以,很适合用于高并发处理。通常,协程可以处理IO密集型程序的效率问题,但是处理CPU密集型不是它的长处,如要充分发挥CPU利用率可以结合多进程+协程。

优点

  1. 无需线程上下文切换的开销
  2. 无需原子操作锁定及同步的开销
  3. 方便切换控制流,简化编程模型

缺点

  1. 无法利用多核资源:协程的本质是个单线程,它不能同时在多个核用上。协程需要和进程配合才能运行在多CPU上。当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
  2. 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序。

一些主流语言对协程的支持情况

  1. Python的yield/send,当协程执行到yield关键字时,会暂停在那一行,等到主线程调用send方法发送了数据,协程才会接到数据继续执行。
  2. Lua从5.0版本开始使用协程,通过扩展库coroutine来实现。
  3. Go语言对协程的实现非常强大而简洁,可以轻松创建成百上千个协程并发执行。
  4. Java语言并没有对协程的原生支持,但是某些开源框架模拟出了协程的功能,如Kilim。

并发与并行

并发:在操作系统中,某一时间段,几个程序在同一个CPU上运行,但在任意一个时间点上,只有一个程序在该CPU上运行。

补充说明:

当有多个线程时,如果系统只有一个CPU,那么CPU不可能真正同时进行多个线程,CPU的运行时间会被划分成若干个时间段,每个时间段分配给各个线程去执行,一个时间段里某个线程运行时,其他线程处于挂起状态,这就是并发。

并发解决了程序排队等待的问题,如果一个程序发生阻塞,其他程序仍然可以正常执行。

并行:当操作系统有多个CPU时,一个CPU处理A线程,另一个CPU处理B线程,两个线程互相不抢占CPU资源,可以同时进行,这种方式成为并行。

并发与并行的区别

并发只是在宏观上给人感觉有多个程序在同时运行,但在实际的单CPU系统中,每一时刻只有一个程序在运行,微观上这些程序是分时交替执行。

在多CPU系统中,将这些并发执行的程序分配到不同的CPU上处理,每个CPU用来处理一个程序,这样多个程序便可以实现同时执行。

Python多线程

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

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

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

因此,同一时刻,只有一个线程在运行,其它线程只能等待,即使是多核CPU,也没办法让多个线程「并行」地同时执行代码,只能是交替执行,因为多线程涉及到上线文切换、锁机制处理(获取锁,释放锁等),所以,在CPU密集型应用中多线程执行不快反慢。

补充说明:GIL

在 CPython 解释器(Python语言的主流解释器)中,有一把全局解释锁GIL(Global Interpreter Lock),某个线程须先拿到GIL才允许进入CPU执行。

什么时候 GIL 被释放呢?

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

计算密集型(CPU-bound)线程执行 100 次解释器的计步(ticks)时(计步可粗略看作 Python 虚拟机的指令),也会释放 GIL。在Python3.x中,GIL不使用ticks计数,改为使用计时器(执行时间达到阈值后,当前线程释放GIL)。

特别提示:GIL只在CPython中才有,因为CPython调用的是c语言的原生线程,所以他不能直接操作cpu,只能利用GIL保证同一时间只能有一个线程拿到数据。而在PyPy和Jython中是没有GIL的。

示例代码如下:

1. 普通线程创建:

import threading
import time

def run(n):
    print(f"task {n} ")
    time.sleep(1)
    print(f"task {n} -> 2s ")
    time.sleep(1)
    print(f"task {n} -> 1s ")
    time.sleep(1)
    print(f"task {n} -> 0s ")
    time.sleep(1)

if __name__ == '__main__':
    t1 = threading.Thread(target=run, args=("t1",))
    t2 = threading.Thread(target=run, args=("t2",))
    t1.start()
    t2.start()
复制代码

运行结果:

task t1 task t2 
task t2 -> 2s 
task t1 -> 2s 
task t1 -> 1s 
task t2 -> 1s 
task t1 -> 0s 
task t2 -> 0s 
复制代码

2. 自定义线程创建:

import threading
import time

class MyThread(threading.Thread):
    
    def __init__(self, n):
        super(MyThread, self).__init__()  # 重构run函数必须要写
        self.n = n

    def run(self):
        print(f"task: {self.n}")
        time.sleep(1)
        print(f"task: {self.n} -> 2s")
        time.sleep(1)
        print(f"task: {self.n} -> 1s")
        time.sleep(1)
        print(f"task: {self.n} -> 0s")
        time.sleep(1)

if __name__ == "__main__":
    t1 = MyThread("t1")
    t2 = MyThread("t2")
    t1.start()
    t2.start()
复制代码

运行结果:

task: t1
task: t2
task: t1 -> 2s
task: t2 -> 2s
task: t2 -> 1s
task: t1 -> 1s
task: t2 -> 0s
task: t1 -> 0s
复制代码

Python多进程

Python要充分利用多核CPU,就用多进程。

原因是:每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,所以在Python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。

示例代码如下:

1. 直接调用

import time
import random
from multiprocessing import Process


def run(name):
    print('%s runing' % name)
    time.sleep(random.randrange(1, 5))
    print('%s running end' % name)


if __name__ == '__main__':
    p1 = Process(target=run, args=('anne',))  # 必须加,号
    p2 = Process(target=run, args=('alice',))
    p3 = Process(target=run, args=('biantai',))
    p4 = Process(target=run, args=('haha',))

    p1.start()
    p2.start()
    p3.start()
    p4.start()
    print('主线程')
复制代码

运行结果:

主线程
anne runing
alice runing
biantai runing
haha runing
biantai running end
haha running end
anne running end
alice running end
复制代码

2. 继承式调用

import time
import random
from multiprocessing import Process


class Run(Process):
    def __init__(self,name):
        super().__init__()
        self.name=name
    def run(self):
        print('%s runing' %self.name)
        time.sleep(random.randrange(1,5))
        print('%s runing end' %self.name)

if __name__ == '__main__':
    p1 = Run('anne')
    p2 = Run('alex')
    p3 = Run('ab')
    p4 = Run('hey')
    p1.start()  # start会自动调用run
    p2.start()
    p3.start()
    p4.start()
    print('主线程')
复制代码

运行结果:

主线程
anne runing
alex runing
ab runing
hey runing
anne runing end
alex runing end
hey runing end
ab runing end
复制代码

Python协程

运行效率极高,协程的切换完全由程序控制,不像线程切换需要花费操作系统的开销,线程数量越多,协程的优势就越明显。

同时,在Python中,协程不需要多线程的锁机制,因为只有一个线程,也不存在变量冲突。

协程对于IO密集型任务非常适用,如果是CPU密集型任务,推荐多进程+协程的方式。对于多核CPU,利用多进程+协程的方式,能充分利用CPU,获得极高的性能。

Python协程的发展时间较长:

  • Python2.5 为生成器引用.send()、.throw()、.close()方法
  • Python3.3 为引入yield from,可以接收返回值,可以使用yield from定义协程
  • Python3.4 加入了asyncio模块
  • Python3.5 增加async、await关键字,在语法层面的提供支持
  • Python3.7 使用async def + await的方式定义协程
  • 此后asyncio模块更加完善和稳定,对底层的API进行的封装和扩展
  • Python将于3.10版本中移除以yield from的方式定义协程

示例代码如下:

1. Python2.x 实现协程

Python2.x对协程的支持是通过generator实现的。

在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。

但是Python的yield不但可以返回一个值,它还可以接收调用者发出的参数。

下面改用协程实现生产者消费者模式,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产, 效率极高:

def consumer():
    print("consumer----------")
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[消费者]Consuming %s...' % n)
        r = '200 OK'

def producer(c):
    print("producer----------")
    # 启动生成器
    c.send(None)
    n = 0
    while n < 5:
        n = n + 1
        print('[生产者]Producing %s...' % n)
        r = c.send(n)
        print('[生产者]Consumer return: %s' % r)
    c.close()

if __name__ == '__main__':
    c = consumer()
    producer(c)
复制代码

代码流程说明:

consumer函数是一个generator,把一个consumer传入produce后:

  1. 首先调用c.send(None)启动生成器(在生成带有yield的 generator后 第一个迭代必须是__next__()。 __next__() 和 send(None) 的效果是相同的);
  2. 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
  3. consumer通过yield拿到消息,处理,又通过yield把结果传回;
  4. produce拿到consumer处理的结果,继续生产下一条消息;
  5. produce决定不生产了,通过c.close()关闭consumer,整个过程结束。

运行结果:

producer----------
consumer----------
[生产者]Producing 1...
[消费者]Consuming 1...
[生产者]Consumer return: 200 OK
[生产者]Producing 2...
[消费者]Consuming 2...
[生产者]Consumer return: 200 OK
[生产者]Producing 3...
[消费者]Consuming 3...
[生产者]Consumer return: 200 OK
[生产者]Producing 4...
[消费者]Consuming 4...
[生产者]Consumer return: 200 OK
[生产者]Producing 5...
[消费者]Consuming 5...
[生产者]Consumer return: 200 OK
复制代码

send(msg)与next()的区别

send可以传递参数给yield表达式,这时传递的参数会作为yield表达式的值,而yield的参数是返回给调用者的值。

换句话说,就是send可以强行修改上一个yield表达式的值。

比如函数中有一个yield赋值a = yield 5,第一次迭代到这里会返回5,a还没有赋值。

第二次迭代时,使用send(10),那么就是强行修改yield 5表达式的值为10,本来是5的,结果a = 10。

send(msg)与next()都有返回值,它们的返回值是当前迭代遇到yield时,yield后面表达式的值,其实就是当前迭代中yield后面的参数。

第一次调用send时必须是send(None),否则会报错,之所以为None是因为这时候还没有一个yield表达式可以用来赋值。

2. Python3.x 实现协程

要点

  1. 使用async def的形式定义
  2. 在协程中可以使用await关键字,注意,其后跟的是"可等待对象"(协程, 任务 和 Future)
  3. 协程不能直接执行,需要在asyncio.run()中执行,也可以跟在await后面
  4. async和await这两个关键字只能在协程中使用
import asyncio

async def foo(name):
   await asyncio.sleep(1)      # 这是一个不会阻塞的sleep,是一个协程
   print(f"name = {name}")


async def main():
   # 协程本身就是一个可等待对象
   await foo("lczmx")  # 执行协程
   print("done")

if __name__ == '__main__':
   # 使用asyncio.run运行
   asyncio.run(main())
复制代码

运行结果:

name = lczmx
done
复制代码

其中,asyncio.run(main, *, debug=False)方法就是对run_until_complete进行了封装:

loop = events.new_event_loop()

return loop.run_until_complete(main)

关于可等待对象说明

可等待对象(awaitable)是能在 await 表达式中使用的对象。可以是协程或是具有__await__() 方法的对象。

那么协程是如何成为可等待对象的呢?

  1. collections.abc.Awaitable类,这是为可等待对象提供的类,可被用于 await 表达式中。
class Awaitable(metaclass=ABCMeta):
   __slots__ = ()

   @abstractmethod
   def __await__(self):   # __await__方法必须返回一个 iterator
      yield

   @classmethod
   def __subclasshook__(cls, C):
      if cls is Awaitable:
         return _check_methods(C, "__await__")
      return NotImplemented
复制代码
  1. 用async def复合语句创建的函数,它返回的是一个Coroutine对象,而Coroutine继承Awaitable。

3. Python3.x 使用协程进行并发操作

使用协程进行并发操作,在Python3.7以上的版本,使用asyncio.create_task(coro)方法,返回一个Task对象,Task类继承Future,在Python3.7以下版本中,使用asyncio.ensure_future(coro_or_future)。

import asyncio


async def foo(char:str, count: int):
    for i in range(count):
        print(f"{char}-{i}")
        await asyncio.sleep(1)


async def main():
    task1 = asyncio.create_task(foo("A", 2))
    task2 = asyncio.create_task(foo("B", 3))
    task3 = asyncio.create_task(foo("C", 2))

    await task1
    await task2
    await task3


if __name__ == '__main__':
    asyncio.run(main())
复制代码

执行结果:

A-0
B-0
C-0
A-1
B-1
C-1
B-2
复制代码

总结

  1. 线程和协程推荐在IO密集型的任务(比如网络调用)中使用,而在CPU密集型的任务中,表现较差。
  2. 对于CPU密集型的任务,则需要多个进程,绕开GIL的限制,利用所有可用的CPU核心,提高效率。
  3. 在高并发下的最佳实践就是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。


作者:吃果冻不吐果冻皮
链接:https://juejin.cn/post/7027998293351202853

相关推荐

安全教育登录入口平台(安全教育登录入口平台官网)

122交通安全教育怎么登录:122交通网的注册方法是首先登录网址http://www.122.cn/,接着打开网页后,点击右上角的“个人登录”;其次进入邮箱注册,然后进入到注册页面,输入相关信息即可完...

大鱼吃小鱼经典版(大鱼吃小鱼经典版(经典版)官方版)

大鱼吃小鱼小鱼吃虾是于谦跟郭麒麟的《我的棒儿呢?》郭德纲说于思洋郭麒麟作诗的相声,最后郭麒麟做了一首,师傅躺在师母身上大鱼吃小鱼小鱼吃虾虾吃水水落石出师傅压师娘师娘压床床压地地动山摇。...

谷歌地球下载高清卫星地图(谷歌地球地图下载器)
  • 谷歌地球下载高清卫星地图(谷歌地球地图下载器)
  • 谷歌地球下载高清卫星地图(谷歌地球地图下载器)
  • 谷歌地球下载高清卫星地图(谷歌地球地图下载器)
  • 谷歌地球下载高清卫星地图(谷歌地球地图下载器)
哪个软件可以免费pdf转ppt(免费的pdf转ppt软件哪个好)
哪个软件可以免费pdf转ppt(免费的pdf转ppt软件哪个好)

要想将ppt免费转换为pdf的话,我们建议大家可以下一个那个wps,如果你是会员的话,可以注册为会员,这样的话,在wps里面的话,就可以免费将ppt呢转换为pdfpdf之后呢,我们就可以直接使用,不需要去直接不需要去另外保存,为什么格式转...

2026-02-04 09:03 off999

电信宽带测速官网入口(电信宽带测速官网入口app)

这个网站看看http://www.swok.cn/pcindex.jsp1.登录中国电信网上营业厅,宽带光纤,贴心服务,宽带测速2.下载第三方软件,如360等。进行在线测速进行宽带测速时,尽...

植物大战僵尸95版手机下载(植物大战僵尸95 版下载)

1可以在应用商店或者游戏平台上下载植物大战僵尸95版手机游戏。2下载教程:打开应用商店或者游戏平台,搜索“植物大战僵尸95版”,找到游戏后点击下载按钮,等待下载完成即可安装并开始游戏。3注意:确...

免费下载ppt成品的网站(ppt成品免费下载的网站有哪些)

1、Chuangkit(chuangkit.com)直达地址:chuangkit.com2、Woodo幻灯片(woodo.cn)直达链接:woodo.cn3、OfficePlus(officeplu...

2025世界杯赛程表(2025世界杯在哪个国家)

2022年卡塔尔世界杯赛程公布,全部比赛在卡塔尔境内8座球场举行,2022年,决赛阶段球队全部确定。揭幕战于当地时间11月20日19时进行,由东道主卡塔尔对阵厄瓜多尔,决赛于当地时间12月18日...

下载搜狐视频电视剧(搜狐电视剧下载安装)

搜狐视频APP下载好的视频想要导出到手机相册里方法如下1、打开手机搜狐视频软件,进入搜狐视频后我们点击右上角的“查找”,找到自已喜欢的视频。2、在“浏览器页面搜索”窗口中,输入要下载的视频的名称,然后...

pubg免费下载入口(pubg下载入口官方正版)
  • pubg免费下载入口(pubg下载入口官方正版)
  • pubg免费下载入口(pubg下载入口官方正版)
  • pubg免费下载入口(pubg下载入口官方正版)
  • pubg免费下载入口(pubg下载入口官方正版)
永久免费听歌网站(丫丫音乐网)

可以到《我爱音乐网》《好听音乐网》《一听音乐网》《YYMP3音乐网》还可以到《九天音乐网》永久免费听歌软件有酷狗音乐和天猫精灵,以前要跳舞经常要下载舞曲,我从QQ上找不到舞曲下载就从酷狗音乐上找,大多...

音乐格式转换mp3软件(音乐格式转换器免费版)

有两种方法:方法一在手机上操作:1、进入手机中的文件管理。2、在其中选择“音乐”,将显示出手机中的全部音乐。3、点击“全选”,选中所有音乐文件。4、点击屏幕右下方的省略号图标,在弹出菜单中选择“...

电子书txt下载(免费的最全的小说阅读器)

1.Z-library里面收录了近千万本电子书籍,需求量大。2.苦瓜书盘没有广告,不需要账号注册,使用起来非常简单,直接搜索预览下载即可。3.鸠摩搜书整体风格简洁清晰,书籍资源丰富。4.亚马逊图书书籍...

最好免费观看高清电影(播放免费的最好看的电影)

在目前的网上选择中,IMDb(互联网电影数据库)被认为是最全的电影网站之一。这个网站提供了各种类型的电影和电视节目的海量信息,包括剧情介绍、演员表、评价、评论等。其还提供了有关电影制作背后的详细信息,...

孤单枪手2简体中文版(孤单枪手2简体中文版官方下载)

要将《孤胆枪手2》游戏的征兵秘籍切换为中文,您可以按照以下步骤进行操作:首先,打开游戏设置选项,通常可以在游戏主菜单或游戏内部找到。然后,寻找语言选项或界面选项,点击进入。在语言选项中,选择中文作为游...

取消回复欢迎 发表评论: