进程

什么是进程

  1. 进程:
    正在进行中的程序叫进程。
    一个程序不运行时不占内存。如两个程序在运行,实际上是两个进程在运行。一个程序如果不运行只是一个文件。
  2. pid:
    是一个全系统唯一的对某个进程的标识。
    随着这个进程的重启,pid可能会变化。
    每个运行的程序都有一个pid,而且是唯一的。且在一个进程工作中pid不变。如果程序再次启动。pid会变化。
  • 同一个程序执行两次,在操作系统中会出现两个进程。
    在这里插入图片描述
  1. 进程是操作系统中最小的资源分配单位
    资源分配:分配给程序多少内存,寄存器。。。
  2. 进程与进程间数据是隔离的。为了保证数据安全,无论基于文件还是基于网络socket进程间不可能互相通信。

进程调度

如果有很多进程都找操作系统执行。先执行谁后执行谁是有规则的。多个起来的进程被cpu执行应该遵循的规律。越短的任务分到cpu越多 ,刚开始进来的程序会迅速分到cpu,分到的时间片如果遇到io阻塞cpu不工作。

  1. 先来先服务调度算法 FCFS
  2. 短作业优先调度算法
  3. 时间片轮转法
  4. 多级反馈队列
    垃圾回收机制中的分袋回收和多级反馈队列很像。

进程的并行与并发

并行: 两者同时运行,两个cpu同时运行两个程序。 微观上多个程序在一个时间片内在cpu上同时工作。
并发: 只有一个cpu。可能有5个程序,这5个程序轮流在一个cpu上运行。宏观上多个程序在cpu上同时工作。就是看起来是同时工作的,至于这些程序再一个时间片内在一个或者多个cpu上工作是不确定的。并发包含并行,并行是并发里面的最快的一种情况。
无论并发还是并行。 看起来都是这几个程序同时运行。只不过不知道内部用了两个还是三个还是5个cpu。
对于早期的计算机,只有一个cpu没有并行的概念。有多核了后才有并行的概念

进程三状态图

进程三状态:就绪,运行,阻塞
在这里插入图片描述
补充:如果程序进入io阻塞,分给该程序的时间片还是会是多少就是多少,不是说进入阻塞cpu就去执行下一个程序le,而是cpu进入一个空闲的状态。这个程序的时间片到了才执行下一个任务。
比如分给一个任务5秒的时间片,而这个任务执行1秒就遇到io阻塞,还是会占用cpu4秒的时间,不是说到了io阻塞分给我的时间片还没执行完就把cpu资源分给下一个任务了。而是时间片到了cpu资源释放上一个任务的资源,再来做新任务。

在这里插入图片描述

同步,异步,阻塞,非阻塞

  • 同步:两件事,一件事做完了再去做另外一件事
  • 同步控制:多个进程只有一个进程在工作,如加锁,就变成一个进程完成后然后才能进行下一个进程。而微观上从很小的方面看是同步的,但整体上看还是各做各的,所以宏观上来看是异步程序。
  • 异步:两件事同时做
    程序能够同时做多件事情,多件事情包括阻塞事件,如果有200个进程,有199个都在阻塞,另外一个进程在工作着,也是异步程序,因为阻塞也是自己在做,不是同步
  • 阻塞:停住了,recv,accept,recvfrom,input,sleep,会让整个进程进入阻塞队列
  • 非阻塞:不停,进程只会在就绪和运行状态中切换。
  • 同步阻塞:既是同步,又是阻塞。 阻塞是同步过程中只能等着。不能充分利用cpu
  • 异步非阻塞:异步时没有阻塞事件,即使有阻塞也不等了。过度利用cpu。
  • io 多路复用:比较完善的在网络编程中的解决方案,只限于在网络编程中,而同步阻塞和异步非阻塞普遍存在于程序中。

进程的创建和结束

进程的创建:

  • 用户的交互式请求,如双击qq
  • 启动操作系统时的开机程序
  • 在一个进程运行中开启了一个子进程。如在pycharm中run一个py程序

进程的结束:

  • 正常退出
  • 出错退出,如python中报错
  • 严重错误,如执行非常指令
  • 被其他进程杀死。如在pycharm中强制结束任务。在任务管理器中结束程序。

在python程序中的进程操作

在pytharm程序中除了直接启动一个py文件来开启进程外。当有很多任务需要完成时。也可以在python程序中再起几个进程。 进程中是并行的关系,是可以利用多个cpu的,如果在程序中起4个进程,这四个进程可以同时运行了。也就是形成异步了。可以在不同进程中做不同的事情。

multiprocess模块

