漫天飞雪

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理
内容:
    1. 开启进程的两种方式(*****)
    2. 进程对象的join方法(*****)
    3. 进程之间内存空间隔离(*****)
    4. 进程对象其他相关的属性或方法
    5. 僵尸进程与孤儿进程
    6. 守护进程
    7. 互斥锁
进程的理论:
 
1 什么是进程
  进程指的是一个正在进行/运行的程序,进程是用来描述程序执行过程的虚拟概念

进程vs程序
  程序:一堆代码
  进程:程序的执行的过程

进程的概念起源于操作系统,进程是操作系统最核心的概念,操作系统其它所有的概念都是围绕进程来

操作系统理论:
  1. 操作系统是什么?
    操作系统是一个协调\管理\控制计算机硬件资源与应用软件资源的一段控制程序
  有两大功能:
    1. 将复杂的硬件操作封装成简单的接口给应用程序或者用户去使用
    2. 将多个进程对硬件的竞争变得有序

操作系统发展史
  并发: 多个任务看起来是同时运行的
  串行:一个任务完完整整地运行完毕,才能运行下一个任务

多道技术:(复用=>共享/共用)
  1. 空间上的复用:多个任务复用内存空间
  2. 时间上的复用:多个任务复用cpu的时间
    1. 一个任务占用cpu时间过长会被操作系统强行剥夺走cpu的执行权限:比起串行执行反而会降低效率
    2. 一个任务遇到io操作也会被操作系统强行剥夺走cpu的执行权限:比起串行执行可以提升效率
Windows系统:子进程不仅完全copy父进程的数据,还有会造一些新的数据
Linux系统:子进程和父进程的初始状态数据是完全相同
一旦进程运行起来,子进程和父进程之间内存之间互相隔离,互不影响
应用程序发起开启子进程的请求,操作系统开启子进程,在操作系统眼中.
所有的进程地位都是相同的
 
进程的三种状态:

  运行态:程序申请到cpu执行权限
  阻塞态:执行IO操作时
  就绪态:占用cpu时间过长,被操作系统收回或者被其它优先级更高的进程
cpu抢走
阻塞态只能先进入就绪态,就绪态可以随时进入运行态
我们只能控制阻塞态
程序的效率想要更高,尽量避免IO操作
 
开启子进程:
  有两种方法,开启进程,
    ①使用Process包直接产生进程实例。
    ②使用类的继承,添加绑定方法,进行进程类的实例化

开启子进程的方式一:
导入Process模块,通过process类实例化出一个对象,调用run
方法开启子进程
from multiprocessing import Process
import time

def task(name):
    print('%s is running' %name)
    time.sleep(3)
    print('%s is done' %name)

# 在windows系统上,开启子进程的操作必须放到if __name__ == '__main__'的子代码中
if __name__ == '__main__':
    p=Process(target=task,args=('egon',))
    #Process(target=task,kwargs={'name':'egon'})
    p.start() # 只是向操作系统发送了一个开启子进程的信号
    print('主')
结果:

egon is running
egon is done

开启子进程的方式二:
重写Process类的run方法,调用重写的Myprocess类实例化出一个对象,通过对象调用run方法开启子进程
from multiprocessing import Process
import time

class Myprocess(Process):
    def __init__(self,name):
        super().__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('egon')
    p.start() # 只是向操作系统发送了一个开启子进程的信号
    print('主')
结果:

egon is running
egon is done
 
进程对象的join方法:
join:让主进程在原地等待,等待子进程运行完毕,不会影响子进程的执行
例:
from  multiprocessing import Process
import time
def task(name,i):
    print(' %s is running' %name)
    time.sleep(i)
    print('%s is done' %name)
if __name__ == '__main__':
    start_time=time.time()
    l=[]
    for i in range(1,4):
        obj=Process(target=task,args=('peanut%s'%i,i))
        l.append(obj)
        obj.start()
    for p in l:
        p.join()#遇到join就返回执行子进程代码,直到都执行完毕后,再最后执行join下代码
    print(time.time()-start_time)
    print('master')
运行结果:
peanut1 is running
peanut3 is running
peanut2 is running
peanut1 is done
peanut2 is done
peanut3 is done
3.507101058959961
master
进程之间内存空间互相隔离:
验证方法:定义一个全局变量,在子进程中通过global方法修改变量的值,
在子进程和父进程中输出变量的值来验证
from multiprocessing import Process
n=111
def task():
    global n
    n=1
    print('son:',n)
