并发编程

理论知识-操作系统

2022年7月25日

14:30

操作系统

现代的计算机系统主要是由一个或者多个处理器,主存,硬盘,键盘,鼠标,显示器,打印机,网络接口及其他输入输出设备组成。

一般而言,现代计算机系统是一个复杂的系统。

其一:如果每位应用程序员都必须掌握该系统所有的细节,那就不可能再编写代码了(严重影响了程序员的开发效率:全部掌握这些细节可能需要一万年....)

其二:并且管理这些部件并加以优化使用,是一件极富挑战性的工作,于是,计算安装了一层软件(系统软件),称为操作系统。它的任务就是为用户程序提供一个更好、更简单、更清晰的计算机模型,并管理刚才提到的所有设备。

总结:

程序员无法把所有的硬件操作细节都了解到,管理这些硬件并且加以优化使用是非常繁琐的工作,这个繁琐的工作就是操作系统来干的,有了他,程序员就从这些繁琐的工作中解脱了出来,只需要考虑自己的应用软件的编写就可以了,应用软件直接使用操作系统提供的功能来间接使用硬件。

什么是操作系统

精简的说的话,操作系统就是一个协调、管理和控制计算机硬件资源和软件资源的控制程序。操作系统所处的位置如图1

img

#操作系统位于计算机硬件与应用软件之间,本质也是一个软件。操作系统由操作系统的内核(运行于内核态,管理硬件资源)以及系统调用(运行于用户态,为应用程序员写的应用程序提供系统调用接口)两部分组成,所以,单纯的说操作系统是运行于内核态的,是不准确的。

操作系统应该分成两部分功能:

#一:隐藏了丑陋的硬件调用接口,为应用程序员提供调用硬件资源的更好,更简单,更清晰的模型(系统调用接口)。

应用程序员有了这些接口后,就不用再考虑操作硬件的细节,专心开发自己的应用程序即可。例如:操作系统提供了文件这个抽象概念,对文件的操作就是对磁盘的操作,有了文件我们无需再去考虑关于磁盘的读写控制(比如控制磁盘转动,移动磁头读写数据等细节),

#二:将应用程序对硬件资源的竞态请求变得有序化例如:

很多应用软件其实是共享一套计算机硬件,比方说有可能有三个应用程序同时需要申请打印机来输出内容,那么a程序竞争到了打印机资源就打印,然后可能是b竞争到打印机资源,也可能是c,这就导致了无序,打印机可能打印一段a的内容然后又去打印c...,操作系统的一个功能就是将这种无序变得有序。

操作系统与普通软件的区别

1.主要区别是:

你不想用暴风影音了你可以选择用迅雷播放器或者干脆自己写一个,但是你无法写一个属于操作系统一部分的程序(时钟中断处理程序),操作系统由硬件保护,不能被用户修改。

2.操作系统与用户程序的差异并不在于二者所处的地位。

特别地,操作系统是一个大型、复杂、长寿的软件,

  • 大型:linux或windows的源代码有五百万行数量级。按照每页50行共1000行的书来算,五百万行要有100卷,要用一整个书架子来摆置,这还仅仅是内核部分。用户程序,如GUI,库以及基本应用软件(如windows Explorer等),很容易就能达到这个数量的10倍或者20倍之多。
  • 长寿:操作系统很难编写,如此大的代码量,一旦完成,操作系统所有者便不会轻易扔掉,再写一个。而是在原有的基础上进行改进。(基本上可以把windows95/98/Me看出一个操作系统,而windows NT/2000/XP/Vista则是两位一个操作系统,对于用户来说它们十分相似。还有UNIX以及它的变体和克隆版本也演化了多年,如System V版,Solaris以及FreeBSD等都是Unix的原始版,不过尽管linux非常依照UNIX模式而仿制,并且与UNIX高度兼容,但是linux具有全新的代码基础)

操作系统发展史

第一代计算机(1940~1955):真空管和穿孔卡片

第二代计算机1955~1965**)****:晶体管和批处理系统

第三代计算机1965~1980**)****:集成电路芯片和多道程序设计

第四代计算机(1980~至今):个人计算机

多道技术

cpu在执行一个任务的过程中,若需要操作硬盘,则发送操作硬盘的指令,指令一旦发出,硬盘上的机械手臂滑动读取数据到内存中,这一段时间,cpu需要等待,时间可能很短,但对于cpu来说已经很长很长,长到可以让cpu做很多其他的任务,如果我们让cpu在这段时间内切换到去做其他的任务,这样cpu不就充分利用了吗。这正是多道技术产生的技术背景

多道技术:

多道技术中的多道指的是多个程序,多道技术的实现是为了解决多个程序竞争或者说共享同一个资源(比如cpu)的有序调度问题,解决方式即多路复用,多路复用分为时间上的复用和空间上的复用。

空间上的复用:

将内存分为几部分,每个部分放入一个程序,这样,同一时间内存中就有了多道程序。

img

时间上的复用:

当一个程序在等待I/O时,另一个程序可以使用cpu,如果内存中可以同时存放足够多的作业,则cpu的利用率可以接近100%,类似于我们小学数学所学的统筹方法。(操作系统采用了多道技术后,可以控制进程的切换,或者说进程之间去争抢cpu的执行权限。这种切换不仅会在一个进程遇到io时进行,一个进程占用cpu时间过长也会切换,或者说被操作系统夺走cpu的执行权限)

并发:看起来像同时运行的

并行:真正意义上的同时执行

总结

#一 操作系统的作用:

1:隐藏丑陋复杂的硬件接口,提供良好的抽象接口

2:管理、调度进程,并且将多个进程对硬件的竞争变得有序

#二 多道技术:、

1.产生背景:针对单核,实现并发

ps:现在的主机一般是多核,那么每个核都会利用多道技术有4个cpu,运行于cpu1的某个程序遇到io阻塞,会等到io结束再重新调度,会被调度到4个cpu中的任意一个,具体由操作系统调度算法决定。

