并发编程

目录

并发编程

并发储备知识

背景知识

引入:

# 何为进程?
顾名思义,进程即正在执行的一个过程。进程是对正在运行程序的一个抽象。

好像还是不明白?

比如使用qq,qq从硬盘中加载到了内存, cpu 去内存中取qq的指令去运行。这就是进程。那什么是进程结束了?qq运行完了我退出了qq,要从内存中释放了qq这段代码,这就是结束了进程。

那比如我启动了很多个程序,用进程进行描述会更方便统计。

思考:一个正在运行的程序怎么就叫运行起来了?

一个正在运行的程序怎么就叫运行起来了,是由操作系统发送指令给cpu再发送指令程序从硬盘加载到了内存,也是由操作系统发送指令控制cpu去内存中取值运行,整个过程都归操作系控制的。

而进程的概念是起源于操作系统的,是操作系统最核心的概念,操作系统所有其他的概念都是围绕进程展开的。

要了解进程,有必要了解一下操作系统的前世今生。操作系统发展史

PS:即使可以利用的cpu只有一个(早期的计算机确实如此),也能保证支持(伪)并发的能力。将一个单独的cpu变成多个虚拟的cpu(多道技术:时间多路复用和空间多路复用+硬件上支持隔离),没有进程的抽象,现代计算机将不复存在。

必备的理论基础:

#一 操作系统的作用:
    1:隐藏丑陋复杂的硬件接口,提供良好的抽象接口
    2:管理、调度进程,并且将多个进程对硬件的竞争变得有序

#二 多道技术:
    1.产生背景:针对单核,实现并发
    
    2.空间上的复用:如内存中同时有多道程序
    3.时间上的复用:复用一个cpu的时间片
       强调:遇到io切,占用cpu时间过长也切,核心在于切之前将进程的状态保存下来,这样
            才能保证下次切换回来时,能基于上次切走的位置继续运行
            
#三 现代计算机:
    现在的主机一般是多核,那么每个核都会利用多道技术
    有4个cpu,运行于cpu1的某个程序遇到io阻塞,会等到io结束再重新调度,会被调度到4个
    cpu中的任意一个,具体由操作系统调度算法决定。
# 1、串行:
    一个任务完完整整地运行完毕后,才能运行下一个任务

# 2、并发
    看起来多个任务是同时运行的即可,单核也可以实现并发

# 3、并行:
    真正意义上多个任务的同时运行,只有多核才实现并行

一 为什么要有操作系统

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

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

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

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

总结:

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

二 什么是操作系统

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

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

img

                                                                  图1

细说的话,操作系统应该分成两部分功能:

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

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

详解:

#作用一:为应用程序提供如何使用硬件资源的抽象
例如:操作系统提供了文件这个抽象概念,对文件的操作就是对磁盘的操作,有了文件我们无需再去考虑关于磁盘的读写控制

注意:
操作系统提供给应用程序的该抽象是简单,清晰,优雅的。为何要提供该抽象呢?
硬件厂商需要为操作系统提供自己硬件的驱动程序(设备驱动,这也是为何我们要使用声卡,就必须安装声卡驱动。。。),厂商为了节省成本或者兼容旧的硬件,它们的驱动程序是复杂且丑陋的
操作系统就是为了隐藏这些丑陋的信息,从而为用户提供更好的接口
这样用户使用的shell,Gnome,KDE看到的是不同的界面,但其实都使用了同一套由linux系统提供的抽象接口


#作用二:管理硬件资源
现代的操作系统运行同时运行多道程序,操作系统的任务是在相互竞争的程序之间有序地控制对处理器、存储器以及其他I/O接口设备的分配。
例如:
同一台计算机上同时运行三个程序,它们三个想在同一时刻在同一台计算机上输出结果,那么开始的几行可能是程序1的输出,接着几行是程序2的输出,然后又是程序3的输出,最终将是一团糟(程序之间是一种互相竞争资源的过程)
操作系统将打印机的结果送到磁盘的缓冲区,在一个程序完全结束后,才将暂存在磁盘上的文件送到打印机输出,同时其他的程序可以继续产生更多的输出结果(这些程序的输出没有真正的送到打印机),这样,操作系统就将由竞争产生的无序变得有序化。

img

                                                 图 2

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

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):真空管和插件板

第一代计算机的产生背景:

第一代之前人类是想用机械取代人力,第一代计算机的产生是计算机由机械时代进入电子时代的标志,从Babbage失败之后一直到第二次世界大战,数字计算机的建造几乎没有什么进展,第二次世界大战刺激了有关计算机研究的爆炸性进展。

lowa州立大学的john Atanasoff教授和他的学生Clifford Berry建造了据认为是第一台可工作的数字计算机。该机器使用300个真空管。大约在同时,Konrad Zuse在柏林用继电器构建了Z3计算机,英格兰布莱切利园的一个小组在1944年构建了Colossus,Howard Aiken在哈佛大学建造了Mark 1,宾夕法尼亚大学的William Mauchley和他的学生J.Presper Eckert建造了ENIAC。这些机器有的是二进制的,有的使用真空管,有的是可编程的,但都非常原始,设置需要花费数秒钟时间才能完成最简单的运算。

在这个时期,同一个小组里的工程师们,设计、建造、编程、操作及维护同一台机器,所有的程序设计是用纯粹的机器语言编写的,甚至更糟糕,需要通过成千上万根电缆接到插件板上连成电路来控制机器的基本功能。没有程序设计语言(汇编也没有),操作系统则是从来都没听说过。使用机器的过程更加原始,详见下‘工作过程’。

特点:

没有操作系统的概念
所有的程序设计都是直接操控硬件

工作过程:

程序员在墙上的机时表预约一段时间,然后程序员拿着他的插件版到机房里,将自己的插件板街道计算机里,这几个小时内他独享整个计算机资源,后面的一批人都得等着(两万多个真空管经常会有被烧坏的情况出现)。

后来出现了穿孔卡片,可以将程序写在卡片上,然后读入机而不用插件板

优点:

程序员在申请的时间段内独享整个资源,可以即时地调试自己的程序(有bug可以立刻处理)

缺点:

浪费计算机资源,一个时间段内只有一个人用。
注意:同一时刻只有一个程序在内存中,被cpu调用执行,比方说10个程序的执行,是串行的

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

第二代计算机的产生背景:

由于当时的计算机非常昂贵,自认很自然的想办法较少机时的浪费。通常采用的方法就是批处理系统。

特点:
设计人员、生产人员、操作人员、程序人员和维护人员直接有了明确的分工,计算机被锁在专用空调房间中,由专业操作人员运行,这便是‘大型机’。

有了操作系统的概念

有了程序设计语言:FORTRAN语言或汇编语言,写到纸上,然后穿孔打成卡片,再讲卡片盒带到输入室,交给操作员,然后喝着咖啡等待输出接口

工作过程:插图

img

img

第二代如何解决第一代的问题/缺点:

1.把一堆人的输入攒成一大波输入,
2.然后顺序计算(这是有问题的,但是第二代计算也没有解决)
3.把一堆人的输出攒成一大波输出

现代操作系统的前身:(见图)

优点:

批处理,节省了机时

缺点:

1.整个流程需要人参与控制,将磁带搬来搬去(中间俩小人)

2.计算的过程仍然是顺序计算-》串行

3.程序员原来独享一段时间的计算机,现在必须被统一规划到一批作业中,等待结果和重新调试的过程都需要等同批次的其他程序都运作完才可以(这极大的影响了程序的开发效率,无法及时调试程序)

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

第三代计算机的产生背景:

20世纪60年代初期,大多数计算机厂商都有两条完全不兼容的生产线。

一条是面向字的:大型的科学计算机,如IBM 7094,见上图,主要用于科学计算和工程计算

另外一条是面向字符的:商用计算机,如IBM 1401,见上图,主要用于银行和保险公司从事磁带归档和打印服务

开发和维护完全不同的产品是昂贵的,同时不同的用户对计算机的用途不同。

IBM公司试图通过引入system/360系列来同时满足科学计算和商业计算,360系列低档机与1401相当,高档机比7094功能强很多,不同的性能卖不同的价格

360是第一个采用了(小规模)芯片(集成电路)的主流机型,与采用晶体管的第二代计算机相比,性价比有了很大的提高。这些计算机的后代仍在大型的计算机中心里使用,此乃现在服务器的前身,这些服务器每秒处理不小于千次的请求。

如何解决第二代计算机的问题1:

卡片被拿到机房后能够很快的将作业从卡片读入磁盘,于是任何时刻当一个作业结束时,操作系统就能将一个作业从磁带读出,装进空出来的内存区域运行,这种技术叫做
同时的外部设备联机操作:SPOOLING,该技术同时用于输出。当采用了这种技术后,就不在需要IBM1401机了,也不必将磁带搬来搬去了(中间俩小人不再需要)

如何解决第二代计算机的问题2:

第三代计算机的操作系统广泛应用了第二代计算机的操作系统没有的关键技术:多道技术

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

多道技术:

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

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

img

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

###ps
现代计算机或者网络都是多用户的,多个用户不仅共享硬件,而且共享文件,数据库等信息,共享意味着冲突和无序。

操作系统主要使用来

