Python 进程和线程、协程浅析

版权声明:Shallow@版权所有 原创文章不经允许不得转载 https://blog.csdn.net/xili2532/article/details/89882511

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种方式:

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

计算密集型 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

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

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

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

猜你喜欢

转载自blog.csdn.net/xili2532/article/details/89882511