2.空间上的复用:如内存中同时有多道程序

3.时间上的复用:复用一个cpu的时间片强调:遇到io切,占用cpu时间过长也切,核心在于切之前将进程的状态保存下来,这样才能保证下次切换回来时,能基于上次切走的位置继续运行

2022年10月9日

14:06

进程

2022年7月27日

0:07

什么是进程

进程:正在进行的一个过程或者说一个任务。而负责执行任务则是cpu。

举例(单核+多道,实现多个进程的并发执行)

程序和进程的区别

程序是存储在硬盘上的代码

进程:是程序在运行过程

进程的调度

  • 先来先服务调度算法

长作业有利,短作用不利

  • 短作业优先调度算法

短作业有利,长作业不利

  • 时间片轮转法+多级反馈队列

理论进程的创建

\1. 系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)

\2. 一个进程在运行过程中开启了子进程(如nginx开启多进程,os.fork,subprocess.Popen等)

\3. 用户的交互式请求,而创建一个新进程(如用户双击暴风影音)

\4. 一个批处理作业的初始化(只在大型机的批处理系统中应用)

无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的:

\1. 在UNIX中该系统调用是:fork,fork会创建一个与父进程一模一样的副本,二者有相同的存储映像、同样的环境字符串和同样的打开文件(在shell解释器进程中,执行一个命令就会创建一个子进程)

\2. 在windows中该系统调用是:CreateProcess,CreateProcess既处理进程的创建,也负责把正确的程序装入新进程。

关于创建的子进程,UNIX和windows

1.相同的是:进程创建后,父进程和子进程有各自不同的地址空间(多道技术要求物理层面实现进程之间内存的隔离),任何一个进程的在其地址空间中的修改都不会影响到另外一个进程。

2.不同的是:在UNIX中,子进程的初始地址空间是父进程的一个副本,提示:子进程和父进程是可以有只读的共享内存区的。但是对于windows系统来说,从一开始父进程与子进程的地址空间就是不同的。

理论进程的终止(了解)

\1. 正常退出(自愿,如用户点击交互式页面的叉号,或程序执行完毕调用发起系统调用正常退出,在linux中用exit,在windows中用ExitProcess)

\2. 出错退出(自愿,python a.py中a.py不存在)

\3. 严重错误(非自愿,执行非法指令,如引用不存在的内存,1/0等,可以捕捉异常,try...except...)

\4. 被其他进程杀死(非自愿,如kill -9)

理论进程的层次结构

无论UNIX还是windows,进程只有一个父进程,不同的是:

\1. 在UNIX中所有的进程,都是以init进程为根,组成树形结构。父子进程共同组成一个进程组,这样,当从键盘发出一个信号时,该信号被送给当前与键盘相关的进程组中的所有成员。

\2. 在windows中,没有进程层次的概念,所有的进程都是地位相同的,唯一类似于进程层次的暗示,是在创建进程时,父进程得到一个特别的令牌(称为句柄),该句柄可以用来控制子进程,但是父进程有权把该句柄传给其他子进程,这样就没有层次了。

进程的状态

img

进程并发的实现

进程并发的实现在于,硬件中断一个正在运行的进程,把此时进程运行的所有状态保存下来,为此,操作系统维护一张表格,即进程表(process table),每个进程占用一个进程表项(这些表项也称为进程控制块)

img

该表存放了进程状态的重要信息:程序计数器、堆栈指针、内存分配状况、所有打开文件的状态、帐号和调度信息,以及其他在进程由运行态转为就绪态或阻塞态时,必须保存的信息,从而保证该进程在再次启动时,就像从未被中断过一样。

同步\异步and阻塞\非阻塞

同步

#``所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回。按照这个定义,其实绝大多数函数都是同步调用。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。

异步

#异步的概念和同步相对。当一个异步功能调用发出后,调用者不能立刻得到结果。当该异步功能完成后,通过状态、通知或回调来通知调用者。如果异步功能用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一 种很严重的错误)。如果是使用通知的方式,效率则很高,因为异步功能几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。

阻塞

#阻塞调用是指调用结果返回之前,当前线程会被挂起(如遇到io操作)。函数只有在得到结果之后才会将阻塞的线程激活。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。

非阻塞

#非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前也会立刻返回,同时该函数不会阻塞当前线程。

总结

#1. 同步与异步针对的是函数/任务的调用方式:同步就是当一个进程发起一个函数(任务)调用的时候,一直等到函数(任务)完成,而进程继续处于激活状态。而异步情况下是当一个进程发起一个函数(任务)调用的时候,不会等函数返回,而是继续往下执行当,函数返回的时候通过状态、通知、事件等方式通知进程任务完成。

#2. 阻塞与非阻塞针对的是进程或线程:阻塞是当请求不能满足的时候就将进程挂起,而非阻塞则不会阻塞当前进程

multiprocessing进程

2022年7月27日

12:50

查看进程线程

Windows查看

查看所有

Windows tasklist

指定查看

tasklist | findstr pid

linux查看

UNIX ps aux

指定 ps aux | grep pid

介绍

multiprocessing模块的功能众多:支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。

与线程不同,进程没有任何共享状态,进程修改的数据,改动仅限于该进程内。

Process类的介绍

创建进程的类

Process([group [, target [, name [, args [, kwargs]]]]])``,由该类实例化得到的对象,表示一个子进程中的任务(尚未启动)
强调:`
 `1. ``需要使用关键字的方式来指定参数`
 `2. args``指定的为传给``target``函数的位置参数,是一个元组形式,必须有逗号

参数

group``参数未使用,值始终为None
 
target``表示调用对象,即子进程要执行的任务
 
args``表示调用对象的位置参数元组,args=(1,2,'egon',)
 
kwargs``表示调用对象的字典,kwargs={'name':'egon','age':18}
 