if __name__ == '__main__':
    p=Process(target=task)
    p.start()
    p.join()
    print(n)
输出结果:
son: 1
111 
进程对象其它相关的属性或方法
1.导入current_process模块,调用其下的pid属性,得到进程的pid
from multiprocessing import Process,current_process
import time
def task():
    print('%s is running' %current_process().pid)
    time.sleep(3)
    print('%s is done' %current_process().pid)
if __name__ == '__main__':
    p=Process(target=task)
    p.start()
    print('主',current_process().pid)
结果:
 5808
9040 is running
9040 is done

2.导入os模块,调用其下的getpidgetppid方法,得到子进程及其父进程的
pid
from multiprocessing import Process,current_process
import time,os
def task():
  print('%s is run 爹是:%s' %(os.getpid(),os.getppid()))
  time.sleep(30)
  print('%s is done 爹是:%s' %(os.getpid(),os.getppid()))
if __name__ == '__main__':
    p=Process(target=task)
    p.start()
    print('主:%s 主他爹:%s' %(os.getpid(),os.getppid()))
结果:
主:12848 主他爹:9208
13284 is run 爹是:12848
13284 is done 爹是:12848

3.Process模块下有terminate方法,用于杀死子进程
is_alive方法 判断子进程存活状态(返回True/False)
from multiprocessing import Process,current_process
import time,os
def task():
    print('%s is run 爹是:%s' %(os.getpid(),os.getppid()))
    time.sleep(3)
    print('%s is done 爹是:%s' %(os.getpid(),os.getppid()))
if __name__ == '__main__':
    p=Process(target=task,name='子进程1')
    p.start()
    print(p.name)
    p.terminate()
    time.sleep(0.1)
    print(p.is_alive())
    print('主:%s 主他爹:%s' %(os.getpid(),os.getppid()))
结果:
子进程1
False
主:13216 主他爹:9208
 
僵尸进程和孤儿进程:


僵尸进程指的是子进程死了之后,并没有死干净,会将子进程的pid,占用cpu的时间等状态信息保存下来,
    将进程占用的内存空间,打开的文件资源等释放.所有的子进程都会进入僵尸状态.linux系统下通过
    ps aux |grep 'Z'过滤出僵尸进程.

两种方法回收僵尸进程:
    1.主进程通过wait系统接口(join方法存在wait方法)回收僵尸进程
    2.主进程死后,操作系统会自动将其子进程的僵尸进程进行回收
    
僵尸进程主要是占用pid号和内存

孤儿进程指的是父进程死后,子进程没有死
    Linux系统所有进程都有一个init祖宗进程(其pid0),init会定期回收僵尸进程
    
实例
from multiprocessing import Process
import time

def task(name):
    print('slave:%s is start'%name)
    time.sleep(2)
    print('slave:%s is over'%name)
if __name__ == '__main__':
    process=Process(target=task,args=('joke',))
    process.start()
    print('master is over')
    process.join()#把子进程的join方法放置在最后,模拟出,主进程死后,子进程还存活
 

守护进程
小贴士:
    守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。很多守护进程在
系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束。

守护进程本质就是一个"子进程",该"子进程"的生命周期<=被守护进程的生命周期
主进程通过daemon方法创建守护进程
  其一:守护进程会在主进程代码执行结束后就终止
  其二:守护进程内无法再开启子进程,否则抛出异常
注意:进程之间是互相独立的,主进程代码运行结束,守护进程随即终止
例:
from multiprocessing import Process
import time
def task(name):
    print('老太监%s 活着...'%name)
    time.sleep(3)
    print('老太监%s在正常死亡'%name)
if __name__ == '__main__':
    p=Process(target=task,args=('lmz',))
    p.daemon=True#创建守护进程
    p.start()
    time.sleep(4)#注意程序睡眠时间,跟下面实例对比,查看输出结果
    print('皇帝lmj正在驾崩')
结果:
老太监lmz 活着...
老太监lmz在正常死亡
皇帝lmj正在驾崩