1.记录哪个程序使用什么资源

2.对资源请求进行分配

3.为不同的程序和用户调解互相冲突的资源请求。

我们可将上述操作系统的功能总结为:处理来自多个程序发起的多个(多个即多路)共享(共享即复用)资源的请求,简称多路复用

多路复用有两种实现方式

1.时间上的复用

当一个资源在时间上复用时,不同的程序或用户轮流使用它,第一个程序获取该资源使用结束后,在轮到第二个。。。第三个。。。

例如:只有一个cpu,多个程序需要在该cpu上运行,操作系统先把cpu分给第一个程序,在这个程序运行的足够长的时间(时间长短由操作系统的算法说了算)或者遇到了I/O阻塞,操作系统则把cpu分配给下一个程序,以此类推,直到第一个程序重新被分配到了cpu然后再次运行,由于cpu的切换速度很快,给用户的感觉就是这些程序是同时运行的,或者说是并发的,或者说是伪并行的。至于资源如何实现时间复用,或者说谁应该是下一个要运行的程序,以及一个任务需要运行多长时间,这些都是操作系统的工作。

2.空间上的复用

每个客户都获取了一个大的资源中的一小部分资源,从而减少了排队等待资源的时间。

例如:多个运行的程序同时进入内存,硬件层面提供保护机制来确保各自的内存是分割开的,且由操作系统控制,这比一个程序独占内存一个一个排队进入内存效率要高的多。

有关空间复用的其他资源还有磁盘,在许多系统中,一个磁盘同时为许多用户保存文件。分配磁盘空间并且记录谁正在使用哪个磁盘块是操作系统资源管理的典型任务。

这两种方式合起来便是多道技术

空间上的复用最大的问题是:程序之间的内存必须分割,这种分割需要在硬件层面实现,由操作系统控制。如果内存彼此不分割,则一个程序可以访问另外一个程序的内存,

首先丧失的是安全性,比如你的qq程序可以访问操作系统的内存,这意味着你的qq可以拿到操作系统的所有权限。

其次丧失的是稳定性,某个程序崩溃时有可能把别的程序的内存也给回收了,比方说把操作系统的内存给回收了,则操作系统崩溃。

第三代计算机的操作系统仍然是批处理

许多程序员怀念第一代独享的计算机,可以即时调试自己的程序。为了满足程序员们很快可以得到响应,出现了分时操作系统

如何解决第二代计算机的问题3:

分时操作系统:
多个联机终端+多道技术

20个客户端同时加载到内存,有17在思考,3个在运行,cpu就采用多道的方式处理内存中的这3个程序,由于客户提交的一般都是简短的指令而且很少有耗时长的,索引计算机能够为许多用户提供快速的交互式服务,所有的用户都以为自己独享了计算机资源

CTTS:麻省理工(MIT)在一台改装过的7094机上开发成功的,CTSS兼容分时系统,第三代计算机广泛采用了必须的保护硬件(程序之间的内存彼此隔离)之后,分时系统才开始流行

MIT,贝尔实验室和通用电气在CTTS成功研制后决定开发能够同时支持上百终端的MULTICS(其设计者着眼于建造满足波士顿地区所有用户计算需求的一台机器),很明显真是要上天啊,最后摔死了。

后来一位参加过MULTICS研制的贝尔实验室计算机科学家Ken Thompson开发了一个简易的,单用户版本的MULTICS,这就是后来的UNIX系统。基于它衍生了很多其他的Unix版本,为了使程序能在任何版本的unix上运行,IEEE提出了一个unix标准,即posix(可移植的操作系统接口Portable Operating System Interface)

后来,在1987年,出现了一个UNIX的小型克隆,即minix,用于教学使用。芬兰学生Linus Torvalds基于它编写了Linux

进程概述

什么是进程?

# 进程:正在进行的一个过程或者说一个任务,每个进程在内存中使用的数据彼此是物理级别的隔离

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

班主任老师在一个时间内有很多任务要做:比如唱歌的任务,跳舞的任务,rap的任务。

班主任老师同一时刻只能做一个任务(单核cpu同一时间只能干一个活),如何才能玩出多个任务并发执行的效果?

班主任老师唱会歌然后又去跳了会儿舞蹈,然后又去唱了会儿歌,然后又去哼哼了一会儿rap,这就保证了每个人物都在进行中。

进程与程序的区别?

#区别:正在进行的一个过程或者说一个任务,而程序仅仅仅只是一堆代码。

具体案例:

想象一位有一手好厨艺的计算机科学家egon正在为他的女儿元昊烘制生日蛋糕。

他有做生日蛋糕的食谱,

厨房里有所需的原料:面粉、鸡蛋、韭菜,蒜泥等。

在这个比喻中:

做蛋糕的食谱就是程序(即用适当形式描述的算法)

计算机科学家就是处理器(cpu)

而做蛋糕的各种原料就是输入数据

进程就是厨师阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和

现在假设计算机科学家egon的儿子alex哭着跑了进来,说:XXXXXXXXXXXXXX

科学家egon想了想,处理儿子alex蛰伤的任务比给女儿元昊做蛋糕的任务更重要,于是

计算机科学家就记录下他照着食谱做到哪儿了(保存进程的当前状态),然后拿出一本急救手册,按照其中的指示处理蛰伤。这里,我们看到处理机从一个进程(做蛋糕)切换到另一个高优先级的进程(实施医疗救治),每个进程拥有各自的程序(食谱和急救手册)。当蜜蜂蛰伤处理完之后,这位计算机科学家又回来做蛋糕,从他
离开时的那一步继续做下去。

需要强调的是:同一个程序执行两次,那也是两个进程,比如打开暴风影音,虽然都是同一个软件,但是一个可以播放苍井空,一个可以播放饭岛爱。

进程的创建(了解)

 但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,比如微波炉中的控制器,一旦启动微波炉,所有的进程都已经存在。

 而对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为4中形式创建新的进程

  1. 系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)
  2. 一个进程在运行过程中开启了子进程(如nginx开启多进程,os.fork,subprocess.Popen等)
  3. 用户的交互式请求,而创建一个新进程(如用户双击暴风影音)
  4. 一个批处理作业的初始化(只在大型机的批处理系统中应用)

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

  1. 在UNIX中该系统调用是:fork,fork会创建一个与父进程一模一样的副本,二者有相同的存储映像、同样的环境字符串和同样的打开文件(在shell解释器进程中,执行一个命令就会创建一个子进程)
  2. 在windows中该系统调用是:CreateProcess,CreateProcess既处理进程的创建,也负责把正确的程序装入新进程。

进程的三个基本状态

进程执行时的间断性,决定了进程可能具有多种状态。事实上,运行中的进程可能具有以下三种基本状态。
就绪状态(Ready):

进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。
运行状态(Running):
进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
阻塞状态(Blocked):
由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理器资源分配给该进程,也无法运行。
img

# 尽量减少阻塞状态可以提升我们程序运行的效率

进程的终止

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

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

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

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

进程实现并发(了解)

# 进程并发 = 保存状态+切换

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

img

进程的层次结构(了解)

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

  1. 在UNIX中所有的进程,都是以init进程为根,组成树形结构。父子进程共同组成一个进程组,这样,当从键盘发出一个信号时,该信号被送给当前与键盘相关的进程组中的所有成员。
  2. 在windows中,没有进程层次的概念,所有的进程都是地位相同的,唯一类似于进程层次的暗示,是在创建进程时,父进程得到一个特别的令牌(称为句柄),该句柄可以用来控制子进程,但是父进程有权把该句柄传给其他子进程,这样就没有层次了。

多进程

一 multiprocessing模块

python中的多线程无法利用多核优势,如果想要充分地使用多核CPU的资源(os.cpu_count()查看),在python中大部分情况需要使用多进程。Python提供了multiprocessing。
multiprocessing模块用来开启子进程,并在子进程中执行我们定制的任务(比如函数),该模块与多线程模块threading的编程接口类似。

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

注意:在windows中Process()必须放到# if name == 'main':下

Since Windows has no fork, the multiprocessing module starts a new Python process and imports the calling module. 
If Process() gets called upon import, then this sets off an infinite succession of new processes (or until your machine runs out of resources). 
This is the reason for hiding calls to Process() inside

if __name__ == "__main__"
since statements inside this if-statement will not get called upon import.
由于Windows没有fork,多处理模块启动一个新的Python进程并导入调用模块。 
如果在导入时调用Process(),那么这将启动无限继承的新进程(或直到机器耗尽资源)。 
这是隐藏对Process()内部调用的原,使用if __name__ == “__main __”,这个if语句中的语句将不会在导入时被调用。

二 创建子进程的两种方式

第一种

from multiprocessing import Process
import time

def task(n):
    print('子进程开始')
    time.sleep(n)
    print('子进程结束')

if __name__ == '__main__':
    p = Process(target=task,args=(1,))
    p.start()  # p.start 只是向操作系统发了一个信号,请求开启子进程,操作系统具体什么时候开,开多长时间你控制不了。
    time.sleep(5) #
    print('我是主进程')

第二种

from multiprocessing import Process
import time


class MyProcesszzz(Process):
    def __init__(self,x):# 如果不传参没必要重写init
        super().__init__()
        self.n = x

    def run(self):
        print('我是子进程 start')
        time.sleep(self.n)
        print('我是子进程 end')