name``为子进程的名称
 

方法介绍

p.start()``:
启动进程,并调用该子进程中的``p.run()
p.run():
进程启动时运行的方法,正是它去调用``target``指定的函数,我们自定义类的类中一定要实现该方法
 
p.terminate():
强制终止进程``p``,不会进行任何清理操作,如果``p``创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果``p``还保存了一个锁那么也将不会被释放,进而导致死锁
p.is_alive():
如果``p``仍然运行,返回``True
 
p.join([timeout]):
主线程等待``p``终止(强调:是主线程处于等的状态,而``p``是处于运行的状态)。``timeout``是可选的超时时间,需要强调的是,``p.join``只能``join``住``start``开启的进程,而不能``join``住``run``开启的进程
 

属性介绍

p.daemon``:
默认值为``False``,如果设为``True``,代表``p``为后台运行的守护进程,当``p``的父进程终止时,``p``也随之终止,并且设定为``True``后,``p``不能创建自己的新进程,必须在``p.start()``之前设置
p.name:
进程的名称
p.pid``:
进程的``pid
p.exitcode:
进程在运行时为``None``、如果为–``N``,表示被信号``N``结束``(``了解即可``)
p.authkey:
进程的身份验证键``,``默认是由``os.urandom()``随机生成的``32``字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功(了解即可)

案例:

from multiprocessing import Process,Queue`
 `import time,random,os`
 `def consumer(q):`
 `  while True:`
 `    res=q.get()`
 `    time.sleep(random.randint(1,3))`
 `    print('\033[45m%s ``吃`` %s\033[0m'%(os.getpid(),res))
def producer(q):`
 `  fori inrange(10):`
 `    time.sleep(random.randint(1,3))`
 `    res='``包子``%s'%i`
 `    q.put(res)`
 `    print('\033[44m%s ``生产了`` %s\033[0m'%(os.getpid(),res))
if__name__== '__main__':`
 `  q=Queue()`
 `  #``生产者们``:``即厨师们
p1=Process(target=producer,args=(q,))
#``消费者们``:``即吃货们
c1=Process(target=consumer,args=(q,))
#``开始``  
p1.start()
c1.start()`
 `print('``主``')

查看进程id

一。导入 from multiprocessing import current_proccess

current_process().pid

二。 os.getpid() 获取当前进程pid

os.getppid() 获取父进程的pid

僵尸进程与孤儿进程

一:僵尸进程(有害)

僵尸进程:

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

二:孤儿进程(无害)

孤儿进程:

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。

解决僵尸进程

等待父进程正常结束后会调用wait/waitpid去回收僵尸进程
但如果父进程是一个死循环,永远不会结束,那么该僵尸进程就会一直存在,僵尸进程过多,就是有害的
解决方法一:杀死父进程
解决方法二:对开启的子进程应该记得使用join,join会回收僵尸进程

解决方法三:

signal(``参数一``,``参数二``)

参数一:我们要进行处理的信号。系统的信号我们可以再终端键入 kill -l查看(共64个)。其实这些信号时系统定义的宏。

参数二:我们处理的方式(是系统默认还是忽略还是捕获)。可以写一个handdle函数来处理我们捕获的信号。

SIGCHLD信号

子进程结束时, 父进程会收到这个信号。

如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。

SIG_ING

忽略的意思

守护进程

主进程创建守护进程

其一:守护进程会在主进程代码执行结束后就终止

其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children

注意:进程之间是互相独立的,主进程代码运行结束,守护进程随即终止

使用:``p.daemon=True #``一定要在``p.start()``前设置``,``设置``p``为守护进程``,``禁止``p``创建子进程``,``并且父进程代码执行结束``,p``即终止运行

案例:

frommultiprocessing import Process`
 `import time`
 `import random
class Piao(Process):`
 `  def__init__(self,name):`
 `    self.name=name`
 `    super().__init__()`
 `  def run(self):`
 `    print('%s is piaoing'%self.name)`
 `    time.sleep(random.randrange(1,3))`
 `    print('%s is piao end'%self.name)
p=Piao('egon')`
 `p.daemon=True #``一定要在``p.start()``前设置``,``设置``p``为守护进程``,``禁止``p``创建子进程``,``并且父进程代码执行结束``,p``即终止运行
p.start()`
 `print('``主``')

进程锁

进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的,

而共享带来的是竞争,竞争带来的结果就是错乱,如何控制,就是加锁处理

案例一:多个进程共享同一打印终端

未加锁:#并发运行,效率高,但竞争同一打印终端,带来了打印错乱

加锁: #由并发变成了串行,牺牲了运行效率,但避免了竞争

案例二:多个进程共享同一文件

未加锁:并发写入,效率高,但是竞争同一个文件,到时数据错乱

加锁:并发变成了串行,牺牲了运行效率,但是保证了数据的安全

总结:

#加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。虽然可以用文件共享数据实现进程间通信,但问题是:

1.效率低(共享数据基于文件,而文件是硬盘上的

数据)
2.需要自己加锁处理

#因此我们最好找寻一种解决方案能够兼顾:

1、效率高(多个进程共享一块内存的数据)

2、帮我们处理好锁问题。

这就是mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。

1 队列和管道都是将数据存放于内存中

2 队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来,
我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性。

队列

进程彼此之间互相隔离,要实现进程间通信(IPC),multiprocessing模块支持两种形式:队列和管道,这两种方式都是使用消息传递的

创建队列的类(底层就是以管道和锁定的方式实现):

Queue([maxsize]):创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。

maxsize是队列中允许最大项数,省略则为默认大小。

方法介绍:

q.put
方法用以插入数据到队列中,``put``方法还有两个可选参数:``blocked``和``timeout``。如果``blocked``为``True``(默认值),并且``timeout``为正值,该方法会阻塞``timeout``指定的时间,直到该队列有剩余的空间。如果超时,会抛出``Queue.Full``异常。如果``blocked``为``False``,但该``Queue``已满,会立即抛出``Queue.Full``异常。
 