from multiprocessing import Process
import time
def task(name):
    print('老太监%s 活着...'%name)
    time.sleep(3)
    print('老太监%s在正常死亡'%name)
if __name__ == '__main__':
    p=Process(target=task,args=('lmz',))
    p.daemon=True
    p.start()
    time.sleep(2)#注意程序睡眠时间,跟上面实例对比,查看输出结果
    print('皇帝lmj正在驾崩')
结果:
老太监lmz 活着...
皇帝lmj正在驾崩

互斥锁

多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。

互斥锁为资源引入一个状态:锁定/非锁定

某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。

互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

threading模块中定义了Lock类,可以方便的处理锁定:

# 创建锁
mutex = threading.Lock()

# 锁定
mutex.acquire()

# 释放
mutex.release()

注意:

  • 如果这个锁之前是没有上锁的,那么acquire不会堵塞
  • 如果在调用acquire对这个锁上锁之前 它已经被 其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止

上锁解锁过程

  当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。

  每次只有一个线程可以获得锁。

  如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。

  线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。

总结

锁的好处:

  • 确保了某段关键代码只能由一个线程从头到尾完整地执行

锁的坏处:

  • 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
  • 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁

进程之间的数据不共享,但是共享同一套文件系统,所以访问同一个文件,或者同一个打印终端,是没有问题的,而共享带来的问题就是竞争,竞争带来的结果就是错乱,如下:

#并发运行,效率高,但竞争同一打印终端,带来了打印错乱
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()

模拟抢票练习:

多个进程共享同一文件,我们可以把文件当数据库,用多个进程模拟多个人执行抢票任务;

#文件db.txt的内容为:{"count":1}
#注意一定要用双引号,不然json无法识别
from multiprocessing import Process
import time,json

def search(name):
    dic=json.load(open('db.txt'))
    time.sleep(1)
    print('\033[43m%s 查到剩余票数%s\033[0m' %(name,dic['count']))

def get(name):
    dic=json.load(open('db.txt'))
    time.sleep(1) #模拟读数据的网络延迟
    if dic['count'] >0:
        dic['count']-=1
        time.sleep(1) #模拟写数据的网络延迟
        json.dump(dic,open('db.txt','w'))
        print('\033[46m%s 购票成功\033[0m' %name)

def task(name):
    search(name)
    get(name)

if __name__ == '__main__':
    for i in range(10): #模拟并发10个客户端抢票
        name='<路人%s>' %i
        p=Process(target=task,args=(name,))
        p.start()

并发运行,效率高,但竞争写同一文件,数据写入错乱,只有一张票,卖成功给了10个人

<路人0> 查到剩余票数1
<路人1> 查到剩余票数1
<路人2> 查到剩余票数1
<路人3> 查到剩余票数1
<路人4> 查到剩余票数1
<路人5> 查到剩余票数1
<路人6> 查到剩余票数1
<路人7> 查到剩余票数1
<路人8> 查到剩余票数1
<路人9> 查到剩余票数1
<路人0> 购票成功
<路人4> 购票成功
<路人1> 购票成功
<路人5> 购票成功
<路人3> 购票成功
<路人7> 购票成功
<路人2> 购票成功
<路人6> 购票成功
<路人8> 购票成功
<路人9> 购票成功

加锁处理:购票行为由并发变成了串行,牺牲了运行效率,但保证了数据安全

#把文件db.txt的内容重置为:{"count":1}
from multiprocessing import Process,Lock
import time,json

def search(name):
    dic=json.load(open('db.txt'))
    time.sleep(1)
    print('\033[43m%s 查到剩余票数%s\033[0m' %(name,dic['count']))

def get(name):
    dic=json.load(open('db.txt'))
    time.sleep(1) #模拟读数据的网络延迟
    if dic['count'] >0:
        dic['count']-=1
        time.sleep(1) #模拟写数据的网络延迟
        json.dump(dic,open('db.txt','w'))
        print('\033[46m%s 购票成功\033[0m' %name)

def task(name,lock):
    search(name)
    with lock: #相当于lock.acquire(),执行完自代码块自动执行lock.release()
        get(name)

if __name__ == '__main__':
    lock=Lock()
    for i in range(10): #模拟并发10个客户端抢票
        name='<路人%s>' %i
        p=Process(target=task,args=(name,lock))
        p.start()