if __name__ == '__main__':
    p1 = MyProcesszzz(2)
    p1.start()

    print('我是主进程')


'''
思考:主进程是什么结束的呢?
ps: 主进程会在子进程都结束的时候再结束 
why?详情见僵尸进程
'''

三 僵尸进程孤儿进程(了解)

'''引号的内容不适合初次阅读本博客思考
# 僵尸进程
1 何为没死干净的进程--》2 父亲都会有什么需求--》3 僵尸进程的概念--》4 什么时候回收pid合理--》5 之所以父等待子进程结束的原因。
# 孤儿进程
1 父进程死了谁来回收自己程? --》2 孤儿进程的概念 --》是父进程来回收好?还是init回收好?--》孤儿进程总归是无害的。
'''

#二:僵尸进程(父进程一直不死不停造子进程并且不回收僵尸进程有害)
    一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。
因此,UNⅨ提供了一种机制可以保证父进程可以在任意时刻获取子进程结束时的状态信息:
1、在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)
2、直到父进程通过wait / waitpid来取时才释放. 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

  任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。  如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
# 二:孤儿进程(无害)
  孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

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

论述可能出现的情况:

情况一

1 父进程正常回收子进程 (无论父进程结束自动回收和手动回收僵尸进程均无害)

情况二

2 父进程先死了,出现了孤儿进程(可能父进程先死了 ,子进程没有死 ,子进程编程了孤儿进程由init管理回收,无害。)

情况三

3 父进程一直不死不断开启子进程,并且不手动回收。(有害)

父进程一直不死不断开启子进程,一直不发起回收僵尸进程的情况,父进程不死,init不会帮父进程回收,产生了大量的僵尸进程无人回收,占用了大量的pid,会导致其他程序无pid可用。

如何解决情况三 ?

直接杀死父进程,所有的子进程均变为子进程均变为孤儿进程,统一由init回收。

四 证明进程间内存空间隔离

# 一个进程修改代码,肯定生效
x = 0
def task():
    global x
    x = 50
    print(x)

task()
print(x)
from multiprocessing import Process
# 一个进程修改代码,肯定生效
x = 0
def task():
    global x # 子进程修改的是自己的名称空间里的x,与主进程无关。
    x = 50
    print(f'子进程的x:{x}') 
if __name__ == '__main__':    
    p = Process(target=task)
    p.start()
    print(f'主进程的x:{x}')

五 Process的join方法

引入:

from multiprocessing import Process
import time
def task():
    print('子进程 开始')
    time.sleep(2)
    print('子进程 结束')
if __name__ == '__main__':

    p = Process(target=task)
    p.start()
    time.sleep(5) # 主进程io了5s钟,子进程已经结束
    print('主线程')
    
    '''输出:
    
    子进程 开始
    子进程 结束
    主线程
    
    '''

    # 思考:有没有一种只智能的方式可以动态的等待子进程结束?

1 join具体用法

from multiprocessing import Process
import time
def task():
    print('子进程 开始')
    time.sleep(5)
    print('子进程 结束')
if __name__ == '__main__':
    start_time = time.time() # 记录开始时间
    p = Process(target=task)
    p.start()
    p.join() # 如遇join会阻塞该子进程,直到该子进程结束。并且join调用了wait方法释放僵尸进程。
    end_time = time.time() # 记录结束时间
    print(end_time-start_time) # 计算时间差
    print('主线程')
    
    '''输出:
    
    子进程 开始
    子进程 结束
    5.051951885223389
    
    主线程
    '''

2 思考:如果有多个join是否是串行了?

from multiprocessing import Process
import time
def task(n):
    print('子进程 开始')
    time.sleep(n)
    print('子进程 结束')
if __name__ == '__main__':
    start_time = time.time() # 记录开始时间
    p1 = Process(target=task,args=(2,))
    p2 = Process(target=task,args=(4,))
    p3 = Process(target=task,args=(6,))

    p1.start()
    p2.start()
    p3.start()
    # 换顺序也是一样的也是按照时长最长的那个计算。
    p1.join() # 等待2s
    p2.join() # 等待2s
    p3.join() # 等待2s
    end_time = time.time() # 记录结束时间
    print(end_time-start_time) # 计算时间差
    print('主线程')



    '''输出:
    
    子进程 开始
    子进程 开始
    子进程 开始
    子进程 结束
    子进程 结束
    子进程 结束
    7.566513776779175
    主线程
    
    '''

3 join串行的情况

from multiprocessing import Process
import time
def task(n):
    print('子进程 开始')
    time.sleep(n)
    print('子进程 结束')
if __name__ == '__main__':
    start_time = time.time() # 记录开始时间
    p1 = Process(target=task,args=(2,))
    p2 = Process(target=task,args=(4,))
    p3 = Process(target=task,args=(6,))

    p1.start()
    p1.join() 
    p2.start()
    p2.join() 
    p3.start()
    p3.join() 
    end_time = time.time() # 记录结束时间
    print(end_time-start_time) # 计算时间差
    print('主线程')

    '''输出:
    
    子进程 开始
    子进程 结束
    子进程 开始
    子进程 结束
    子进程 开始
    子进程 结束
    15.407774925231934
    主线程
    
    '''
    # ps:反而不如不开进程,正常调用三次来的快。

4 精炼代码:

from multiprocessing import Process
import time
def task(n):
    print('子进程 开始')
    time.sleep(n)
    print('子进程 结束')
if __name__ == '__main__':
    start_time = time.time() # 记录开始时间
    task_list = []
    for i in range(1,4):
        p = Process(target=task,args=(i,))
        p.start()
        task_list.append(p)
    print(task_list) # [<Process(Process-1, started)>, <Process(Process-2, started)>, <Process(Process-3, started)>]
    for i in task_list:
        i.join()
    end_time = time.time() # 记录结束时间
    print(end_time-start_time) # 计算时间差 4.764175891876221
    print('主线程')

六 Process的其他用法(了解)

pid用法:

'''
    在当前进程查看当前进程pid
        os.getpid()
        current_process().pid
    在当前进程查看子进程pid
        子进程对象.pid()
    在当前进程查看父进程pid
        os.getppid()   
'''

from multiprocessing import Process,current_process
import time,os
def task(x):
    print('进程开始')
    print('子进程对象的pid:',os.getpid()) # 在子进程中查看自己的pid
    print('子进程对象的pid:',current_process().pid) # 在子进程中查看自己的pid
    print('子进程对象的父进程对象的pid:',os.getppid()) # 查看父进程对象的pid
    time.sleep(100) # 注意要保证子进程不死,方便在cmd下测试。
    print('进程结束')

if __name__ == '__main__':
    p = Process(target=task,args=('子进程',))
    p.start()
    print('在父进程中查看子进程的pid',p.pid) # 在父进程中查看子进程的pid
    print('主进程自己查看自己的pid:',os.getpid()) # 父进程查看自己的pid
    print('主进程自己查看自己的pid:',current_process().pid) # 父进程查看自己的pid

name的用法:

from multiprocessing import Process,current_process

def task():
    print(current_process().name) # 在子进程查看自己的name

if __name__ == '__main__':
    p = Process(target=task) 
    p2 = Process(target=task)
    p3 = Process(target=task,name='rocky') # 已定义name属性为rocky
    p.start()
    p2.start()
    p3.start()
    print(p.name) #Process-1
    print(p2.name) #Process-2
    print(p3.name) #rocky

is_alive用法:

from multiprocessing import Process,current_process
import time
def task():
    print('子进程开始')
    time.sleep(1)
    print('子进程结束')

if __name__ == '__main__':
    p = Process(target=task)
    p.start()
    # p.terminate() #发送一个指令给操作系统但是不会立即结束子进程
    # time.sleep(1)
    print(p.is_alive()) # True 判断子进程代码是否结束
    time.sleep(3)
    print(p.is_alive()) # False

七 守护进程

主进程创建守护进程

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

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

例一:主进程代码运行完,守护进程立即结束

from multiprocessing import Process
import time
def foo():
    print('守护进程开始')
    time.sleep(5)
    print('守护进程开始')


if __name__ == '__main__':
    p1 = Process(target=foo)
    p1.daemon = True # 一定要凡在start之前,表示设置为一个守护进程。
    p1.start()
    print('主进程')
    
    '''输出:
    主进程
    '''
    # 守护进程一旦发现主进程代码运行完,立刻结束,并不会管自己的进程是否运行完

例二 主进程代码运行完后等在子进程运行阶段,线程不会参与守护

from multiprocessing import Process

import time
def foo():
    print('守护进程开始')
    time.sleep(5)
    print('守护进程结束')
def task():
    print('子进程开始')
    time.sleep(3)
    print('子进程开始')

if __name__ == '__main__':
    p1 = Process(target=foo)
    p2 = Process(target=task)
    p1.daemon = True # 一定要凡在start 之前
    p1.start()
    p2.start()
    # p2 = Process(target=task)
    print('主进程')

    '''输出:
    主进程
    子进程开始
    子进程开始
    '''
    #分析 守护进程在运行完主进程最后一行代码就结束,但是主进程并没有结束,主进程在等待子进程运行结束.