q.get
方法可以从队列读取并且删除一个元素。
get``方法有两个可选参数:``blocked``和``timeout``。
如果``blocked``为``True``(默认值),并且``timeout``为正值,那么在等待时间内没有取到任何元素,会抛出``Queue.Empty``异常。
如果``blocked``为``False``,有两种情况存在,如果``Queue``有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出``Queue.Empty``异常``.
 
q.get_nowait():``同``q.get(False)
q.put_nowait():``同``q.put(False)
 
q.empty():
调用此方法时``q``为空则返回``True``,该结果不可靠,比如在返回``True``的过程中,如果队列中又加入了项目。
 
q.full()``:
调用此方法时``q``已满则返回``True``,该结果不可靠,比如在返回``True``的过程中,如果队列中的项目被取走。
 
q.qsize():
返回队列中目前项目的正确数量,结果也不可靠,理由同``q.empty()``和``q.full()``一样
 
q.cancel_join_thread():
不会在进程退出时自动连接后台线程。可以防止``join_thread()``方法阻塞
 
q.close():
关闭队列,防止队列中加入更多数据。
调用此方法,后台线程将继续写入那些已经入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果``q``被垃圾收集,将调用此方法。关闭队列不会在队列使用者中产生任何类型的数据结束信号或异常。例如,如果某个使用者正在被阻塞在``get()``操作上,关闭生产者中的队列不会导致``get()``方法返回错误。
 
q.join_thread()``:
连接队列的后台线程。此方法用于在调用``q.close()``方法之后,等待所有队列项被消耗。默认情况下,此方法由不是``q``的原始创建者的所有进程调用。调用``q.cancel_join_thread``方法可以禁止这种行为

进程池

在利用Python进行系统管理的时候,特别是同时操作多个文件目录,或者远程控制多台主机,并行操作可以节约大量的时间。多进程是实现并发的手段之一,需要注意的问题是:

  1. 很明显需要并发执行的任务通常要远大于核数
  2. 一个操作系统不可能无限开启进程,通常有几个核就开几个进程
  3. 进程开启过多,效率反而会下降(开启进程是需要占用系统资源的,而且开启多余核数目的进程也无法做到并行)

例如当被操作对象数目不大时,可以直接利用multiprocessing中的Process动态成生多个进程,十几个还好,但如果是上百个,上千个。。。手动的去限制进程数量却又太过繁琐,此时可以发挥进程池的功效。

我们就可以通过维护一个进程池来控制进程数目,比如httpd的进程模式,规定最小进程数和最大进程数...

ps:对于远程过程调用的高级应用程序而言,应该使用进程池,Pool可以提供指定数量的进程,供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,就重用进程池中的进程。

创建进程池的类:

如果指定numprocess为3,则进程池会从无到有创建三个进程,然后自始至终使用这三个进程去执行所有任务,不会开启其他进程

Pool([numprocess [,initializer [, initargs]]]):``创建进程池`` 

参数介绍

1 numprocess:``要创建的进程数,如果省略,将默认使用``cpu_count()``的值`
 `2 initializer``:是每个工作进程启动时要执行的可调用对象,默认为``None`
 `3 initargs``:是要传给``initializer``的参数组

方法介绍

p.apply(func[,args[,kwargs]]):
在一个池工作进程中执行``func(*args,**kwargs),``然后返回结果。需要强调的是:此操作并不会在所有池工作进程中并执行``func``函数。如果要通过不同参数并发地执行``func``函数,必须从不同线程调用``p.apply()``函数或者使用``p.apply_async()
 
p.apply_async(func[,args[,kwargs]]):
在一个池工作进程中执行``func(*args,**kwargs),``然后返回结果。此方法的结果是``AsyncResult``类的实例,``callback``是可调用对象,接收输入参数。当``func``的结果变为可用时,将理解传递给``callback``。``callback``禁止执行任何阻塞操作,否则将接收其他异步操作中的结果。
 
p.close():
关闭进程池,防止进一步操作。如果所有操作持续挂起,它们将在工作进程终止前完成
 
P.jion():
等待所有工作进程退出。此方法只能在``close``()或``teminate()``之后调用
 
obj.get():
返回结果,如果有必要则等待结果到达。``timeout``是可选的。如果在指定时间内还没有到达,将引发一场。如果远程操作中引发了异常,它将在调用此方法时再次被引发。
 
obj.ready():
如果调用完成,返回``True
 
obj.successful():
如果调用完成且没有引发异常,返回``True``,如果在结果就绪之前调用此方法,引发异常
 
obj.wait([timeout]):
等待结果变为可用。
 
obj.terminate()``:
立即终止所有工作进程,同时不执行任何清理或结束任何挂起工作。如果``p``被垃圾回收,将自动调用此函数

案例

from multiprocessing import Pool
import os,time
def work(n):
  print('%s run' %os.getpid())
  time.sleep(3)
  return n**2
if __name__ == '__main__':
  p=Pool(3) #``进程池中从无到有创建三个进程``,``以后一直是这三个进程在执行任务
  res_l=[]
  for i in range(10):
    res=p.apply_async(work,args=(i,)) #``同步运行``,``阻塞、直到本次任务执行完毕拿到``res
    res_l.append(res)
  #``异步``apply_async``用法:如果使用异步提交的任务,主进程需要使用``jion``,等待进程池内任务都处理完,然后可以用``get``收集结果,否则,主进程结束,进程池可能还没来得及执行,也就跟着一起结束了
  p.close()
  p.join()
  for res in res_l:
    print(res.get()) #``使用``get``来获取``apply_aync``的结果``,``如果是``apply,``则没有``get``方法``,``因为``apply``是同步执行``,``立刻获取结果``,``也根本无需``get

线程

2022年7月27日

0:07

什么是线程

