Python 进程和线程、协程浅析

41 篇文章 2 订阅

python进程

调度

在传统的计算机操作系统中,CPU调度的基本单位是进程。后来操作系统普遍引入了线程的概念,线程成为了CPU调度的基本单位,进程只能作为资源拥有的基本单位。

并行

由于线程的引入,原先一个进程只能有一个并发,现在一个进程可以有多个线程并行执行。早期的很多HTTP server都是通过线程来解决服务器的并发,比起之前用fork子进程来处理并发效率有了数倍的提升。这一切都得益于线程可以用进程更低的代价实现并发。

定义

进程,是执行中的计算机程序。也就是说,每个代码在执行的时候,首先本身即是一个进程。
一个进程具有:就绪,运行,中断,僵死,结束等状态(不同操作系统不一样)。

特性

1.每个程序,本身首先是一个进程
2.运行中每个进程都拥有自己的地址空间、内存、数据栈及其它资源。
3.操作系统本身自动管理着所有的进程(不需要用户代码干涉),并为这些进程合理分配可以执行时间。
4.进程可以通过派生新的进程来执行其它任务,不过每个进程还是都拥有自己的内存和数据栈等。
5.进程间可以通讯(发消息和数据),采用 进程间通信(IPC) 方式。

说明

1.多个进程可以在不同的 CPU 上运行,互不干扰
2.同一个CPU上,可以运行多个进程,由操作系统来自动分配时间片
3.由于进程间资源不能共享,需要进程间通信,来发送数据,接受消息等
4.多进程,也称为“并行”。

Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。

子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID

Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程

print(f"开始的进程是{os.getpid()}")
pid = os.fork()

if pid == 0 :
    print(f"我是子进程{os.getpid()},我的父进程是{os.getppid()}")
else:
    print(f"我是父进程{os.getpid()},我创建了子进程是{pid}")

运行结果:

开始的进程是9443
我是父进程9443,我创建了子进程是9444
我是子进程9444,我的父进程是9443

因为python是跨平台的,所以上面的是在Linux中,而在windows中呢(虽然很少在Windows中写)? 是用multiprocessing这个模块,multiprocessing模块提供了一个Process类来代表一个进程对象, 以下的这个例子就是启动一个子进程并等待其结束。


def run_child(name):
    print(f"子进程的名字是{name},进程的pid是{os.getpid()}")

if __name__ == "__main__":
    print(f"父进程是{os.getpid()}")
    p = Process(target=run_child, args=('test',))
    print('子进程即将开始')
    p.start()
    p.join()
    print("子进程即将结束")

运行结果:

父进程是9409
子进程即将开始
子进程的名字是test,进程的pid是9410
子进程即将结束

python线程

定义

线程,是在进程中执行的代码。
一个进程下可以运行多个线程,这些线程之间共享主进程内申请的操作系统资源。
在一个进程中启动多个线程的时候,每个线程按照顺序执行。现在的操作系统中,也支持线程抢占,也就是说其它等待运行的线程,可以通过优先级,信号等方式,将运行的线程挂起,自己先运行。
多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。

使用

1.用户编写包含线程的程序(每个程序本身都是一个进程)
2.操作系统“程序切换”进入当前进程
3.当前进程包含了线程,则启动线程
4.多个线程,则按照顺序执行,除非抢占
5.多线程,也被称为”并发“执行。

特性

1.线程,必须在一个存在的进程中启动运行
2.线程使用进程获得的系统资源,不会像进程那样需要申请CPU等资源
3.线程无法给予公平执行时间,它可以被其他线程抢占,而进程按照操作系统的设定分配执行时间
4.每个进程中,都可以启动很多个线程

多任务可以由多进程完成,也可以由一个进程内的多线程完成。并且,进程是由若干线程组成的,一个进程至少有一个线程。
Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行

# 新线程执行的代码:
def loop():
    print(f"子线程{threading.current_thread().name}正在执行")
    n = 0
    while n < 5:
        n = n + 1
        print(f"子线程{threading.current_thread().name}>>>{n}")
        time.sleep(1)
    print(f"子线程{threading.current_thread().name}结束了")

print(f"线程{threading.current_thread().name}正在运行啊")
t = threading.Thread(target=loop)
t.start()
t.join()
print(f"线程{threading.current_thread().name}结束")

运行结果:

线程MainThread正在运行啊
子线程Thread-1正在执行
子线程Thread-1>>>1
子线程Thread-1>>>2
子线程Thread-1>>>3
子线程Thread-1>>>4
子线程Thread-1>>>5
子线程Thread-1结束了
线程MainThread结束

由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread,子线程的名字在创建时指定, 如果不起名字Python就自动给线程命名为Thread-1,Thread-2。

进程和线程的区别

一个进程中的各个线程与主进程共享相同的资源,与进程间互相独立相比,线程之间信息共享和通信更加容易(都在进程中,并且共享内存等)。

线程一般以并发执行,正是由于这种并发和数据共享机制,使多任务间的协作成为可能。

进程一般以并行执行,这种并行能使得程序能同时在多个CPU上运行;

区别于多个线程只能在进程申请到的的“时间片”内运行(一个CPU内的进程,启动了多个线程,线程调度共享这个进程的可执行时间片),进程可以真正实现程序的“同时”运行(多个CPU同时运行)。

进程和线程的常用应用场景

一般来说,在Python中编写并发程序的经验:

