并发

1.背景知识

  • 进程即正在执行的一个过程。进程是对正在运行程序的一个抽象
  • 操作系统作用及原理
作用:
1、将复杂的硬件操作封装成接口,让应用程序使用;
2、管理和调度进程,让多个进程对硬件的使用变得有序

原理:
1、串行:
    一个任务完完整整地运行完毕后,才能运行下一个任务
2、并发
    看起来多个任务是同时运行的即可,单核也可以实现并发(切换+保存状态)
3、并行:
    真正意义上多个任务的同时运行,只有多核才实现并行
4、cpu的功能:
    cpu是用来做计算,cpu是无法执行IO操作的,一旦遇到io操作,应该让cpu去执行别的任务
  • 多道技术
背景:让单核实现并发;
空间上复用:内存中用多个程序运行;
时间上的复用:复用一个cpu的时间片;

注:时间片即CPU分配给各个程序的时间,
遇到io切,占用cpu时间过长也切,核心在于切之前将进程的状态保存下来,这样
才能保证下次切换回来时,能基于上次切走的位置继续运行

2.多进程

2.1 进程定义及创建

  • 正在进行一个过程或者一个任务,而负责执行任务的是cpu;(单核和多道技术可以实现多个进程的并发执行)
  • 与程序区别:进程是一堆代码,进程是程序运行的过程
  • 进程创建
新进程的创建时由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的

关于创建的子进程,UNIX和windows:
(1)相同的是:进程创建后,父进程和子进程有各自不同的地址空间(多道技术要求物理层面实现进程之
间内存的隔离),任何一个进程的在其地址空间中的修改都不会影响到另外一个进程。
(2)不同的是:在UNIX中,子进程的初始地址空间是父进程的一个副本,提示:子进程和父进程是可以有只
读的共享内存区的。但是对于windows系统来说,从一开始父进程与子进程的地址空间就是不同的。

2.2 进程的层次结构及运行状态

  • 层次结构
无论UNIX还是windows,进程只有一个父进程,不同的是:
(1) 在UNIX中所有的进程,都是以init进程为根,组成树形结构。父子进程共同组成一个进程组,
当从键盘发出一个信号时,该信号被送给当前与键盘相关的进程组中的所有成员。
(2)在windows中,没有进程层次的概念,所有的进程都是地位相同的,唯一类似于进程层次的暗示,是在创建进程时,
父进程得到一个特别的令牌(称为句柄),该句柄可以用来控制子进程,但是父进程有权把该句柄传给其他子进程,这样就没有层次了
  • 运行状态
运行态:一个程序正在被cpu执行;
阻塞态:遇到IO操作;
就绪态:IO执行完毕,等待CPU

2.3 同步异步and阻塞非阻塞

  • 同步异步
同步:发起同步调用后,就在原地等着任务结束,不考虑任务是在计算还是在io阻塞;
异步:异步调用后,并不会等待任务结束才返回,相反,会立即获取一个临时结果;
注:发出异步功能调用,并不会立刻得到结果,当该异步功能完成后,通过状态、通知或回调来通知调用者;通知和回调函数的方式比状态更有效率。
  • 阻塞和非阻塞
阻塞:调用结果返回之前,当前线程会被挂起(如遇到了IO操作)
非阻塞:不能立刻得到结果之前也会立刻返回,同时该函数不会阻塞当前线程

同步与异步,重点在于消息通知的方式;
阻塞与非阻塞,重点在于等消息时候的行为。

2.4 进程开启

  • multiprocessing模块
开启子进程
包含了Process、Queue、Pipe、Lock等组件
进程间内存空间隔离,没有共享状态
  • 开启进程的方式
方式一:
from  multiprocessing import Process
import time
def task(x):   #创建的子进程
    print('%s is running' %x)
    time.sleep(3)
    print('%s is done' %x)
if __name__ == '__main__':  #当前文件是执行文件才会执行,如果是导入模块自己不会执行
    p= Process(target= task,args=('子进程',))#括号内是一个参数要加逗号,调用类产生对象
    #p = Process(target=task,kwargs= {'x':'子进程'})
    p.start() #只是在向操作系统发送一个开启子进程的信号,开启子进程需要申请内存空间,把父进程的数据拷贝一份
    print('主')
 
方式二:   
from  multiprocessing import Process
import time
class Myprocess(Process):  #创建自己的类,继承Process功能
    def __init__(self,x):  #初始化进程属性,进程名
        super().__init__()  #调用父类属性
        self.name=x
    def run(self):   #子进程要执行的任务在run方法内
        print('%s is running' % self.name)  #self.name 内置方法,进程的名字
        time.sleep(3)
        print('%s is done' %self.name)
if __name__ == '__main__':
    p= Myprocess('子进程1')
    p.start()  #调用函数run
    print('主')
  • 进程对象的方法及属性