线程顾名思义,就是一条流水线工作的过程,一条流水线必须属于一个车间,一个车间的工作过程是一个进程

进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。

多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。

为何要用多线程

多线程指的是,在一个进程中开启多个线程,简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。详细的讲分为4点:

\1. 多线程共享一个进程的地址空间

\2. 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用

\3. 若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度。

\4. 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python)

经典的线程模型

多个线程共享同一个进程的地址空间中的资源,是对一台计算机上多个进程的模拟,有时也称线程为轻量级的进程

而对一台计算机上多个进程,则共享物理内存、磁盘、打印机等其他物理资源。

多线程的运行也多进程的运行类似,是cpu在多个线程之间的快速切换。

img

不同的进程之间是充满敌意的,彼此是抢占、竞争cpu的关系,如果迅雷会和QQ抢资源。而同一个进程是由一个程序员的程序创建,所以同一进程内的线程是合作关系,一个线程可以访问另外一个线程的内存地址,大家都是共享的,一个线程干死了另外一个线程的内存,那纯属程序员脑子有问题。

类似于进程,每个线程也有自己的堆栈

img

不同于进程,线程库无法利用时钟中断强制线程让出CPU,可以调用thread_yield运行线程自动放弃cpu,让另外一个线程运行。

线程通常是有益的,但是带来了不小程序设计难度,线程的问题是:

\1. 父进程有多个线程,那么开启的子线程是否需要同样多的线程

如果是,那么附近中某个线程被阻塞,那么copy到子进程后,copy版的线程也要被阻塞吗,想一想nginx的多线程模式接收用户连接。

\2. 在同一个进程中,如果一个线程关闭了问题,而另外一个线程正准备往该文件内写内容呢?

如果一个线程注意到没有内存了,并开始分配更多的内存,在工作一半时,发生线程切换,新的线程也发现内存不够用了,又开始分配更多的内存,这样内存就被分配了多次,这些问题都是多线程编程的典型问题,需要仔细思考和设计。

POSIX线程

为了实现可移植的线程程序,IEEE在IEEE标准1003.1c中定义了线程标准,它定义的线程包叫Pthread。大部分UNIX系统都支持该标准,简单介绍如下

img

在用户空间实现的线程

线程的实现可以分为两类:用户级线程(User-Level Thread)和内核线线程(Kernel-Level Thread),后者又称为内核支持的线程或轻量级进程。在多线程操作系统中,各个系统的实现方式并不相同,在有的系统中实现了用户级线程,有的系统中实现了内核级线程。

用户级线程内核的切换由用户态程序自己控制内核切换,不需要内核干涉,少了进出内核态的消耗,但不能很好的利用多核Cpu,目前Linux pthread大体是这么做的。

img

在用户空间模拟操作系统对进程的调度,来调用一个进程中的线程,每个进程中都会有一个运行时系统,用来调度线程。此时当该进程获取cpu时,进程内再调度出一个线程去执行,同一时刻只有一个线程执行。

在内核空间实现的线程

内核级线程:切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态;可以很好的利用smp,即利用多核cpu。windows线程就是这样的。

img

用户级与内核级线程的对比

一: 以下是用户级线程和内核级线程的区别:

  1. 内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。
  2. 用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。
  3. 用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。
  4. 在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。
  5. 用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是可以运行在任何状态下的程序。

二: 内核线程的优缺点

优点:

  1. 当有多个处理机时,一个进程的多个线程可以同时执行。

缺点:

  1. 由内核进行调度。

三: 用户进程的优缺点

优点:

  1. 线程的调度不需要内核直接参与,控制简单。
  2. 可以在不支持线程的操作系统中实现。
  3. 创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。
  4. 允许每个进程定制自己的调度算法,线程管理比较灵活。
  5. 线程能够利用的表空间和堆栈空间比内核级线程多。
  6. 同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。另外,页面失效也会产生同样的问题。

缺点:

  1. 资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用

threading线程

2022年7月29日

13:37

基本使用和进程类似

方法

Thread实例对象的方法

# isAlive(): 返回线程是否活动的。

# getName(): 返回线程名。

# setName(): 设置线程名。

threading模块提供的一些方法:

# threading.currentThread(): 返回当前的线程变量。

# threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。

# threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

守护线程

无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁

需要强调的是:运行完毕并非终止运行

#1.对主进程来说,运行完毕指的是主进程代码运行完毕

#2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕

GIL锁

首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL

介绍

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

可以肯定的一点是:保护不同的数据的安全,就应该加不同的锁。

要想了解GIL,首先确定一点:每次执行python程序,都会产生一个独立的进程。

定义:

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple `
 `native threads from executing Python bytecodes at once. This lock is necessary mainly `
 `because CPython``’``s memory management is not thread-safe. (However, since the GIL `
 `exists, other features have grown to depend on the guarantees that it enforces.)
 
翻译

在CPython中,全局解释器锁(GIL)是一个防止多重锁的互斥锁

本机线程一次执行Python字节码。这把锁主要是必需的

因为CPython的内存管理不是线程安全的。(然而,自从GIL

存在,其他功能已经发展到依赖于它实施的保证),

 
 

img

GIL与Lock

GIL保护的是解释器级的数据,保护用户自己的数据则需要自己加锁处理,如下图

img

三个需要注意的点:

#1.线程抢的是GIL锁,GIL锁相当于执行权限,拿到执行权限后才能拿到互斥锁Lock,其他线程也可以抢到GIL,但如果发现Lock仍然没有被释放则阻塞,即便是拿到执行权限GIL也要立刻交出来

#2.join是等待所有,即整体串行,而锁只是锁住修改共享数据的部分,即部分串行,要想保证数据安全的根本原理在于让并发变成串行,join与互斥锁都可以实现,毫无疑问,互斥锁的部分串行效率要更高

GIL与多线程

对计算来说,cpu越多越好,但是对于I/O来说,再多的cpu也没用