这个模块可以直接帮助调度操作系统。因为只要涉及进程是操作系统帮助开启的。
进程的调用和线程几乎一样,他们用的API(Application Program Interface)结构和内容几乎一致。

开启子进程方法一,创建multiprocess.Process对象

开启子进程方法一

开启进程的第二种方式

开启进程第二种方式

查看进程id
from multiprocessing import Process
import os
import time

def info(title):
    print('title: ', title)
    print('parent process: ', os.getppid())  # 上级进程id
    print('process id: ', os.getpid())  # process id 当前进程id

if __name__ == '__main__':
    info('main process line')

    time.sleep(1)
    print('--------------------')
    p = Process(target=info, args=('laura',))
    p.start()
    p.join()

运行结果

title:  main process line
parent process:  9984
process id:  15508
--------------------
title:  laura
parent process:  15508
process id:  21604
守护进程

deamon是Process类中的一个属性。设置为True表示这个子进程为守护进程。守护进程就是主进程守护子进程,我主进程结束了你子进程也要结束,我守护你。
在start()方法上面设置正常情况下主进程会等待子进程完全结束才结束。如果设置了一个守护进程,守护进程会随着主进程的代码执行完毕而结束。而不是等待整个主进程都结束。
为什么?守护进程也想等到主进程整体结束而结束。但是守护进程除了是个守护进程还是一个子进程。子进程有个特点,必须在主进程前结束。因为主进程要给他收尸。所以守护进程不可能在主进程结束他才结束。
所以主进程会等待子进程结束而结束。守护进程只等待主进程代码结束就结束了。
守护进程的作用:程序的报活,子进程汇报自己是否活着。
主程序,守护进程:每隔五分钟就向一台机器汇报自己的状态,只有能汇报,证明还在运行。主进程还可以分配任务给子进程,否则,证明程序已经关闭。

Lock属于multipocessing的一个类
为了在进程之间保证数据的安全, 在异步的情况下,多个进程又可能同时修改同一份资源,这份资源或文件就不安全。(进程中内存数据是安全的)就给这个修改的过程加上锁。
加锁,降低了程序的效率,让原原来能够同时执行的代码变成了顺序执行。异步变同步的过程。
互斥锁: 在同一个进程中有两个连续acquire而不release的话,是执行不下去的。程序就锁死了,这种现象也叫死锁现象。所以必须一次accuire对应一次release。

信号量

Semaphore属于multipocessing中的一个类
锁是一把钥匙,信号量是很多把钥匙。锁的变形,同时只有一个人能执行变成同时一些人执行。主要控制一段代码被几个进程同时执行。
信号量的实现机制:
信号量主要是在多进程中控制资源的。底层是计数器+锁实现的。

事件event

Event属于multiprocessing中的一个类
Event提供了两个方法

  1. wait() 方法 ,开启事件时默认为False,也就是阻塞。是否阻塞使用e.is_set()看event对象内部的一个属性。如果为True,就通过,如为False,就阻塞。也就是内部有一个信号控制wait的阻塞事件。而控制这个信号需要以下几个方法。
  2. 怎样控制该属性的值:
  • set() 将这个属性的值改为True
  • clear() 将这个属性的值改为False
  • is_set() 查看当前的属性是否为True

from multiprocessing import Process,Lock,Event
import time
import random


def t(e):
    print('\033[31m红灯亮\033[0m')
    while True:
        if e.is_set():  # e = True
            time.sleep(2)
            print('\033[31m红灯亮\033[0m')
            e.clear()  # e = False
        else:
            time.sleep(2)
            print('\033[32m绿灯亮\033[0m')
            e.set()  # e = True

def car(e, i):
    if not e.is_set():  # e = False
        print('car %s 在等待' % i)    # 红灯就等着,绿灯才能通过
        e.wait()   # e默认是False,阻塞直到e = True 才能走
        time.sleep(5)
    print('car %s 通过了' % i)



if __name__ == '__main__':
    e = Event()  # e = False
    p = Process(target=t, args=(e,))
    p.daemon = True   # 属性
    p.start()
    p_lst = []
    for i in range(20):
        time.sleep(random.randrange(0, 3, 2))   # 随机几秒种来一辆车,
        u = Process(target=car, args=(e, i))
        u.start()
        p_lst.append(u)
    for j in p_lst: j.join()  # 列表中的进程全部执行结束,主进程的代码才算执行完,守护进程才能跟着执行挂

运行结果