什么情况适合用守护进程?

当主进程代码结束,该子进程再执行无意义的情况可以用守护进程。

八 进程同步(锁)

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

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

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

#并发运行,效率高,但竞争同一打印终端,带来了打印错乱
from multiprocessing import Process
import os,time
def work():
    print('%s is running' %os.getpid())
    time.sleep(2)
    print('%s is done' %os.getpid())

if __name__ == '__main__':
    for i in range(3):
        p=Process(target=work)
        p.start()
#由并发变成了串行,牺牲了运行效率,但避免了竞争
from multiprocessing import Process,Lock
import os,time
def work(lock):
    lock.acquire()
    print('%s is running' %os.getpid())
    time.sleep(2)
    print('%s is done' %os.getpid())
    lock.release()
if __name__ == '__main__':
    lock=Lock()
    for i in range(3):
        p=Process(target=work,args=(lock,))
        p.start()

part2:多个进程共享同一文件

文件当数据库,模拟抢票

from multiprocessing import Process,Lock
import os , time
import json
def search():
    with open('a.json',encoding='utf-8') as f:
        data = json.load(f)

    print(f'主页面还剩下{data["count"]}张票')

def get():
    # 来到支付页面通常会再次查看一下票数
    with open('a.json', encoding='utf-8') as f:
        data = json.load(f)
    time.sleep(0.5) # 模拟网络io
    if data['count'] > 0:
        print(f'支付页面-还剩{data["count"]}张票')
        print(f'{os.getpid()}进程, 抢到了1张票')
        time.sleep(0.5) # 模拟网络io
        with open('a.json','w',encoding='utf-8') as f:
            data['count'] -=1
            json.dump(data,f)

def task(lock):
    # search()
    lock.acquire()
    get()
    lock.release()


if __name__ == '__main__':
    lock = Lock()
    for i in range(1,20):# 模拟20个人抢票
        p = Process(target=task,args=(lock,))
        p.start()
#加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。
虽然可以用文件共享数据实现进程间通信,但问题是:
1.效率低(共享数据基于文件,而文件是硬盘上的数据)
2.需要自己加锁处理



#因此我们最好找寻一种解决方案能够兼顾:1、效率高(多个进程共享一块内存的数据)2、帮我们处理好锁问题。这就是mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。
1 队列和管道都是将数据存放于内存中
2 队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来,
我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性。

六 队列(推荐使用)

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

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

1 Queue([maxsize]):创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。 
**参数介绍:**
1 maxsize是队列中允许最大项数,省略则无大小限制。    

  方法介绍:

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

from multiprocessing import Process,Queue
import time
q=Queue(3)


#put ,get ,put_nowait,get_nowait,full,empty
q.put(3)
q.put(3)
q.put(3)
print(q.full()) #满了

print(q.get())
print(q.get())
print(q.get())
print(q.empty()) #空了

七 生产者消费者模型

生产者消费者模型

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

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

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

什么是生产者消费者模式

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

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

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):
    for i in range(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出死循环

from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
    while True:
        res=q.get()
        if res is None:break #收到结束信号则结束
        time.sleep(random.randint(1,3))
        print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res))

def producer(q):
    for i in range(10):
        time.sleep(random.randint(1,3))
        res='包子%s' %i
        q.put(res)
        print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res))
    q.put(None) #发送结束信号
if __name__ == '__main__':
    q=Queue()
    #生产者们:即厨师们
    p1=Process(target=producer,args=(q,))

    #消费者们:即吃货们
    c1=Process(target=consumer,args=(q,))

    #开始
    p1.start()
    c1.start()
    print('主')

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

from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
    while True:
        res=q.get()
        if res is None:break #收到结束信号则结束
        time.sleep(random.randint(1,3))
        print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res))

def producer(q):
    for i in range(2):
        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()

    p1.join()
    q.put(None) #发送结束信号
    print('主')

但上述解决方式,在有多个生产者和多个消费者时,我们则需要用一个很low的方式去解决

from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
    while True:
        res=q.get()
        if res is None:break #收到结束信号则结束
        time.sleep(random.randint(1,3))
        print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res))

def producer(name,q):
    for i in range(2):
        time.sleep(random.randint(1,3))
        res='%s%s' %(name,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))
    p2=Process(target=producer,args=('骨头',q))
    p3=Process(target=producer,args=('泔水',q))

    #消费者们:即吃货们
    c1=Process(target=consumer,args=(q,))
    c2=Process(target=consumer,args=(q,))

    #开始
    p1.start()
    p2.start()
    p3.start()
    c1.start()

    p1.join() #必须保证生产者全部生产完毕,才应该发送结束信号
    p2.join()
    p3.join()
    q.put(None) #有几个消费者就应该发送几次结束信号None
    q.put(None) #发送结束信号
    print('主')

其实我们的思路无非是发送结束信号而已,有另外一种队列提供了这种机制

   #JoinableQueue([maxsize]):这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。

   #参数介绍:
    maxsize是队列中允许最大项数,省略则无大小限制。    
  #方法介绍:
    JoinableQueue的实例p除了与Queue对象相同的方法之外还具有:
    q.task_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常
    q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止
from multiprocessing import Process,JoinableQueue
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))

        q.task_done() #向q.join()发送一次信号,证明一个数据已经被取走了

def producer(name,q):
    for i in range(10):
        time.sleep(random.randint(1,3))
        res='%s%s' %(name,i)
        q.put(res)
        print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res))
    


if __name__ == '__main__':
    q=JoinableQueue()
    #生产者们:即厨师们
    p1=Process(target=producer,args=('包子',q))
    p2=Process(target=producer,args=('骨头',q))
    p3=Process(target=producer,args=('泔水',q))

    #消费者们:即吃货们
    c1=Process(target=consumer,args=(q,))
    c2=Process(target=consumer,args=(q,))
    c1.daemon=True
    c2.daemon=True

    #开始
    p_l=[p1,p2,p3,c1,c2]
    for p in p_l:
        p.start()

    p1.join()
    p2.join()
    p3.join()
    q.join()  # 主进程代码运行完毕--生产者运行完毕,队列也取空了--消费者没有存在的意义了。

   # 主进程代码运行完毕,某个进程应该结束掉,这种情况使用守护线程会非常边便捷。

线程概述

一 什么是线程

  在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程

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

  车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一个流水线

  流水线的工作需要电源,电源就相当于cpu

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

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

  例如,北京地铁与上海地铁是不同的进程,而北京地铁里的13号线是一个线程,北京地铁所有的线路共享北京地铁所有的资源,比如所有的乘客可以被所有线路拉。

二 纠正概念

在这呢我就要给大家纠正一个概念了。

1 以前咱们说进程怎么怎么运行 ,那是在没有学习线程的时候不得已才那么说。进程根本就不是一个执行单位,进程其实是一个资源单位。

2 以后说进程,在内存当中有一块隔离的空间, 专门存这个进程运行过程当中一些相关的数据。

3 我们之前说进程申请空间,然后运行代码,那运行代码的过程我们就抽出来了叫线程的执行 。

4 每个进程内都自带一个线程,线程才是cpu上的执行单位,之前说切换实际切换到的执行的是进程里面的线程。cpu会在多个线程间切换。

二 如果把操作系统当成一个工厂

img

操作系统 ===》工厂

进程 ===》车间

线程 ===》流水线 (需要电源)

cpu ===》电源

创建进程的开销要远大于线程?

如果我们的软件是一个工厂,该工厂有多条流水线,流水线工作需要电源,电源只有一个即cpu(单核cpu)

一个车间就是一个进程,一个车间至少一条流水线(一个进程至少一个线程)

创建一个进程,就是创建一个车间(申请空间,在该空间内建至少一条流水线)

而建线程,就只是在一个车间内造一条流水线,无需申请空间,所以创建开销小

故事开始:

如果把咱们的操作系统比喻为一个工厂, 每在操作系统中启动了一个进程,就相当于在工厂内开了一个车间,比如我起了一个进程,相当于在这个工厂内开了一个造方向盘的车间。又启动了一个车间来了一个造发动机的车间。你造一个车间要干什么事情。

你跟着我想。

1 造车间需要干什么? 各种各样的原材料

比如这个造方向盘的车间。首先你造一个房子,需要塑料盒橡胶。。。专门放到这个造方向盘的车间。造方向盘一些相关的原材料丢进去吧,如果让你去造一个工厂的话,造车门造发动机的。。。,

2 进程其实是一个资源单位,一个车间独占一份空间

如果没有车间的话划分是不是原材料全部多混在了一起了 。我在这个工厂内是不是就是为了把车间隔离开,造方向盘的就专门放造方向盘的材料。造车间目的就是为了把一块资源根别人隔离开。咱们的车间刚刚比喻为了进程。造一个进程就是在操作系统中开了一个车间,车间里面放的各种各样的资源,进程其实其实是一个资源单位(或者说资源集合)。

其实是为了告诉你一件事情,qq进程相关的数据占了独一份空间,world进程相关的数据是占了独一份空间。

3 造车间的目的,其实具体的一条条流水线(线程)负责生产零件。

说到这,接着再想,造好车间的目的是为了划分一块资源,往里面丢入一些原材料,相当于一堆数据丢进去了,丢进去是为了好看的吗?