当然对运行一个程序来说,随着cpu的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析python的多线程到底有无用武之地

 

#分析:

我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:

方案一:开启四个进程

方案二:一个进程下,开启四个线程

#单核情况下,分析结果:

如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜

如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜

#多核情况下,分析结果:

如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜

如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜

#结论:

现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。

死锁和递归锁

from threading import RLock
R=``Rlock()

进程也有死锁与递归锁,在进程那里忘记说了,放到这里一切说了额

所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

解决方法,

递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。

这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:

mutexA=mutexB=threading.RLock() #``一个线程拿到锁,``counter``加``1,``该线程内又碰到加锁的情况,则``counter``继续加``1``,这期间所有其他线程都只能等待,等待该线程释放所有锁,即``counter``递减到``0``为止

信号量

fromthreadingimport,Semaphore
R=Semaphore(10) ``# ``同时实现10个锁

线程队列

class queue.Queue(maxsize=0) #先进先出

class queue.LifoQueue(maxsize=0) #结果(后进先出):

class queue.PriorityQueue(maxsize=0) #存储数据时可设置优先级的队列

 
import queue
q=queue.PriorityQueue()
#put``进入一个元组``,``元组的第一个元素是优先级``(``通常是数字``,``也可以是非数字之间的比较``),``数字越小优先级越高
q.put((20,'a'))
q.put((10,'b'))
q.put((30,'c'))
print(q.get())
print(q.get())
print(q.get())
'''
结果``(``数字越小优先级越高``,``优先级高的优先出队``):
(10, 'b')
(20, 'a')
(30, 'c')
'''

concurrent.futures``模块提供了高度封装的异步调用接口
ThreadPoolExecutor``:线程池,提供异步调用
ProcessPoolExecutor: ``进程池,提供异步调用
Both implement the same interface, which is defined by the abstract Executor class.
#2 ``基本方法
#submit(fn, *args, **kwargs)
异步提交任务
#map(func, *iterables, timeout=None, chunksize=1) 
取代``for``循环``submit``的操作
#shutdown(wait=True) 
相当于进程池的``pool.close()+pool.join()``操作
wait=True``,等待池内所有任务执行完毕回收完资源后才继续
wait=False``,立即返回,并不会等待池内的任务执行完毕
但不管``wait``参数为何值,整个程序都会等到所有任务执行完毕
submit``和``map``必须在``shutdown``之前
#result(timeout=None)
取得结果
#add_done_callback(fn)
回调函数

协程

2022年7月27日

0:07

并发的本质:

切换+保存状态

cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长或有一个优先级更高的程序替代了它

img

ps:在介绍进程理论时,提及进程的三种执行状态,而线程才是执行单位,所以也可以将上图理解为线程的三种状态

协程

1、协程:

单线程实现并发

在应用程序里控制多个任务的切换+保存状态

优点:

​ 应用程序级别速度要远远高于操作系统的切换

缺点:

​ 多个任务一旦有一个阻塞没有切,整个线程都阻塞在原地

​ 该线程内的其他的任务都不能执行了

​ 一旦引入协程,就需要检测单线程下所有的IO行为,

​ 实现遇到IO就切换,少一个都不行,以为一旦一个任务阻塞了,整个线程就阻塞了,

​ 其他的任务即便是可以计算,但是也无法运行了

2、协程序的目的:

想要在单线程下实现并发

并发指的是多个任务看起来是同时运行的

并发=切换+保存状态

协程介绍:

是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。、

需要强调的是:

python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)#2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)

对比操作系统控制线程的切换,用户在单线程内控制协程的切换

优点如下:

#1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
#2. 单线程内就可以实现并发的效果,最大限度地利用cpu

缺点如下:

#1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
#2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

总结协程特点:

必须在只有一个单线程里实现并发

修改共享数据不需加锁

用户程序里自己保存多个控制流的上下文栈

附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))

Gevent介绍

#安装pip3 install gevent

Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

#用法