执行结果:
<路人0> 查到剩余票数1
<路人1> 查到剩余票数1
<路人2> 查到剩余票数1
<路人3> 查到剩余票数1
<路人4> 查到剩余票数1
<路人5> 查到剩余票数1
<路人6> 查到剩余票数1
<路人7> 查到剩余票数1
<路人8> 查到剩余票数1
<路人9> 查到剩余票数1
<路人0> 购票成功

互斥锁与join

使用join可以将并发变成串行,互斥锁的原理也是将并发变成穿行,那我们直接使用join就可以了啊,为何还要互斥锁,说到这里我赶紧试了一下

#把文件db.txt的内容重置为:{"count":1}
from multiprocessing import Process,Lock
import time,json

def search(name):
    dic=json.load(open('db.txt'))
    print('\033[43m%s 查到剩余票数%s\033[0m' %(name,dic['count']))

def get(name):
    dic=json.load(open('db.txt'))
    time.sleep(1) #模拟读数据的网络延迟
    if dic['count'] >0:
        dic['count']-=1
        time.sleep(1) #模拟写数据的网络延迟
        json.dump(dic,open('db.txt','w'))
        print('\033[46m%s 购票成功\033[0m' %name)

def task(name,):
    search(name)
    get(name)

if __name__ == '__main__':
    for i in range(10):
        name='<路人%s>' %i
        p=Process(target=task,args=(name,))
        p.start()
        p.join()

执行结果:
<路人0> 查到剩余票数1
<路人0> 购票成功
<路人1> 查到剩余票数0
<路人2> 查到剩余票数0
<路人3> 查到剩余票数0
<路人4> 查到剩余票数0
<路人5> 查到剩余票数0
<路人6> 查到剩余票数0
<路人7> 查到剩余票数0
<路人8> 查到剩余票数0
<路人9> 查到剩余票数0

发现使用join将并发改成穿行,确实能保证数据安全,但问题是连查票操作也变成只能一个一个人去查了,很明显大家查票时应该是并发地去查询而无需考虑数据准确与否,此时join与互斥锁的区别就显而易见了,

join是将一个任务整体串行,而互斥锁的好处则是可以将一个任务中的某一段代码串行,比如只让task函数中的get任务串行

def task(name,):
    search(name) # 并发执行

    lock.acquire()
    get(name) #串行执行
    lock.release()

总结:
加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行地修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。

虽然可以用文件共享数据实现进程间通信,但问题是:

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

2、需要自己加锁处理

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

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

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

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

队列和管道都是将数据存放于内存中,而队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来,因而队列才是进程间通信的最佳选择。

我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性。

死锁

在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。

尽管死锁很少发生,但一旦发生就会造成应用的停止响应。下面看一个死锁的例子

import threading
import time


class MyThread1(threading.Thread):

    def run(self):
        
        # 对mutex1上锁
        mutex1.acquire()
        
        # mutex1上锁后,延迟1秒,等待另外一个线程,把mutex2上锁
        print(self.name + " up")
        time.sleep(1)

        # 此时这里会堵塞,因为mutex2已经被另外的线程抢先上锁了
        mutex2.acquire()
        print(self.name + " down")
        mutex2.release()
        
        # 对mutex1解锁
        mutex1.release()
        

class MyThread2(threading.Thread):

    def run(self):
        
        # 对mutex2上锁
        mutex2.acquire()
        
        # mutex2上锁后,延迟1秒,等待另外一个线程,把mutex1上锁
        print(self.name + " up")
        time.sleep(1)

        # 此时这里会堵塞,因为mutex1已经被另外的线程抢先上锁了
        mutex1.acquire()
        print(self.name + " down")
        mutex1.release()
        
        # 对mutex2解锁
        mutex2.release()


mutex1 = threading.Lock()
mutex2 = threading.Lock()

if __name__ == "__main__":
    
    t1 = MyThread1()
    t1.start()

    t2 = MyThread2()
    t2.start()

运行结果:

Thread-1 up
Thread-2 up

此时已经进入到了死锁状态,可以使用ctrl-c退出

避免死锁

  • 程序设计时要尽量避免(银行家算法)
  • 添加超时时间等
 
posted on 2018-12-27 09:09  漫天飞雪世情难却  阅读(145)  评论(0编辑  收藏  举报