造车间的本质是为了干什么事情?是为了让车间运作,那一个车间运作必须车间内至少要有一条流水线,也就是咱们刚刚所说的每一个进程内,都应该至少有一个线程。就是咱们车间内的流水线。那这个流水线,他在运作的过程中就是取来造方向盘的一个个数据进程加工处理就行了。

4 每个流水线需要电运转起来,相当于每个线程需要cpu去运行。

5 结论

如果把操作系统比喻为一做工厂

在工厂内没造出一个车间 ===》 启动一个进程

每个车间内至少有一条流水线 ===》 每个进程内至少有一个线程。

进程之间是竞争关系,线程之间是协作关系?(了解)

车间直接是竞争/抢电源的关系,竞争(不同的进程直接是竞争关系,是不同的程序员写的程序运行的,迅雷抢占其他进程的网速,360把其他进程当做病毒干死)
一个车间的不同流水线式协同工作的关系(同一个进程的线程之间是合作关系,是同一个程序写的程序内开启动,迅雷内的线程是合作关系,不会自己干自己)

三 在程序当中应该如何看待进程和线程?

1 右键运行干了个什么事情?

产生了一个进程---》启动进程的目的就是开了一个内存空间以及方便方便操作系统调度---》开空间的目的是为了存进程相关的数据。进程其实指的是一个资源单位或者说资源集合=》开了空间之后里面放了代码,而代码需要去运行=》而代码的运行过程则是线程。

用一个比喻的说法:进程是线程的容器

线程 = 代码的运行过程

进程 = 线程+各种资源(比如内存空间等)

进程内的线程运行就像车间内的一条条流水线运行。

四 再次论述一下 把教室比喻为车间:

1 具体例子。

教室 --》车间 --- 》进程

人干活 --》流水线----》 线程

干活的工具 --》电源 ----》cpu

如果把教室想象成一个车间,启动进程就是把教室准备好了,然后我要干具体的活了就是运行代码被,那也就是谁可以干活啊,人去干活被,人是不是需要干活的工具干活啊,那人就相当于线程,干活的工具就相当于cpu。干活用的一些原料(水泥螺丝)就是这个教室提供的也就是该进程提供的。

人拿着工具干活就相当于线程拿着cpu运行,而需要的一些水泥各种原料从这个教室里面拿也就是从进程里面拿。

五 作为开发的常识

1 进程的启动和销毁(空间开始和释放)

只要一说一个进程启动了,那你知道了一个事情,是关于这个程序的一块空间造好了。

只要一说进程销毁了,那你也知道了一个事情,是关于这个程序的空间释放了。

2 线程的启动和销毁(进程里的一段代码运行起来了,进程里的一段代码运行结束)

六 线程与进程的区别是什么?

线程==》单指代码的执行过程

进程==》资源的申请与销毁的过程

ps: 但是同一进程下可不可以开多个线程? 就如同一个车间可不可以开多条流水线,答案是yes。

进程vs线程

1 、内存共享or隔离

多个进程内存空间彼此隔离。

同一进程下的多个线程共享该进程内的数据。

2 、创建速度

造线程的速度,要远远快于造进程。

多线程

一 threading模块介绍

multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性,因而不再详细介绍

官网链接:https://docs.python.org/3/library/threading.html?highlight=threading#

二 开启线程的两种方式

#方式一
from threading import Thread
import time

def task(name):
    print(f'{name}task is running')
    time.sleep(2)
    print(f'{name}task is done')

if __name__ == '__main__':
    t = Thread(target=task,args=('egon',))
    t.start() # 告诉操作系统开一个线程 开启线程无需申请内存空间会非常快。
    print('主')
# 注意:1 同一个进程下没有父子线程之分大家地位都一样。
#      2 右键运行发生了个什么事情? 开启了一个进程,开辟了一个内存空间,把代码都丢进去,然后运行代码(自带的主线程运行)
#      然后又开启了一个子线程。


# ps:主线程结束和子线程结束没有任何必然联系,比如主线程运行结束,子线程还在运行当中。不是主线程在等待子线程结束,是进程在等待自己的所有线程结束。
from threading import Thread
import time

class MyTread(Thread):
    def run(self):
        print('thread is running')
        time.sleep(2)
        print('thread is end')


if __name__ == '__main__':
    t = MyTread()
    t.start() #
    print('主线程')
    # 输出:
    # thread is running
    # 主线程
    # thread is end

img

三 进程vs线程

1 比起进程来说线程的开启速度快

from threading import Thread
from multiprocessing import Process
import time
def task(name):
    print(f"{name} is running")
    time.sleep(2)
    print(f"{name} is done")

if __name__ == '__main__':
    t = Thread(target=task,args=('子线程',))
    p = Process(target=task,args=('子进程',))
    t.start()
    # p.start()
    print('主')
    '''
    开启子进程打印的效果:
    
    >主
    >子进程 is running
    >子进程 is done
    
    开启子线程打印的效果:
    >子线程 is running
    >主
    >子线程 is done
    
    对比的结果:从打印效果就能看的出来,开启线程要快于进程。
    开启进程需要申请空间,复制代码到该空间,然后执行自带线程,非常慢。
    开启子线程 基本没有资源消耗所以非常快。
    '''

2 同一进程下的线程共享内存空间

from threading import Thread
import time

x = 100
def task():
    global x
    x = 20

if __name__ == '__main__':
    t = Thread(target=task)
    t.start()
    time.sleep(2)
    print(x) # 20
    # 首先子线程修改了全局变量为20,主线程等待子线程修改完毕后打印x为20。
    # 说明同一个进程下所有的线程共享同一份内存空间。

3 瞅瞅pid

from threading import Thread
import os

def task():
    print('子线程',os.getpid()) # 子线程 14576

if __name__ == '__main__':
    t = Thread(target=task)
    t.start()
    print('主线程',os.getpid())  #主线程 14576

四 线程对象join用法

主线程等待子线程运行结束。

from threading import Thread
import time

def task(name,n):
    print(f'{name} is running')
    time.sleep(n)
    print(f'{name} is done')

if __name__ == '__main__':
    t1 = Thread(target=task,args=('线程1',1))
    t2 = Thread(target=task,args=('线程2',2))
    t3 = Thread(target=task,args=('线程3',3))

    start_time = time.time()


    t1.start() #
    t2.start() #
    t3.start() #

    t1.join() # 等1s
    t2.join() # 等1s
    t3.join() # 等1s
    end_time = time.time()
    print(end_time-start_time) # 3.0023529529571533

    print('主')
    '''
    线程1 is running
    线程1 is done
    主  
    '''

ps:了解 对比进程的join,进程的join是当前线程在等子进程运行结束并不影响其他线程。

from multiprocessing import Process
from threading import Thread
import time

def threadtask(name):
    print(f'{name} start')
    time.sleep(5)
    print(f'{name} end')


def processtask(name):
    print(f'{name} start')
    time.sleep(20)
    print(f'{name} end')

if __name__ == '__main__':
    t = Thread(target=threadtask,args=('子线程',))
    p = Process(target=processtask,args=('子进程',))
    p.start()
    t.start()
    p.join() # 当前线程等待当前进程下的p子进程结束,然后往下运行。
from threading import Thread
from multiprocessing import Process
import os

def work():
    print('hello',os.getpid())

if __name__ == '__main__':
    #part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样
    t1=Thread(target=work)
    t2=Thread(target=work)
    t1.start()
    t2.start()
    print('主线程/主进程pid',os.getpid())

    #part2:开多个进程,每个进程都有不同的pid
    p1=Process(target=work)
    p2=Process(target=work)
    p1.start()
    p2.start()
    print('主线程/主进程pid',os.getpid())
from  threading import Thread
from multiprocessing import Process
import os
def work():
    global n
    n=0

if __name__ == '__main__':
    # n=100
    # p=Process(target=work)
    # p.start()
    # p.join()
    # print('主',n) #毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100


    n=1
    t=Thread(target=work)
    t.start()
    t.join()
    print('主',n) #查看结果为0,因为同一进程内的线程之间共享进程内的数据

五 思考主线程是否会等子线程运行结束

import time

def task(name):
    print(f'线程 start {name}')
    time.sleep(3)
    print('线程 end')

if __name__ == '__main__':
    t = Thread(target=task,args=('egon',))
    t.start() #非常快
    print('主')

分析

1 其实是进程在等
貌似是主线程在原地等着,主线程已经运行完。
原来没有子线程的情况下,其实就一个主线程这一条流水线工作完了,这个进程就结束了。
那现在的情况是当前进程有其他的子线程,是进程等待自己所有的子线程运行完。

# 主进程等子进程是因为主进程要给子进程收尸
# 现在看到的等是进程必须等待其内部所有线程都运行完毕才结束。

六 线程相关的其他方法(了解)

Thread实例对象的方法
  # isAlive(): 返回线程是否活动的。
  # getName(): 返回线程名。
  # setName(): 设置线程名。

threading模块提供的一些方法:
  # threading.currentThread(): 返回当前的线程变量。
  # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
from threading import Thread
import threading

'''以后只有周日!!!'''

def task():
    import time
    time.sleep(3)
    print(threading.current_thread().getName())