g1=gevent.spawn(func,1,,2,3,x=4,y=5)
创建一个协程对象``g1``,``spawn``括号内第一个参数是函数名,如``eat``,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数``eat``的
g2=gevent.spawn(func2)
g1.join() #``等待``g1``结束`
 `g2.join() #``等待``g2``结束``#``或者上述两步合作一步:``gevent.joinall([g1,g2])`
 `g1.value#``拿到``func1``的返回值

GIL

2022年10月9日

14:07

一 、基本概念

以下概念都是在 Python 环境下

  1. Sync 同步编程
  2. Async 异步 ,是指在外观上看来程序不会等待,而是找出可执行的操作/任务/线程 继续执行
  3. Asyncio 单个主线程 多个不同的任务 task ,这些 future 对象 被 event loop 对象控制,就好像多线程版本的多个线程
  4. GIL 全局解释器锁 Global Interpret Lock
  5. Task 任务
  6. 并发 Concurrency 同一时刻只有一个任务/线程 在执行,只不过会不停的切换这些任务
  7. 并行 Parallelism 多个进程同一个时刻一起执行

一、GIL全局解释器锁

  • 全局解释器锁

在CPython中,全局解释器锁(GIL)是一个互斥锁,它可以防止多个本机线程同时执行Python代码。之所以需要这个锁,主要是因为CPython的内存管理不是线程安全的。(然而,自从GIL存在以来,其他特性已经逐渐依赖于它所执行的保证)

  • 什么是GIL
    全局解释器锁, 施加在解释器上的互斥锁

  • 为什么需要GIL
    由于CPython的内存管理时非线程安全,于是CPython就给解释器加上锁, 解决了安全问题.

  • GIL的加锁与解锁时机

    • 加锁的时机: 在调用解释器时立即加锁

    • 解锁的时机:

      • 当前线程遇到了IO时
      • 当前线程执行时间超过设定值时 , 一旦达到某个阈值 , CPU会通知线程保存状态切换线程 , 以此来保证数据安全
  • 总结 : 由于GIL锁的特性 , 我们需要考虑什么情况下用多线程什么情况下用多进程

    • 在单核情况下 , 无论是IO密集型还是计算密集型 , GIL都不会产生影响,多线程会更加经济实惠.

    • 在多核情况下 , IO密集型会受到GIL的影响

      • 对于计算密集型 , 需要并行处理 , 所以需要用到多进程
      • 对于IO密集型 , 由于IO时间较长 , 创建进程不经济 , 所以应该用多线程

二、GIL带来的问题

  • GIL的优缺点:

    • 优点:保证Cpython解释器内存管理的线程安全
    • 缺点: 同一进程内所有的线程同一时刻只能有一个执行, 也就说Cpython解释器的多线程无法实现并行

asyncio模块

2022年7月30日

19:55

协程 asyncio

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的方式定义协程

注意:

使用``async def``的形式定义
在协程中可以使用``await``关键字,注意,其后跟的是``"``可等待对象``"(``协程``, ``任务`` ``和`` Future)
协程不能直接执行,需要在``asyncio.run()``中执行,也可以跟在``await``后面
async``和``await``这两个关键字只能在协程中使用
其中,``asyncio.run(main, *, debug=False)``方法就是对``run_until_complete``进行了封装:
loop = events.new_event_loop()
return loop.run_until_complete(main)

Lock

class`` ``asyncio.Lock
实现一个用于`` asyncio ``任务的互斥锁。`` ``非线程安全。
asyncio ``锁可被用来保证对共享资源的独占访问。
使用`` Lock ``的推荐方式是通过`` `[async with](file:///C:/Users/FQCj/Desktop/python-3.10.5-docs-html/reference/compound_stmts.html#async-with)` ``语句``:
lock=asyncio.Lock()
# ... Later 
async with lock:
# access shared state
这等价于``:
lock=asyncio.Lock()
# ... Later
Await lock.acquire() 
try:
# access shared state
finally:
lock.release()
在`` 3.10 ``版更改``:`` ``Removed the`` ``loop`` ``parameter.
coroutine`` ``acquire()
获取锁。
此方法会等待直至锁为`` ``unlocked``,将其设为`` ``locked`` ``并返回`` ``True``。
当有一个以上的协程在`` `[acquire()](file:///C:/Users/FQCj/Desktop/python-3.10.5-docs-html/library/asyncio-sync.html#asyncio.Lock.acquire)` ``中被阻塞则会等待解锁,最终只有一个协程会被执行。
锁的获取是`` ``公平的``: ``被执行的协程将是第一个开始等待锁的协程。
release()
释放锁。
当锁为`` ``locked`` ``时,将其设为`` ``unlocked`` ``并返回。
如果锁为`` ``unlocked``,则会引发`` `[RuntimeError](file:///C:/Users/FQCj/Desktop/python-3.10.5-docs-html/library/exceptions.html#RuntimeError)`。
locked()
如果锁为`` ``locked`` ``则返回`` ``True``。
 
异步协程爬取框架
未设置并发度:
import asyncio
# ``获取事件循环
loop = asyncio.get_event_loop()
# ``根据协程语法定义一个协程方法
async def main(url):
  await get_url(url) ``# ``其他方法
# ``await ``不会等待直接进入下一个
创建``task``列表
tasks = [loop.create_task(main(url)) for url in urls]
# ``执行爬虫事件列表
loop.run_until_complete(asyncio.wait(tasks))
设置并发度
import asyncio
# ``获取事件循环
 
asem = asyncio.Semaphore(10) ``# ``设置10的最大并发 
loop = asyncio.get_event_loop()
# ``根据协程语法定义一个协程方法
async def main(url):
async with asem:
    await get_url(url) # ``其他方法
# await ``不会等待直接进入下一个
 
创建``task``列表
tasks = [loop.create_task(main(url)) for url in urls]
# ``执行爬虫事件列表
loop.run_until_complete(asyncio.wait(tasks))
 
 

img

img

并发度第二种方法:

i``mport 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())

第三种方法设置并发度:

 
Import asyncio
Async def foo(char:str, count: int):
For i in range(count):
print(f"{char}-{i}")
awaitasyncio.sleep(.5)
Async def main():
Await asyncio.gather(foo("A", 2), foo("B", 3), foo("C", 2))
if__name__ == '__main__':
asyncio.run(main())
 

创建一个任务或者说并发任务

asyncio.create_task(coro,`` ``*,`` ``name=None)
将`` ``coro`` ``协程`` ``封装为一个`` ``Task`` ``并调度其执行。返回`` Task ``对象。``一个异步方法``\``协程方法
name`` ``不为`` ``None``,它将使用`` ``Task.set_name()`` ``来设为任务的名称

运行 asyncio 程序

asyncio.run(coro, ***, debug=False)

