并发编程

 背景知识

进程即正在执行的一个过程,进程是对正在运行程序的一个抽象。

进程的概念起源于操作系统,是操作系统最核心的概念,也是操作系统提供的最古老也是最重要的抽象概念之一。操作系统的其他所有内容都是围绕进程的概念展开的

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

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

进程与程序

进程是正在运行的程序,程序是程序员编写的一堆代码,也就是一堆字符,当这堆代码被系统加载到内存中并执行时,就有了进程。

例如:生活中我们会按照菜谱来做菜,那么菜谱就是程序,做菜的过程就是进程

需要注意的是:一个程序是可以产生多个进程的,就像我们可以同时运行多个QQ程序一样,会形成多个进程

测试:

import time
while True:
time.sleep(1)
多次运行该文件,就会产生多个python.exe进程,可以通过tasklist来查看运行的程序

并发与并行

无论是并行还是并发,在用户看来都是'同时'运行的,不管是进程还是线程,都只是一个任务而已,真是干活的是cpu,cpu来做这些任务,而一个cpu同一时刻只能执行一个任务

一 并发:是伪并行,即看起来是同时运行。单个cpu+多道技术就可以实现并发,(并行也属于并发)

二 并行:同时运行,只有具备多个cpu才能实现并行

         单核下,可以利用多道技术,多个核,每个核也都可以利用多道技术(多道技术是针对单核而言的

         有四个核,六个任务,这样同一时间有四个任务被执行,假设分别被分配给了cpu1,cpu2,cpu3,cpu4,

         一旦任务1遇到I/O就被迫中断执行,此时任务5就拿到cpu1的时间片去执行,这就是单核下的多道技术

         而一旦任务1的I/O结束了,操作系统会重新调用它(需知进程的调度、分配给哪个cpu运行,由操作系统说了算),可能被分配给四个cpu中的任意一个去执行

同步异步and阻塞非阻塞

同步:

所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回。

异步:

异步的概念和同步相对。当一个异步功能调用发出后,调用者不能立刻得到结果。当该异步功能完成后,通过状态、通知或回调来通知调用者

阻塞:

阻塞调用是指调用结果返回之前,当前线程会被挂起(如遇到io操作)。函数只有在得到结果之后才会将阻塞的线程激活。

非阻塞:

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

PID和PPID

PID

在一个操作系统中通常都会运行多个应用程序,也就是多个进程,那么如何来区分进程呢?

系统会给每一个进程分配一个进程编号即PID,如同人需要一个身份证号来区分。

验证:

tasklist 用于查看所有的进程信息

taskkill /f /pid pid 该命令可以用于结束指定进程

# 在python中可以使用os模块来获取pid
import os
print(os.getpid())
PPID

当一个进程a开启了另一个进程b时,a称为b的父进程,b称为a的子进程

在python中可以通过os模块来获取父进程的pid

# 在python中可以使用os模块来获取ppid
import os
print("self",os.getpid()) # 当前进程自己的pid
print("parent",os.getppid()) # 当前进程的父进程的pid
如果是在pycharm中运行的py文件,那pycahrm就是这个python.exe的父进程,当然你可以从cmd中来运行py文件,那此时cmd就是python.exe的父进程

python中开启子进程的两种方式

方式1:

实例化Process类

from multiprocessing import Process
import time

def task(name):
   print('%s is running' %name)
   time.sleep(3)
   print('%s is done' %name)
if __name__ == '__main__':
   # 在windows系统之上,开启子进程的操作一定要放到这下面
   # Process(target=task,kwargs={'name':'egon'})
   p=Process(target=task,args=('jack',))
   p.start() # 向操作系统发送请求,操作系统会申请内存空间,然后把父进程的数据拷贝给子进程,作为子进程的初始状态
   print('======主')
方式2:

继承Process类 并覆盖run方法

from multiprocessing import Process
import time

class MyProcess(Process):
   def __init__(self,name):
       super(MyProcess,self).__init__()
       self.name=name

   def run(self):
       print('%s is running' %self.name)
       time.sleep(3)
       print('%s is done' %self.name)
if __name__ == '__main__':
   p=MyProcess('jack')
   p.start()
   print('主')
需要注意的是

1.在windows下 开启子进程必须放到__main__下面,因为windows在开启子进程时会重新加载所有的代码造成递归创建进程

2.第二种方式中,必须将要执行的代码放到run方法中,子进程只会执行run方法其他的一概不管

进程间内存相互隔离

from multiprocessing import Process
import time
x=1000
def task():
   global x
   x=0
   print('儿子死啦',x)


if __name__ == '__main_
   print(x)
   p=Process(target=task)
   p.start()
   time.sleep(5)
   print(x)
join函数

调用start函数后的操作就由操作系统来玩了,至于何时开启进程,进程何时执行,何时结束都与应用程序无关,所以当前进程会继续往下执行,join函数就可以是父进程等待子进程结束后继续执行

案例1:

from multiprocessing import Process
import time

x=1000

def task():
   time.sleep(3)
   global x
   x=0
   print('儿子死啦',x)
if __name__ == '__main__':
   p=Process(target=task)
   p.start()

   p.join() # 让父亲在原地等
   print(x)
案例2:

from multiprocessing import Process
import time,random

x=1000

def task(n):
   print('%s is runing' %n)
   time.sleep(n)

if __name__ == '__main__':
   start_time=time.time()

   p1=Process(target=task,args=(1,))
   p2=Process(target=task,args=(2,))
   p3=Process(target=task,args=(3,))
   p1.start()
   p2.start()
   p3.start()

   p3.join() #3s
   p1.join()
   p2.join()

   print('主',(time.time() - start_time))

   start_time=time.time()
   p_l=[]
   for i in range(1,4):
       p=Process(target=task,args=(i,))
       p_l.append(p)
       p.start()
   for p in p_l:
       p.join()
   
   print('主',(time.time() - start_time))
Process对象常用属性

from multiprocessing import Process
def task(n):
   print('%s is runing' %n)
   time.sleep(n)

if __name__ == '__main__':
   start_time=time.time()
   p1=Process(target=task,args=(1,),name='任务1')
   p1.start() # 启动进程
   print(p1.pid) # 获取进程pid
   print(p1.name) # 获取进程名字
   p1.terminate() # 终止进程
   p1.join() # 提高优先级
   print(p1.is_alive()) # 获取进程的存活状态
   print('主')

孤儿进程与僵尸进程

什么是孤儿进程

孤儿进程指的是开启子进程后,父进程先于子进程终止了,那这个子进程就称之为孤儿进程

例如:qq聊天中别人发给你一个链接,点击后打开了浏览器,那qq就是浏览器的父进程,然后退出qq,此时浏览器就成了孤儿进程

孤儿进程是无害的,有其存在的必要性,在父进程结束后,其子进程会被操作系统接管。

什么是僵尸进程

僵尸进程指的是,当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被操作系统接管,子进程退出后操作系统会回收其占用的相关资源!

僵尸进程的危害:

由于子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束. 那么会不会因为父进程太忙来不及wait子进程,或者说不知道 子进程什么时候结束,而丢失子进程结束时的状态信息呢? 不会。因为UNⅨ提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就必然可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放. 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生[僵死进程],将因为没有可用的进程号而导致系统不能产生新的进程. 此为僵尸进程的危害,应当避免。

原文地址:https://www.cnblogs.com/zhangdajin/p/11148851.html