if __name__ == '__main__':
    t = Thread(target=task)
    t1 = Thread(target=task)
    print(threading.current_thread().setName('张三')) # None
    print(threading.current_thread().getName()) # 张三
    t.start()
    t1.start()
    #  返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
    print(threading.enumerate()) # [<_MainThread(MainThread, started 17944)>, <Thread(Thread-1, started 19320)>, <Thread(Thread-2, started 19308)>]
    print(len(threading.enumerate())) # 3
    print(threading.activeCount()) # 返回正在运行的线程数量。与len(threading.enumerate())有相同的结果。
    

七 守护线程(了解)

# 守护线程首先是一个线程。
    # 守护线程守护到当前进程运行结束。
    # ps:比如有未完成的子进程阶段会守护,比如有未完成的其他子线程也均会守护。
# 守护进程首先是一个进程。
    # 守护进程守护到当前进程的最后一行代码结束。

代码演示:

# 守护线程守护到当前进程结束
from threading import Thread
import threading
import time

def threadtask(name):
    print(f' {name}  start')
    print(time.sleep(20))
    print(f' {name}  end')
    # print(time.sleep(6))

def threadtask2(name):
    print(f'{name} start')
    time.sleep(10)
    print(threading.enumerate()) # [<_MainThread(MainThread, stopped 14544)>, <Thread(Thread-1, started daemon 13676)>, <Thread(Thread-2, started 13148)>]
    print(f'{name} end')

if __name__ == '__main__':
    t = Thread(target=threadtask,args=('守护线程',))
    t2 = Thread(target=threadtask2,args=('子线程',))
    t.daemon = True
    t.start()
    t2.start()
    print('主')
    '''
    守护线程  start
    子线程 start
    主
    [<_MainThread(MainThread, stopped 15448)>, <Thread(Thread-1, started daemon 17520)>, <Thread(Thread-2, started 10356)>]
    子线程 end

    '''
    可以看到当主线程已经结束的时候,其他子线程没有结束的时候打印当前的活跃的线程发现有守护线程。

八 同步锁(线程的互斥锁)

0 前言:

x = 1
def func1():
    global x
    print(x) # 1
    func2()
    print(x) # 10000  每次拿x的时候都会去全局拿到那个最新的x
    x  = x -1
def func2():
    global x
    x = 10000

func1()
# func2()
print(x) # 9999

1 多线程修改数据会造成混乱

from threading import Thread,current_thread,Lock
import time
x = 0

def task():
    global x
    for i in range(100000): # 最少10万级别才能看出来
        x = x+1   # 有可能右边的x刚拿到了0,
        # 发生线程不安全的原因:
        # t1 x+1 阶段 x = 0 保存了状态 cpu被切走  t2 x+1 x = 0 保存了状态 cpu被切走
        # 下次t1 继续运行代码 x = 0+1  下次t2 再运行代码的时候也是 x = 0+1
        #  也就说修改了两次 x 只加了一次1 。
        # time.sleep()
    # lock.release()
if __name__ == '__main__':
    t_list = []
    for i in range(3):
        t = Thread(target=task)
        t_list.append(t)
        t.start()
    for i in t_list:
        i.join()

    print(x) # 99

2 使用线程锁解决线程修改数据混乱问题。

from threading import Thread,current_thread,Lock
import time
x = 0
lock = Lock()
def task():
    global x
    lock.acquire()
    for i in range(100000): # 最少10万级别才能看出来  
        x = x+1   # 有可能右边的x刚拿到了0,
    lock.release()
if __name__ == '__main__':
    t_list = []
    for i in range(3):
        t = Thread(target=task)
        t_list.append(t)
        t.start()
    for i in t_list:
        i.join()

    print(x) # 99

3 死锁问题

from threading import Thread,Lock
import time
mutexA=Lock()
mutexB=Lock()

class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutexA.acquire()
        print('\033[41m%s 拿到A锁\033[0m' %self.name)

        mutexB.acquire()
        print('\033[42m%s 拿到B锁\033[0m' %self.name)
        mutexB.release()

        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('\033[43m%s 拿到B锁\033[0m' %self.name)
        time.sleep(2)

        mutexA.acquire()
        print('\033[44m%s 拿到A锁\033[0m' %self.name)
        mutexA.release()

        mutexB.release()

if __name__ == '__main__':
    for i in range(10):
        t=MyThread()
        t.start()

'''
Thread-1 拿到A锁
Thread-1 拿到B锁
Thread-1 拿到B锁
Thread-2 拿到A锁
然后就卡住,死锁了
'''

解决死锁问题

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

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

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

九 信号量Semaphore

同进程的一样

Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):

from threading import Thread,Semaphore
import threading
import time


def task():
    sm.acquire()
    print(f"{threading.current_thread().name} get sm")
    time.sleep(3)
    sm.release()

if __name__ == '__main__':
    sm = Semaphore(5) # 同一时间只有5个进程可以执行。
    for i in range(20):
        t = Thread(target=task)
        t.start()

与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程

二 GIl

一 介绍

'''
定义:
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.)
'''
# 1
在Cpython解释器中有一把GIL锁,GIl锁本质是一把互斥锁。
因为GIL锁:同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势

# 2
GIL本质就是一把互斥锁,那既然是互斥锁,原理都一样,都是让多个并发线程同一时间只能
    有一个执行
    即:有了GIL的存在,同一进程内的多个线程同一时刻只能有一个在运行,意味着在Cpython中
        一个进程下的多个线程无法实现并行===》意味着无法利用多核优势
        多个线程只能并发不能并行
# 3
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本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。

# 为何要有GIL?
    因为Cpython解释器自带垃圾回收机制不是线程安全的。
# 如果不对垃圾回收机制线程做任何处理,也没有GIL锁行不行?
    提示:如果是问题的这种情况,多线程和垃圾回收线程就会并发了。

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

要想了解GIL,首先确定一点:每次执行python程序,都会产生一个独立的进程。例如python test.py,python aaa.py,python bbb.py会产生3个不同的python进程

'''
#验证python test.py只会产生一个进程
#test.py内容
import os,time
print(os.getpid())
time.sleep(1000)
'''
python3 test.py 
#在windows下
tasklist |findstr python
#在linux下
ps aux |grep python

在一个python的进程内,不仅有test.py的主线程或者由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一个进程内,毫无疑问

#1 所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码)
例如:test.py定义一个函数work(代码内容如下图),在进程内所有线程都能访问到work的代码,于是我们可以开启三个线程然后target都指向该代码,能访问到意味着就是可以执行。

#2 所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码。

综上:

如果多个线程的target=work,那么执行流程是

多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行

解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了一个问题:对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,解决这种问题没有什么高明的方法,就是加锁处理,如下图的GIL,保证python解释器同一时间只能执行一个任务的代码

img

三 GIL与Lock

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

多进程vs多线程

#分析:
我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:
方案一:开启四个进程
方案二:一个进程下,开启四个线程

#多核情况下,分析结果:
  如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜。
  如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜

 
#结论:现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
#计算密集型 -----》推荐多进程
-多线程
四个线程每个都要做计算的时间一点也没少。
-多进程
四个进程可以实现并行的去计算,只花了一个进程计算的时间。
#io密集型  99%都在做io ----》推荐多线程
假设io要10s
-多进程
四个进程每个io 10s,开四个进程并行的拿到cpu,启动4个进程的时间(这个时间比较长)+10s+1点计算时间(忽略不计)+进程的切换(时间较久)。
启动进程时间耗的比较久,进程的切换耗时也比较久。
-多线程   
四个线程每个io 10s 遇到io了切遇到io了切,启动4个线程的时间+10s+4点计算的时间(忽略不计)+线程的切换。

计算密集型测试(推荐使用多进程)

# 计算密集型
from multiprocessing import Process
from threading import Thread
import os,time
def work1():
    res=0
    for i in range(100000000): #1+8个0
        res*=i

def work2():
    res=0
    for i in range(100000000):
        res*=i

def work3():
    res=0
    for i in range(100000000):
        res*=i

def work4():
    res=0
    for i in range(100000000):
        res*=i

if __name__ == '__main__':
    l=[]
    # print(os.cpu_count()) #本机为4核
    start=time.time()
    # p1=Process(target=work1) #
    # p2=Process(target=work2)
    # p3=Process(target=work3)
    # p4=Process(target=work4)

    p1=Thread(target=work1) #
    p2=Thread(target=work2)
    p3=Thread(target=work3)
    p4=Thread(target=work4)

    p1.start()
    p2.start()
    p3.start()
    p4.start()
    p1.join()
    p2.join()
    p3.join()
    p4.join()
    stop=time.time()
    print('run time is %s' %(stop-start)) 
    #多进程 6.484470367431641
    #多线程 17.391708850860596

io密集型测试(推荐使用多线程)

# IO密集型
from multiprocessing import Process
from threading import Thread
import os,time
def work1():
    time.sleep(5)

def work2():
    time.sleep(5)

def work3():
    time.sleep(5)

def work4():
    time.sleep(5)