执行 [coroutine](file:///C:/Users/FQCj/Desktop/python-3.10.5-docs-html/glossary.html#term-coroutine) coro 并返回结果。

此函数会运行传入的协程,负责管理 asyncio 事件循环,终结异步生成器,并关闭线程池。

当有其他 asyncio 事件循环在同一线程中运行时,此函数不能被调用。

如果 debug 为 True,事件循环将以调试模式运行。

此函数总是会创建一个新的事件循环并在结束时关闭之。它应当被用作 asyncio 程序的主入口点,理想情况下应当只被调用一次。

总结:

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

I/O模型

2022年7月27日

0:08

介绍

同步(synchronous) IO和异步(asynchronous) IO,阻塞(blocking) IO和非阻塞(non-blocking)IO分别是什么,到底有什么区别?这个问题其实不同的人给出的答案都可能不同,比如wiki,就认为asynchronous IO和non-blocking IO是一个东西。这其实是因为不同的人的知识背景不同,并且在讨论这个问题的时候上下文(context)也不相同。所以,为了更好的回答这个问题,我先限定一下本文的上下文。

本文讨论的背景是Linux环境下的network IO。本文最重要的参考文献是Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ”,6.2节“I/O Models ”,Stevens在这节中详细说明了各种IO的特点和区别,如果英文够好的话,推荐直接阅读。Stevens的文风是有名的深入浅出,所以不用担心看不懂。本文中的流程图也是截取自参考文献。

Stevens在文章中一共比较了五种IO Model:

  `` * blocking IO
  `` * nonblocking IO
  `` * IO multiplexing
  `` * signal driven IO
  `` * asynchronous IO
  ``由``signal driven IO``(信号驱动``IO``)在实际中并不常用,所以主要介绍其余四种``IO Model``。

再说一下IO发生时涉及的对象和步骤。对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,该操作会经历两个阶段:

#1)等待数据准备 (Waiting for the data to be ready)
#2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)

记住这两点很重要,因为这些IO模型的区别就是在两个阶段上各有不同的情况。

补充:

#1、输入操作:read、readv、recv、recvfrom、recvmsg共5个函数,如果会阻塞状态,则会经理wait data和copy data两个阶段,如果设置为非阻塞则在wait 不到data时抛出异常

#2、输出操作:write、writev、send、sendto、sendmsg共5个函数,在发送缓冲区满了会阻塞在原地,如果设置为非阻塞,则会抛出异常

#3、接收外来链接:accept,与输入操作类似

#4、发起外出链接:connect,与输出操作类似

阻塞IO(blocking IO)

在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:

img

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。

而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。

几乎所有的程序员第一次接触到的网络编程都是从listen()、send()、recv() 等接口开始的,使用这些接口可以很方便的构建服务器/客户机的模型。然而大部分的socket接口都是阻塞型的。如下图

ps:所谓阻塞型接口是指系统调用(一般是IO接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。

img

实际上,除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用recv(1024)的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。

一个简单的解决方案:

#在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。

该方案的问题是:

#开启多进程或都线程的方式,在遇到要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程本身也更容易进入假死状态。

改进方案:

#很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如websphere、tomcat和各种数据库等。

改进后方案其实也存在着问题:

#“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。

对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。

非阻塞IO(non-blocking IO)

Linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:

img

从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是用户就可以在本次到下次再发起read询问的时间间隔内做其他事情,或者直接再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存(这一阶段仍然是阻塞的),然后返回。

也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。

但是非阻塞IO模型绝不被推荐。

我们不能否则其优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在“”同时“”执行)。

但是也难掩其缺点:

#1. 循环调用recv()将大幅度推高CPU占用率;这也是我们在代码中留一句time.sleep(2)的原因,否则在低配主机下极容易出现卡机情况
#2. 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。

多路复用IO(IO multiplexing)

IO multiplexing这个词可能有点陌生,但是如果我说select/epoll,大概就都能明白了。有些地方也称这种IO方式为事件驱动IO(event driven IO)。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:

img

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而blocking IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

强调:

\1. 如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

\2. 在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

结论: select的优势在于可以处理多个连接,不适用于单个连接

该模型的优点:

#相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。

该模型的缺点:

#首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。如果需要实现更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异,所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。
#其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。

异步IO(Asynchronous I/O)

Linux下的asynchronous IO其实用得不多,从内核2.6版本才开始引入。先看一下它的流程:

img

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

生产者消费者模型

2022年7月27日

13:27

生产者消费者模型

在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。

为什么要使用生产者和消费者模式

在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。

什么是生产者消费者模式

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

基于队列实现生产者消费者模

frommultiprocessing import Process,Queue`
 `import time,random,os`
 `def consumer(q):`
 `  while True:`
 `    res=q.get()`
 `    time.sleep(random.randint(1,3))`
 `    print('\033[45m%s ``吃`` %s\033[0m'%(os.getpid(),res))
def producer(q):`
 `  fori inrange(10):`
 `    time.sleep(random.randint(1,3))`
 `    res='``包子``%s'%i`
 `    q.put(res)`
 `    print('\033[44m%s ``生产了`` %s\033[0m'%(os.getpid(),res))
if__name__== '__main__':`
 `  q=Queue()`
 `  #``生产者们``:``即厨师们``p1=Process(target=producer,args=(q,))
#``消费者们``:``即吃货们``c1=Process(target=consumer,args=(q,))
#``开始``  p1.start()`
 `  c1.start()`
 `  print('``主``')
 

解决死循环问题

方法一

此时的问题是主进程永远不会结束,原因是:生产者p在生产完后就结束了,但是消费者c在取空了q之后,则一直处于死循环中且卡在q.get()这一步。

解决方式无非是让生产者在生产完毕后,往队列中再发一个结束信号,这样消费者在接收到结束信号后就可以break出死循环

注意:结束信号None,不一定要由生产者发,主进程里同样可以发,但主进程需要等生产者结束后才应该发送该信号

方法二

#JoinableQueue([maxsize])``:这就像是一个``Queue``对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。
#``参数介绍:``  maxsize``是队列中允许最大项数,省略则无大小限制。``  
#``方法介绍:``  
JoinableQueue``的实例``p``除了与``Queue``对象相同的方法之外还具有:`
 `  q.task_done()``:使用者使用此方法发出信号,表示``q.get()``的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发``ValueError``异常`
 `  q.join():``生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用``q.task_done``()方法为止

生产者消费者模型总结

 
#``程序中有两类角色
一类负责生产数据(生产者)
一类负责处理数据(消费者)
 
#``引入生产者消费者模型为了解决的问题是:
平衡生产者与消费者之间的工作能力,从而提高程序整体处理数据的速度
 
#``如何实现:
生产者``<-->``队列``<——>``消费者
#``生产者消费者模型实现类程序的解耦和
 
posted @ 2022-11-17 12:42  小符玩代码  阅读(66)  评论(0)    收藏  举报