红灯亮
绿灯亮
car 0 通过了
car 1 通过了
car 2 通过了
红灯亮
car 3 在等待
绿灯亮
car 4 通过了
红灯亮
car 7 在等待
car 8 在等待
car 5 在等待
car 6 在等待
car 9 在等待
car 10 在等待
car 11 在等待
绿灯亮
car 12 通过了
car 3 通过了
红灯亮
car 13 在等待
绿灯亮
car 14 通过了
car 15 通过了
car 16 通过了
car 11 通过了
car 7 通过了
car 9 通过了
car 8 通过了
car 6 通过了
car 10 通过了
car 5 通过了
红灯亮
car 17 在等待
绿灯亮
car 18 通过了
car 13 通过了
红灯亮
car 19 在等待
绿灯亮
car 17 通过了
红灯亮
绿灯亮
car 19 通过了

Process finished with exit code 0

进程间通信 IPC

IPC: inter-process communication
event,lock,semaphore都用到进程内部偷偷的通信。只不过通信内容是一些信号。这些信号是我们不能改变的。他们有自己的通信机制。我们只能用realease等去控制。底层用socket基于文件家族通信的方去传递一些信号。如p.join()怎样知道子进程已经结束了呢?因为子进程执行了sk.send(‘finished’) ,而p.join()本质是一个recv事件。等着子进程发送固定的信息。收到该信息后再告诉主进程。
进程之间虽然内存不共享,在内存占两个空间,而线程与子线程数据是共享的,内存地址一样。进程之间是可以通信的。
multiprocessing程序提供了两种通信方法,满足不同需求。

进程队列

进程队列

管道

进程管道

进程池

进程池

进程间数据共享

默认情况下进程间数据不共享
multiprocessing 模块中的manager模块把实现了数据共享的比较便捷的类都重新封装了一遍,并且在原有的multiprocessing基础上增加了新的机制。如dict,list,Lock,Semaphone,Event,Queue,Value等,正常情况下,list和dict不能实现进程中数据共享。但是由于,但是在manager类中做了处理,使得这些数据变得共享了。

manager

Queue和pipe只是实现了数据交互,并没有实现数据共享,即一个进程取更改另一个进程的数据。

  • 例1
from multiprocessing import Manager, Process, Lock
def work(d, lock):
    d['count'] -= 1

if __name__ == '__main__':
    lock = Lock()
    m = Manager()
    dic = m.dict({'count': 100})   # 该dict成了进程间共享数据
    p_l = []
    for i in range(100):
        p = Process(target=work, args=(dic, lock))   # 开100个进程
        p_l.append(p)
        p.start()
    for i in p_l:
        p.join()
    print(dic)

运行结果

{'count': 3}

为什么最后结果是3呢?
底层原理图示
在这里插入图片描述

在这里插入图片描述
由于数据不安全,所以要加锁

from multiprocessing import Manager, Process, Lock
def work(d, lock):
    lock.acquire()
    d['count'] -= 1
    lock.release()

if __name__ == '__main__':
    lock = Lock()
    m = Manager()
    dic = m.dict({'count': 100})   
    p_l = []
    for i in range(100):
        p = Process(target=work, args=(dic, lock))  
        p_l.append(p)
        p.start()
    for i in p_l:
        p.join()
    print(dic)

运行结果

{'count': 0}
python的上下文管理

with open(‘kkk’) as f:
然后可以操作文件了,之所以能用with语句,因为执行文件句柄操作是先open(),最后执行close(),这个机制在python叫上下文管理:
语法
with…
一大段语句

在执行一大段语句之前自动做某个操作 open
在执行一大段语句之后做某个操作 close

所以以上例子
lock.acquire()
d[‘count’]-=1
lock.release()
可以写为
with lock():
d[‘count’]-=1

m = Manager()可以写成
with manager() as m
面向对象的魔术方法还有
__enter\ __
__exit__

  • 例2
from multiprocessing import Process, Manager
def f(d, l, n):
    d[n] = '1'  # 当n=0: {0:’1‘}
    l.append(n)   # [0]

if __name__ == '__main__':
    with Manager() as manager:
        d = manager.dict()  # {}
        l = manager.list()  # []
        p_list = []
        for i in range(10):
            p = Process(target=f, args=(d, l, i))
            p.start()
            p_list.append(p)

        for res in p_list:
            res.join()
        print(d)  # 每个进程中的d和l内容都一样的,只是地址不一样。# {0: '1', 2: '1', 1: '1', 3: '1', 4: '1', 5: '1', 6: '1', 7: '1', 8: '1', 9: '1'}
        print(l)  # 每个进程执行时间不一样,列表值中顺序也并不一样,但最终都加进了共享的列表# [0, 2, 1, 3, 4, 5, 6, 7, 8, 9]