(1)join  当前代码执行完毕后执行下面代码
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__':
    p=Process(target=task,args=('子进程1',))
    p.start()
    p.join() # 让父进程在原地等待,等到子进程运行完毕后,才执行下一行代码
    print('主')

(2)pid 及ppid    查看当前进程ID 父进程ID
from multiprocessing import Process
import time
import os
def task():
    print('自己的id:%s 父进程的id:%s'%(os.getpid(),os.getppid()))
    time.sleep(2)
if __name__ == '__main__':
    p= Process(target=task)
    p.start()
    print('主',os.getpid(),os.getppid())#父进程及创建父进程的进程ID

(3)查看进程名  终止进程 判断子进程是否存活
from  multiprocessing import Process,current_process
import time
def task():
    print('子进程[%s]运行。。'%current_process().name)#在子进程内查看子进程名
    time.sleep(200)
if __name__ == '__main__':
    p1= Process(target=task,name='子进程1')
    p1.start()
    p1.terminate()  #向操作系统发起终止进程的信号 中间加个join或者tima.sleep 子进程状态就为FALSE
    #p1.join()
    print(p1.is_alive())#子进程是否活着
    #print(p1.name)
    print('主')

2.5 僵尸进程、孤儿进程及守护进程

  • 僵尸进程(有害)
一个进程创建子进程,子进程在退出后,父进程没有获取子进程的状态信息,子进程的进程描述符仍然保存在系统中。

危害性:没有可用的进程号而导致系统不能产生新的进程

解决方案:1、结束父进程,让僵尸进程变成孤儿进程,让系统init进程回收
2、对开启的子进程应该记得使用join,join会回收僵尸进程(join方法中调用了wait,告诉系统释放僵尸进程)
3、signal模块
  • 孤儿进程(无害)
父进程先退出,子进程还在运行,这些子进程就变成孤儿进程,被系统的init进程回收
  • 守护进程
定义:守护进程在主进程运行结束后终止
用途:父进程并发执行任务需要开启子进程,当该子进程内的代码在父进程代码运行完毕后就没有存在的意义了,就应该
将该子进程设置为守护进程,会在父进程代码结束后死掉
创建守护进程:
from multiprocessing 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('主')

2.6 进程间通信(队列与管道)

  • 互斥锁
可以将要执行任务的部分代码(只涉及到修改共享数据的代码)变成串行
#join:是要执行任务的所有代码整体串行
  • 队列
进程间通信(IPC) 管道  队列
#方式一:pipe  方式二:queue: pipe+锁
from multiprocessing import Queue
q=Queue(3)   #队列对象  先进先出 队列中放三条任意类型信息
#注:队列占用的是内存空间;不应该存放大数据,应该只存放数据量较小的信息
q.put('first')  #放队列信息
q.put('f')
q.put('d')
#放的消息超过队列设置的限制,队列堵塞,必须要取走
print(q.get()) #取队列信息
print(q.get())

from multiprocessing import Queue
q= Queue(3)
q.put('first',block= True ,timeout= 3)
q.put('f',block= True ,timeout= 3)
q.put('d',block= True ,timeout= 3)
q.put('mm',block= True ,timeout= 3)#如果超过三秒还没有人取走队列信息就会报错
#q.put('mm',block= False ) #队列满了就抛异常,与超时没关系,不会阻塞
q.get(block= True ,timeout= 3)  #block= True 时timeout才有意义
#如果队列中信息取空,再取就会报错
  • 管道
  • 生产者与消费者模型
用途:
实现了生产者与消费者的解耦和,生产者可以不停地生产,消费者也可以不停地消费
从而平衡了生产者的生产能力与消费者消费能力,提升了程序整体运行的效率
代码:
from multiprocessing import JoinableQueue,Process
import time
import os
import random
def producer(name,food,q):
    for i in range(3):
        res='%s%s' %(food,i)
        time.sleep(random.randint(1,3))
        # 往队列里丢
        q.put(res)
        print('33[45m%s 生产了 %s33[0m' %(name,res))
    # q.put(None)
def consumer(name,q):
    while True:
        #从队列里取走
        res=q.get()
        if res is None:break
        time.sleep(random.randint(1,3))
        print('33[46m%s 吃了 %s33[0m' %(name,res))
        q.task_done()
if __name__ == '__main__':
    q=JoinableQueue()
    # 生产者们
    p1=Process(target=producer,args=('egon','包子',q,))
    p2=Process(target=producer,args=('杨军','泔水',q,))
    p3=Process(target=producer,args=('猴老师','翔',q,))
    # 消费者们
    c1=Process(target=consumer,args=('Alex',q,))
    c2=Process(target=consumer,args=('wupeiqidsb',q,))
    c1.daemon=True  # 守护进程
    c2.daemon=True
    
    p1.start()
    p2.start()
    p3.start()
    c1.start()
    c2.start()

    p1.join()
    p2.join()
    p3.join()

    q.join() #等待队列被取干净
    # q.join() 结束意味着
    # 主进程的代码运行完毕--->(生产者运行完毕)+队列中的数据也被取干净了->消费者没有存在的意义

    # print('主')
  • 信号量