if __name__ == '__main__':
    l=[]
    # print(os.cpu_count()) #本机为4核
    start=time.time()
    # p1=Process(target=work1) #
    # p2=Process(target=work2)
    # p3=Process(target=work3)
    # p4=Process(target=work4)

    p1=Thread(target=work1) #
    p2=Thread(target=work2)
    p3=Thread(target=work3)
    p4=Thread(target=work4)

    p1.start()
    p2.start()
    p3.start()
    p4.start()
    p1.join()
    p2.join()
    p3.join()
    p4.join()
    stop=time.time()
    print('run time is %s' %(stop-start)) 
    #多进程 5.162574291229248 
    #多线程 5.002141714096069

线程定时器

from threading import Timer,current_thread

def task(x):
    print('%s run....' %x)
    print(current_thread().name)

if __name__ == '__main__':
    t=Timer(3,task,args=(10,)) # 3s后执行该线程
    t.start()
    print('主')

线程queue

queue队列 :使用import queue,用法与进程Queue一样

queue is especially useful in threaded programming when information must be exchanged safely between multiple threads.

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

q=queue.Queue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
'''
结果(先进先出):
first
second
third
'''

class queue.LifoQueue(maxsize=0) #实现堆栈的效果 先进后出

import queue

q=queue.LifoQueue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
'''
结果(后进先出):
third
second
first
'''

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')
'''

基本socket代码模板

# 服务端
import socket
server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(('127.0.0.1',8081))
server.listen(5)

while True:
    conn,addr=server.accept()
    while True:
        try:
            msg=conn.recv(1024)
            if len(msg) == 0:break
            conn.send(msg.upper())
        except ConnectionResetError:
            print('客户端关闭了一个链接')
            break
    conn.close()
# 客户端
import socket
client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

client.connect(('127.0.0.1',8081))

while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    client.send(msg.encode('utf-8'))
    feedback=client.recv(1024)
    print(feedback.decode('utf-8'))

client.close()

思考socket服务端是io密集型程序,io密集型程序更适合使用多线程处理。

# 多线程改写socket服务端

多线程实现服务端并发

# 多线程服务端
from threading import Thread
import socket


def talk(conn):
    while True:
        try:
            msg = conn.recv(1024)
            if len(msg) == 0: break
            conn.send(msg.upper())
        except ConnectionResetError:
            print('客户端关闭了一个链接')
            break
    conn.close()

def socket_server_demo():
    server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server.bind(('127.0.0.1',8081))
    server.listen(5)
    while True:
        conn,addr=server.accept()
        t= Thread(target=talk,args=(conn,))
        t.start()

if __name__ == '__main__':
    socket_server_demo()
# 多线程客户端
import socket
from threading import currentThread,Thread

def task():
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 8081))
    while True:
        msg = f'{currentThread().name}'
        client.send(msg.encode('utf-8'))
        feedback = client.recv(1024)
        print(feedback.decode('utf-8'))

    client.close()


if __name__ == '__main__':
    for i in range(5):
        t = Thread(target=task)
        t.start()

同步异步

#所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回。按照这个定义,其实绝大多数函数都是同步调用。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。
#异步的概念和同步相对。当一个异步功能调用发出后,调用者不能立刻得到结果。当该异步功能完成后,通过状态、通知或回调来通知调用者。如果异步功能用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一 种很严重的错误)。如果是使用通知的方式,效率则很高,因为异步功能几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。

ps:

”同步“就好比:你去外地上学(人生地不熟),突然生活费不够了;此时你决定打电话回家,通知家里转生活费过来,可是当你拨出电话时,对方一直处于待接听状态(即:打不通,联系不上),为了拿到生活费,你就不停的oncall、等待,最终可能不能及时要到生活费,导致你今天要做的事都没有完成,而白白花掉了时间。
“异步”就是:在你打完电话发现没人接听时,猜想:对方可能在忙,暂时无法接听电话,所以你发了一条短信(或者语音留言,亦或是其他的方式)通知对方后便忙其他要紧的事了;这时你就不需要持续不断的拨打电话,还可以做其他事情;待一定时间后,对方看到你的留言便回复响应你,当然对方可能转钱也可能不转钱。但是整个一天下来,你还做了很多事情。 或者说你找室友临时借了一笔钱,又开始happy的上学时光了。

进程池&线程池

前言:

'''重点掌握概念
1、什么时候用池:
    池的功能是限制启动的进程数或线程数,
    什么时候应该限制???
    当并发的任务数远远超过了计算机的承受能力时,即无法一次性开启过多的进程数或线程数时
    就应该用池的概念将开启的进程数或线程数限制在计算机可承受的范围内。

2、同步vs异步
    同步、异步指的是提交任务的两种方式

    同步:提交完任务后就在原地等待,直到任务运行完毕后拿到任务的返回值,再继续运行下一行代码
    异步:提交完任务(可以绑定一个回调函数来实现)后根本就不在原地等待,直接运行下一行代码,等到任务有返回值后会自动触发回调函数
    回调机制:任务执行中处于某种机制的情况自动触发回调函数。

'''

'''