数据共享机制
支持数据类型非常有限
list dict都不是数据不安全的,需要自己加锁来保证数据安全。由于加锁比较麻烦,代码也容易出错。
所以使用第三方工具比较方便

回调函数

因为有了回调函数,所以进程池中子进程的返回值不重要了
回调函数只在进程池里面才有

from multiprocessing import Pool
def func(i):
    print('第一个任务')
    return '*' * i    

def call_back(res):  # 回调函数一般有两个函数
    print('res-->', res)

if __name__ == '__main__':
    p = Pool()
    p.apply_async(func, args=(1,), callback=call_back)
    p.close()
    p.join()

运行结果

第一个任务
res--> *

回调函数在什么情况下执行:
当func执行完毕后执行callback函数,
且只能接受一个参数
func的返回值会作为callback的参数

回调函数是哪个进程执行的??
答案是主进程,因为子进程执行完了后又回到主进程来执行。所以才叫回调函数。

from multiprocessing import Pool
import os
def func(i):
    print('子进程id:', os.getpid(), '父进程id:',os.getppid())
    print('第一个任务')
    return '*' * i, 111

def call_back(res): 
    print('回调函数执行id:', os.getpid(), '父进程id:', os.getppid())
    print('res-->', res)

if __name__ == '__main__':
    p = Pool()
    print('主进程id:', os.getpid(), '父进程id:', os.getppid())
    p.apply_async(func, args=(1,), callback=call_back)
    p.close()
    p.join()

执行结果

主进程id: 23624 父进程id: 22684
子进程id: 9820 父进程id: 23624
第一个任务
回调函数执行id: 23624 父进程id: 22684
res--> ('*', 111)

在这里插入图片描述
为什么要在主进程执行回调函数:
1,使用主进程统一获取结果,这样的话在主进程之间没有数据隔离,就可以操作主进程中全局变量或一些资源了。如果全放在子进程没有办法汇总。
2、多个子进程做的事情比较长时间,如访问网站等,而在主进程中一般情况下回调函数的执行速度都比较快,假设开50个进程,当50个进程所有返回值返回给主进程并处理数据时,就要一个进程做一件事情的时间我要做50件事情。这样的情况下适合用回调函数。也就是当子进程中访问网络,数据分析等重复性高的高io的,比较复杂的工作,主进程拿着数据分析结果来做汇总工作时,这时没有io阻塞,只是计算。

子进程去访问网页,主进程处理网页的结果

from urllib.request import urlopen
from multiprocessing import Pool
import re
url_lst = [
    'http://www.baidu.com',
    'http://www.souhu.com',
    'http://www.sogou.com',
    'http://www.4399.com',
    'http://www.cnblogs.com'
]

def get_url(url):
    response = urlopen(url)
    ret = re.search('www\.(.*?)\.com', url)
    print('%s finished' % ret.group(1))
    return (ret.group(1), response.read())

def call(content):
    url, con = content
    with open(url+'.html', 'wb') as f:
        f.write(con)
if __name__ == '__main__':
    p = Pool()
    for url in url_lst:
        p.apply_async(get_url, args=(url,), callback=call)
    p.close()
    p.join()

运行结果

baidu finished
4399 finished
sogou finished
cnblogs finished

为什么不直接拿返回值然后在主进程中处理结果,而用回调函数去处理每个子进程返回的数据呢:
代码简洁度不同
效率问题
在这里插入图片描述

from multiprocessing import Process, Pool
import time, os
def Foo(i):
    time.sleep(1)
    print('son', os.getpid())  # son 3084  # son 26148  # son 3084  # son 26148
    return 'hello %s' % i

def Bar(arg):
    print('arg', arg)  # arg hello 0  # arg hello 1  # arg hello 2  # arg hello 3
    print('Bar', os.getpid())  # Bar 6888
if __name__ == '__main__':
    pool = Pool(2)
    print('main pid', os.getpid())  # main pid 6888
    for i in range(4):
        pool.apply_async(func=Foo, args=(i,), callback=Bar)  # 回调函数的参数默认为func的返回值。回调函数是在主进程中调用的,如果主进程和子进程共同写一份文件,由于资源不共享,于是在主进程中调用子进程中的返回值写进此文件就不用在每个进程中建文件了。如写日志时。
    pool.close()  
    pool.join()
    print('end')

运行结果

main pid 6888
0
son 3084
arg hello 0
Bar 6888
1
son 26148
arg hello 1
Bar 6888
2
son 3084
arg hello 2
Bar 6888
3
son 26148
arg hello 3
Bar 6888
end
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值