允许指定数量的线程更改同一数据
from multiprocessing import Process,Semaphore
  • 进程池:
作用:操控进程开启的数目
当并发的任务数远远超过了计算机的承受能力时,即无法一次性开启过多的进程数或线程数时
就应该用池的概念将开启的进程数或线程数限制在计算机可承受的范围内

2. 多线程

2.1 定义

  • 进程将资源集中到一起,是资源单位,线程是具体的执行单位;一个进程中的多个控制线程,多个线程共享该进程的地址空间

2.2 创建对比及用途

创建:
进程:需要申请内存空间,内存空间中至少有一条控制线程
线程:在已有的内存空间中创建
注:进程间是竞争关系,线程间是协作关系

用途:
1、多个任务共用一个内存空间;
2、想对于进程,更加轻量级,容易创建撤销;
3、对于计算和I/O处理,多个线程可以重叠运行,加快程序执行的速度;
4、在CPU系统中最大限度的利用多核优势

2.3 开启线程的两种方式

方式一:
def task(name):
    print('%s is running' %name)
    time.sleep(3)
    print('%s is done' %name)
if __name__ == '__main__':
    t=Thread(target=task,args=('子线程',))
    t.start()
    print('主')
    
方式二:
class Mythread(Thread):
    def run(self):
        print('%s is running' %self.name)
        time.sleep(3)
        print('%s is done' %self.name)

if __name__ == '__main__':
    t=Mythread()
    t.start()
    print('主')

2.4 守护线程

主线程所在的进程内,所有的非守护线程全都运行完毕后,主线程才运行完毕

2.5 锁相关

  • 全局解释器锁(GIL)
1、保证解释器同一时间只能执行一个任务的代码
2、GIL保护的是解释器级的数据,保护用户自己的数据则需要自己加锁处理
3、GIL与多线程,同一时刻同一进程中只有一个线程被执行
  • 性能测试
I/O密集型,创建效率高,用多线程; 如:socket,爬虫,web
计算密集型 发挥多核优势,用多进程; 如:金融分析
  • 同步锁
1、线程拿的是GIL锁(执行权限),拿到执行权限后才能拿到互斥锁Lock,其他线程也可以抢到GIL,
但如果发现Lock仍然没有被释放则阻塞,即便是拿到执行权限GIL也要立刻交出来
2、join 是整体串行,加锁是部分串行,效率更高;
  • 死锁现象
两个或者两个以上的进程或者线程应争夺资源而造成相互等待的现象
解决方案:
递归锁,RLock代替Lock
  • 信号量
管理一个内置计数器,调用时减一,释放时加一,计数器小于0时,线程阻塞;
保证多个线程不会互相冲突
  • 定时器
    from threading import Timer
  • 线程queue
1、队列:先进先出queue.Queue
q=queue.Queue(3)
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())

2、堆栈:先进后出 queue.LifoQueue

3、优先级队列,优先级队列:优先级高的先出来,数字越小,优先级越高
q=queue.PriorityQueue()
q.put((3,'data1'))
q.put((-10,'data2'))
q.put((11,'data3'))
print(q.get())
print(q.get())
print(q.get())

3. 协程

3.1 定义

单线程下的并发,用户通过自己设置的程序进行控制调度(协程遇到IO操作自动切换到其它协程,通过gevent模块)

优点:更加轻量级,程序级别的切换
单线程内实现并发,
缺点:无法利用多核优势
协程出现阻塞,将会阻塞整个线程
  • 创建
from  gevent import monkey,spawn;monkey.patch_all()
import gevent
import time

def eat(name):
    print('%s eat 1'%name)
    time.sleep(3)
    print('%s eat 2'%name)

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

start = time.time()
gl= spawn(eat,'egon')  #创建协程对象
g2= spawn(play,'zmy')
gl.join()
g2.join()  #等协程运行完毕
print(time.time()-start)

4. I/O模型

阻塞IO  非阻塞IO  多路复用IO 异步IO
信号驱动IO(不常用)
  • paramiko是一个用于做远程控制的模块,使用该模块可以对远程服务器进行命令或文件操作
  • 进程、线程与协程区别
进程是资源单位,线程是执行单位
进程间内存空间彼此隔离,线程共用一个内存空间
进程创建所需的开销较大
计算密集型用多进程,IO密集型用多线程
协程在程序级别切换,gevent
原文地址:https://www.cnblogs.com/quqinchao/p/11218164.html