计算密集型任务使用多进程
IO密集型(如:网络通讯)任务使用多线程,较少使用多进程.
这是由于 IO操作需要独占资源,比如:

网络通讯(微观上每次只有一个人说话,宏观上看起来像同时聊天)每次只能有一个人说话
文件读写同时只能有一个程序操作(如果两个程序同时给同一个文件写入 ‘a’, ‘b’,那么到底写入文件的哪个呢?)
都需要控制资源每次只能有一个程序在使用,在多线程中,由主进程申请IO资源,多线程逐个执行,哪怕抢占了,也是逐个运行,感觉上“多线程”并发执行了。

如果多进程,除非一个进程结束,否则另外一个完全不能用,显然多进程就“浪费”资源了。

问题:

我们前面编写的所有的Python程序,都是执行单任务的进程,也就是只有一个线程。如果我们要同时执行多个任务怎么办?
有两种解决方案:
1.一种是启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。
2.还有一种方法是启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务。
3.当然还有第三种方法,就是启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。

总结一下就是,多任务的实现有3种方式:

多进程模式;
多线程模式;
多进程+多线程模式。

python协程

协程,又称微线程,纤程。英文名Coroutine。协程,顾名思义就是协同合作的程序,一个程序里可以有很多协程,这些协程在主线程的调度下,表现得像是并发执行得一样;这样得好处就是:轻量级。

协程:程序自己进行调度,调度号:函数名,全部由程序自身完成。

线程是系统级别的它们由操作系统调度,而协程则是程序级别的由程序根据需要自己调度。在一个线程中会有很多函数,我们把这些函数称为子程序,在子程序执行过程中可以中断去执行别的子程序,而别的子程序也可以中断回来继续执行之前的子程序,这个过程就称为协程。也就是说在同一线程内一段代码在执行过程中会中断然后跳转执行别的代码,接着在之前中断的地方继续开始执行,类似与yield操作。

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。

人们常说:线程是轻量级的进程;到了协程这里,我们可以说协程是最轻量级的执行单元;不会再有比协程更轻的了;一个协程可以是一个函数或者任何可调用对象,它的调度也完全由程序自身控制,因此可以将其调度开销按照自身需要降到最低。这就是它的优势所在;

利用第三方库实现协程

Python通过yield提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持。
gevent是第三方库,通过greenlet实现协程,其基本思想是:

当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。详细请看:协程gevent

由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成:
使用gevent,可以获得极高的并发性能,但gevent只能在Unix/Linux下运行,在Windows下不保证正常安装和运行。
由于gevent是基于IO切换的协程,所以最神奇的是,我们编写的Web App代码,不需要引入gevent的包,也不需要改任何代码,仅仅在部署的时候,用一个支持gevent的WSGI服务器,立刻就获得了数倍的性能提升。

from gevent import monkey; monkey.patch_all()
import gevent
import urllib2

def f(url):
    print('GET: %s' % url)
    resp = urllib2.urlopen(url)
    data = resp.read()
    print('%d bytes received from %s.' % (len(data), url))

gevent.joinall([
        gevent.spawn(f, 'https://www.python.org/'),
        gevent.spawn(f, 'https://www.yahoo.com/'),
        gevent.spawn(f, 'https://github.com/'),
])

输出如下:

GET: https://www.python.org/
GET: https://www.yahoo.com/
GET: https://github.com/
45661 bytes received from https://www.python.org/.
14823 bytes received from https://github.com/.
304034 bytes received from https://www.yahoo.com/.

可以看到,当遇到阻塞(如IO操作,或者gevent.sleep(0)),gevent的调度器会自动切换协程去执行;而且它的协程都是一个个的函数,这样就很nice了:看上去像线程,其实是协程。

计算密集型 vs. IO密集型

是否采用多任务的第二个考虑是任务的类型。我们可以把任务分为计算密集型和IO密集型。

计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。

计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。

第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。

IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。

异步IO

异步IO:由消息中间件负责调度,调度号:消息队列。
考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。

现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。

对应到Python语言,单线程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python进程线程协程是实现并发编程的不同方式。 1. 进程(Process)是操作系统分配资源的基本单位,每个进程有独立的内存空间,互不干扰。进程之间的通信需要使用特定的机制,如管道、消息队列等。在Python,可以使用`multiprocessing`模块创建和管理进程。 2. 线程(Thread)是进程内的独立执行流,一个进程可以包含多个线程,它们共享相同的内存空间。线程之间的通信比进程更方便,可以使用共享内存或者全局变量。然而,由于全局解释器锁(GIL)的存在,同一时间只有一个线程在执行Python字节码,因此多线程在CPU密集型任务并不能提高性能。但是对于I/O密集型任务,多线程可以提升效率。Python内置的`threading`模块提供了对线程的支持。 3. 协程(Coroutine)是一种轻量级的线程,由程序控制在特定位置进行挂起和恢复的并发执行。协程能够在执行过程被暂停和继续,并且可以通过yield语句进行交互式通信。Python协程通过生成器函数(generator function)和`asyncio`库来实现。 总结一下: - 进程是资源分配的基本单位,进程之间资源独立,通信复杂。 - 线程进程内的执行流,共享内存,通信相对方便,但受到GIL的限制。 - 协程是一种轻量级的线程,可以在特定位置挂起和恢复执行,并通过yield语句进行通信。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值