'''

Python标准模块--concurrent.futures

https://docs.python.org/dev/library/concurrent.futures.html

#1 介绍
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)
回调函数

进程池/线程池的基本用法

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
from threading import currentThread
from multiprocessing import current_process
import time,os


def task(i):
    # print(f'{currentThread().name} 在运行 任务{i}')
    print(f'{current_process().name} 在运行 任务{i}')
    time.sleep(0.2)
    return i**2

if __name__ == '__main__':
    pool = ProcessPoolExecutor(4)
    fu_list = []
    for i in range(20):
        future = pool.submit(task,i)
        # print(future.result())  # 拿不到值会阻塞在这里。
        fu_list.append(future)

    pool.shutdown(wait=True)  # 等待池内所有任务执行完毕
    for i in fu_list:
        print(i.result())# 拿不到值会阻塞在这里。

回调函数

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
from threading import currentThread
from multiprocessing import current_process
import time,os


def task(i):
    # print(f'{currentThread().name} 在运行 任务{i}')
    print(f'{current_process().name} 在运行 任务{i}')
    time.sleep(0.2)
    return i**2
def parse(future):
    # print(future.result())
    # print(currentThread().name,'拿到了结果',future.result()) # 如果是线程池 执行完当前任务 负责执行回调函数的是执行任务的线程。
    print(current_process().name,'拿到了结果',future.result()) # 如果是进程池 执行完当前任务 负责执行回调函数的是执行任务的是主进程


if __name__ == '__main__':
    pool = ProcessPoolExecutor(4)
    # pool = ThreadPoolExecutor(4)
    fu_list = []
    for i in range(20):
        future = pool.submit(task,i)
        future.add_done_callback(parse) # 绑定回调函数
        # 当任务执行结束拿到返回值的时候自动触发回调函数。并且把future当做参数直接传给回调函数parse

rocky手撸系列(了解)

我们每一个请求进来的时候都开一个进程肯定不合理,那么如果每一个请求进来都是串行的,那么根本实现不了并发,所以我们假定每一个请求进来使用的是线程。

那么线程中数据互相不隔离,存在修改数据的时候数据不安全的问题 。

假定我们的需求是,每个线程都要设置值,并且该线程打印该线程修改的值。

from threading import Thread,current_thread
import time

class Foo(object):
    def __init__(self):
        self.name = 0

locals_values = Foo()

def func(num):
    locals_values.name = num
    time.sleep(2)             # 取出该线程的名字
    print(locals_values.name, current_thread().name)

for i in range(10):
                                    # 设置该线程的名字
    t = Thread(target=func,args=(i,),name='线程%s'%i)
    t.start()

很明显阻塞了2秒的时间所有的线程都完成了修改值,而2秒后所有的线程打印出来的时候都是9了。
img

那我们的需求是想看到自己设置的值。

所以我们要解决这种线程不安全的问题,有如下两种解决方案。

- 方案一:是加锁

- 方案二:使用`threading.local`对象把要修改的数据复制一份,使得每个数据互不影响。

  我们要实现的并发是多个请求实现并发,而不是纯粹的只是修改一个数据,所以第二种思路更适合做我们每个请求的并发,把每个请求对象的内容都复制一份让其互相不影响。

  *详解:为什么不用加锁的思路?加锁的思路是多个线程要真正实现共用一个数据,并且该线程修改了数据之后会影响到其他线程,更适合类似于12306抢票的应用场景,而我们是要做请求对象的并发,想要实现的是该线程对于请求对象这部分内容有任何修改并不影响其他线程。所以使用方案二*

threading.local 模块

多个线程修改同一个数据,复制多份数据给每个线程用,为每个线程开辟一块空间进行数据存储

实例:

from threading import Thread,current_thread,local
import time

locals_values = local()
# 可以简单理解为,识别到新的线程的时候,都会开辟一片新的内存空间,相当于每个线程对该值进行了拷贝。

def func(num):
    locals_values.name = num
    time.sleep(2)
    print(locals_values.name, current_thread().name)

for i in range(10):
    t = Thread(target=func,args=(i,),name='线程%s'%i)
    t.start()

img

如上通过threading.local实例化的对象,实现了多线程修改同一个数据,每个线程都复制了一份数据,并且修改的也都是自己的数据。达到了我们想要的效果。

手动撸一个threading.local

实例:

from threading import get_ident,Thread,current_thread
# get_ident()可以获取每个线程的唯一标记,
import time

class Local(object):
    storage = {}# 初始化一个字典
    get_ident = get_ident # 拿到get_ident的地址
    def set(self,k,v):
        ident =self.get_ident()# 获取当前线程的唯一标记
        origin = self.storage.get(ident)
        if not origin:
            origin={}
        origin[k] = v
        self.storage[ident] = origin
    def get(self,k):
        ident = self.get_ident() # 获取当前线程的唯一标记
        v= self.storage[ident].get(k)
        return v

locals_values = Local()
def func(num):
    # get_ident() 获取当前线程的唯一标记
    locals_values.set('KEY',num)
    time.sleep(2)
    print(locals_values.get('KEY'),current_thread().name)

for i in range(10):
    t = Thread(target=func,args=(i,),name='线程%s'%i)
    t.start()

讲解:

利用get_ident()获取每个线程的唯一标记作为键,然后组织一个字典storage。

:{线程1的唯一标记:{k:v},线程2的唯一标记:{k:v}.......}

 {
    15088: {'KEY': 0}, 
    8856: {'KEY': 1},
    17052: {'KEY': 2}, 
    8836: {'KEY': 3}, 
    13832: {'KEY': 4}, 
    15504: {'KEY': 5}, 
    16588: {'KEY': 6}, 
    5164: {'KEY': 7}, 
    560: {'KEY': 8}, 
    1812: {'KEY': 9}
                    }

运行效果
img

进阶版(setattr getattr)

from threading import get_ident,Thread,current_thread
# get_ident()可以获取每个线程的唯一标记,
import time

class Local(object):
    storage = {}# 初始化一个字典
    get_ident = get_ident # 拿到get_ident的地址

    def __setattr__(self, k, v):
        ident =self.get_ident()# 获取当前线程的唯一标记
        origin = self.storage.get(ident)
        if not origin:
            origin={}
        origin[k] = v
        self.storage[ident] = origin
    def __getattr__(self, k):
        ident = self.get_ident() # 获取当前线程的唯一标记
        v= self.storage[ident].get(k)
        return v

locals_values = Local()
def func(num):
    # get_ident() 获取当前线程的唯一标记
    locals_values.KEY=num
    time.sleep(2)
    print(locals_values.KEY,current_thread().name)

for i in range(10):
    t = Thread(target=func,args=(i,),name='线程%s'%i)
    t.start()

进阶版2 每个对象有自己的存储空间(字典)

我们可以自定义实现了threading.local的功能,但是现在存在一个问题,如果我们想生成多个Local对象,但是会导致多个Local对象所管理的线程设置的内容都放到了类属性storage = {}里面,所以我们如果想实现每一个Local对象所对应的线程设置的内容都放到自己的storage里面,就需要重新设计代码。
实例:

from threading import get_ident,Thread,current_thread
# get_ident()可以获取每个线程的唯一标记,
import time

class Local(object):
    def __init__(self):
        # 千万不要按照注释里这么写,否则会造成递归死循环,死循环在__getattr__中,不理解的话可以全程使用debug测试。
        # self.storage = {}
        # self.get_ident =get_ident
        object.__setattr__(self,"storage",{})
        object.__setattr__(self,"get_ident",get_ident) #借用父类设置对象的属性,避免递归死循环。

    def __setattr__(self, k, v):
        ident =self.get_ident()# 获取当前线程的唯一标记
        origin = self.storage.get(ident)
        if not origin:
            origin={}
        origin[k] = v
        self.storage[ident] = origin
    def __getattr__(self, k):
        ident = self.get_ident() # 获取当前线程的唯一标记
        v= self.storage[ident].get(k)
        return v

locals_values = Local()
locals_values2 = Local()
def func(num):
    # get_ident() 获取当前线程的唯一标记
    # locals_values.set('KEY',num)
    locals_values.KEY=num
    time.sleep(2)
    print(locals_values.KEY,current_thread().name)
    # print('locals_values2.storage:',locals_values2.storage) #查看locals_values2.storage的私有的storage

for i in range(10):
    t = Thread(target=func,args=(i,),name='线程%s'%i)
    t.start()

显示效果我们就不做演示了,和前几个案例演示效果一样。

flask浅析flask框架原理

  • 情况一:单进程单线程,基于全局变量就可以做

  • 情况二:单进程多线程,基于threading.local对象做

  • 情况三:单进程多线程多协程,如何做?

    提示:协程属于应用级别的,协程会替代操作系统自动切换遇到 IO的任务或者运行级别低的任务,而应用级别的切换速度远高于操作系统的切换

    当然如果是自己来设计框架,为了提升程序的并发性能,一定是上诉的情况三,不光考虑多线程并且要多协程,那么该如何设计呢?

    在我们的flask中为了这种并发需求,依赖于底层的werkzeug外部包,werkzeug实现了保证多线程和多携程的安全,werkzeug基本的设计理念和上一个案例一致,唯一的区别就是在导入的时候做了一步处理,且看werkzeug源码。

    werkzeug.local.py部分源码

    ...
    
    try:
        from greenlet import getcurrent as get_ident # 拿到携程的唯一标识
    except ImportError:
        try:
            from thread import get_ident #线程的唯一标识
        except ImportError:
            from _thread import get_ident
    
    class Local(object):
        ...
    
        def __init__(self):
            object.__setattr__(self, '__storage__', {})
            object.__setattr__(self, '__ident_func__', get_ident)
    
          ...
    
        def __getattr__(self, name):
            try:
                return self.__storage__[self.__ident_func__()][name]
            except KeyError:
                raise AttributeError(name)
    
        def __setattr__(self, name, value):
            ident = self.__ident_func__()
            storage = self.__storage__
            try:
                storage[ident][name] = value
            except KeyError:
                storage[ident] = {name: value}
    

    讲解:

    原理就是在最开始导入线程和协程的唯一标识的时候统一命名为get_ident,并且先导入协程模块的时候如果报错说明不支持协程,就会去导入线程的get_ident,这样无论是只有线程运行还是协程运行都可以获取唯一标识,并且把这个标识的线程或协程需要设置的内容都分类存放于__storage__字典中。

协程

'''
1、协程:
    单线程实现并发
    在应用程序里控制多个任务的切换+保存状态
    优点:
        应用程序切换的速度要远远高于操作系统的切换的速度。
    缺点(对比多线程,多进程):
        多个任务一旦有一个阻塞没有切,整个线程都阻塞在原地
        该线程内的其他的任务都不能执行了
        一旦引入协程,就需要检测单线程下所有的IO行为,


2、协程序的目的:
    想要在单线程下实现并发
    并发指的是多个任务看起来是同时运行的
    并发=切换+保存状态
    
    3、思考什么样的协程有意义?
    盲目切换反而会导致效率低下,遇到io切换的协程才会提升单个线程下的执行效率(降低io时间)
'''

通过yield实现协程

import time
def func1():
    while True:
        1000000+1
        yield

def func2():
    g = func1()
    for i in range(100000000):
        i+1
        next(g)

start = time.time()
func2()
stop = time.time()
print(stop - start) # 28.522686004638672

### 对比通过yeild切换运行的时间反而比串行更消耗时间,这样实现的携程是没有意义的。
import time

def func1():
    for i in range(100000000):
        i+1
def func2():
    for i in range(100000000):
        i+1

start = time.time()
func1()
func2()
stop = time.time()
print(stop - start) # 17.141255140304565

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的返回值
import gevent
def eat(name):
    print('%s eat 1' %name)
    gevent.sleep(2)
    print('%s eat 2' %name)

def play(name):
    print('%s play 1' %name)
    gevent.sleep(1)
    print('%s play 2' %name)


g1=gevent.spawn(eat,'egon')
g2=gevent.spawn(play,name='egon')
g1.join()
g2.join()
#或者gevent.joinall([g1,g2])
print('主')

上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,

而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了

from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前

或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头

from gevent import monkey
monkey.patch_all()

import gevent
import time
def eat():
    print('eat food 1')
    time.sleep(2)
    print('eat food 2')

def play():
    print('play 1')
    time.sleep(1)
    print('play 2')
start = time.time()
g1 = gevent.spawn(eat)
g2 = gevent.spawn(play)
g1.join()
g2.join()
# gevent.joinall([g1,g2])
end = time.time() # 3.0165441036224365
# 如果打好了补丁 就可以识别非gevent.sleep阻塞进行切换
print(end-start)

socket服务端单线程并发。

通过gevent实现单线程下的socket并发(from gevent import monkey;monkey.patch_all()一定要放到导入socket模块之前,否则gevent无法识别socket的阻塞)

from threading import Thread
import socket
from gevent import monkey;monkey.patch_all()
import gevent


def talk(conn):
    while True:
        try:
            msg = conn.recv(1024)
            if len(msg) == 0: break
            conn.send(msg.upper())
        except ConnectionResetError:
            print('客户端关闭了一个链接')
            break
    conn.close()

def socket_server_demo():
    server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server.bind(('127.0.0.1',8081))
    server.listen(5)
    while True:
        conn,addr=server.accept()
        gevent.spawn(talk,conn)

if __name__ == '__main__':
    g = gevent.spawn(socket_server_demo)
    g.join()

转自张明岩博客

posted @ 2019-09-22 10:30  極9527  阅读(235)  评论(0编辑  收藏  举报