并发编程

并发编程

进程

进程的三种状态: 运行,就绪,阻塞

理论基础

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

二 多道技术:
1.产生背景:针对单核,实现并发
ps:
现在的主机一般是多核,那么每个核都会利用多道技术
有4个cpu,运行于cpu1的某个程序遇到io阻塞,会等到io结束再重新调度,会被调度到4个
cpu中的任意一个,具体由操作系统调度算法决定。

2.空间上的复用:如内存中同时有多道程序
3.时间上的复用:复用一个cpu的时间片
强调:遇到io切,占用cpu时间过长也切,核心在于切之前将进程的状态保存下来,这样
才能保证下次切换回来时,能基于上次切走的位置继续运行

进程:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

一些概念:

**串行: ** 一个进程执行完才去执行下一个进程

**并行: ** 多个进程在同一时间被执行

**并发: ** 看起来像是多个进程在被同时执行(核心就是 切换加保存状态)

进程间的资源空间是相互隔离的在开启子进程时必须放在if _ _ name _ _ == '_ _ main _ _ ':下,否则在子进程加载父进程的资源时会出现循环导入现象

开启子进程的两种方式

方式一:Process实例化

from multiprocessing import Process
import time


def task(name, n):
    print('%s%s号 is runing' % (name, n))
    time.sleep(n)
    print('%s%s号 is stop' % (name, n))
if __name__ == '__main__':
    # p1 = Process(target=task,args=('Tom',1))
    # p2 = Process(target=task,args=('Tom',2))
    # p1.start()
    # p2.start()
    # p1.join()
    # p2.join()
    p_lis = []
    for i in range(4):
        p = Process(target=task,args=('Tom',i))
        p_lis.append(p)
        p.start()
    for i in p_lis:
        p.join()
    print('Father')

方式二:自定义Process类

class MyProcess(Process):
    def __init__(self, name, n):
        super().__init__()
        self.name = name
        self.n = n
    def run(self):
        print('%s%s号 is runing' % (self.name, self.n))
        time.sleep(self.n)
        print('%s%s号 is stop' % (self.name, self.n))

if __name__ == '__main__':
    p_lis = []
    for i in range(4):
        p = MyProcess('Tom', i)
        p_lis.append(p)
        p.start()
    print(p_lis)
    for p in p_lis:
        p.join()
    print('Father')

join方法

如上面两种开启进程的方法 所示,主进程会在执行了join方法子进程执行完后才执行

即:让主进程在原地等待,等待子进程运行完毕

进程对象的其他属性

进程pid:每一个进程在操作系统内都有一个唯一的id号,称之为pid

current_process

from multiprocessing import Process,current_process
import time

def task():
    print('%s is running' %current_process().pid)
    time.sleep(30)
    print('%s is done' %current_process().pid)

if __name__ == '__main__':
    p=Process(target=task)
    p.start()
    print('主',current_process().pid)

os 模块也提供了获取进程id的接口

os.getpid() :获取当前进程id

os.getppid() : 获取父进程id

from multiprocessing import Process,current_process
import time,os

def task():
    print('%s is running 爹是:%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()))

is_alive() :判断当该进程是否正在运行

terminate() : 阻断该进程的执行

子进程在创建时会有默认name属性 可以对其进行更改 如下

p=Process(target=task,name='子进程1')

p.name 获取进程名称

僵尸进程与孤儿进程

守护进程

通过令子进程的daemon 属性为True 来使其成主进程的守护进程

守护进程: 在主进程结束时,守护进程同时结束

互斥锁

在进程中,join方法可以让进程间变为的串行, 互斥锁可以让不同进程的部分代码的执行变得串行

也就是说join只能将进程的任务整体变成串行,而互斥锁可以让其内部某些代码串行,而进程自身仍是并行

下模拟购票例 很好的说明了 部分代码串行的特点

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

def search(name):
    with open('db.json','rt',encoding='utf-8') as f:
        dic=json.load(f)
    time.sleep(1)
    print('%s 查看到余票为 %s' %(name,dic['count']))

def get(name):
    with open('db.json','rt',encoding='utf-8') as f:
        dic=json.load(f)
    if dic['count'] > 0:
        dic['count'] -= 1
        time.sleep(random.randint(1,3))
        with open('db.json','wt',encoding='utf-8') as f:
            json.dump(dic,f)
            print('%s 购票成功' %name)
    else:
        print('%s 查看到没有票了' %name)

def task(name,mutex):
    search(name) #并发
    mutex.acquire()
    get(name) #串行
    mutex.release()

    # with mutex:
    #     get(name)

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

进程间通信--队列(multiprocess.Queue)

进程间通信 : IPC(Inter-Process Communication)

队列

Queue([maxsize])
创建共享的进程队列。
参数 :maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。
底层队列使用管道和锁定实现。

Queue([maxsize])
创建共享的进程队列。maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。底层队列使用管道和锁定实现。另外,还需要运行支持线程以便队列中的数据传输到底层管道中。
Queue的实例q具有以下方法:

q.get( [ block [ ,timeout ] ] )
返回q中的一个项目。如果q为空,此方法将阻塞,直到队列中有项目可用为止。block用于控制阻塞行为,默认为True. 如果设置为False,将引发Queue.Empty异常(定义在Queue模块中)。timeout是可选超时时间,用在阻塞模式中。如果在制定的时间间隔内没有项目变为可用,将引发Queue.Empty异常。

q.get_nowait( )
同q.get(False)方法。

q.put(item [, block [,timeout ] ] )
将item放入队列。如果队列已满,此方法将阻塞至有空间可用为止。block控制阻塞行为,默认为True。如果设置为False,将引发Queue.Empty异常(定义在Queue库模块中)。timeout指定在阻塞模式中等待可用空间的时间长短。超时后将引发Queue.Full异常。

q.qsize()
返回队列中目前项目的正确数量。此函数的结果并不可靠,因为在返回结果和在稍后程序中使用结果之间,队列中可能添加或删除了项目。在某些系统上,此方法可能引发NotImplementedError异常。

q.empty()
如果调用此方法时 q为空,返回True。如果其他进程或线程正在往队列中添加项目,结果是不可靠的。也就是说,在返回和使用结果之间,队列中可能已经加入新的项目。

q.full()
如果q已满,返回为True. 由于线程的存在,结果也可能是不可靠的(参考q.empty()方法)

q.close()
关闭队列,防止队列中加入更多数据。调用此方法时,后台线程将继续写入那些已入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将自动调用此方法。关闭队列不会在队列使用者中生成任何类型的数据结束信号或异常。例如,如果某个使用者正被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误。

q.cancel_join_thread()
不会再进程退出时自动连接后台线程。这可以防止join_thread()方法阻塞。

q.join_thread()
连接队列的后台线程。此方法用于在调用q.close()方法后,等待所有队列项被消耗。默认情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread()方法可以禁止这种行为。

生产者消费者模型

from multiprocessing import Process, JoinableQueue
import time, random


def product(food, q):
    for i in range(1, 3):
        res = '%s%s' % (food, i)
        time.sleep(random.randint(1, 3))
        q.put(res)
        print('%s%s被生产' % (food, i))


def consumer(name, q):
    while True:
        food = q.get()
        time.sleep(random.randrange(0, 3))
        print('%s被[%s]吃了' % (food, name))
        q.task_done()


if __name__ == '__main__':
    q = JoinableQueue()
    # 生产者进程
    p = Process(target=product, args=('鸡腿', q))
    p1 = Process(target=product, args=('烧麦', q))
    p2 = Process(target=product, args=('鸭蛋', q))
    p3 = Process(target=product, args=('金典', q))
    # 消费者进程
    p4 = Process(target=consumer, args=('张三', q))
    p5 = Process(target=consumer, args=('李四', q))

    # 向操作系统发起开启进程请求
    p.start()
    p1.start()
    p2.start()
    p3.start()
    # 为了保证 父进程结束时 消费者模型同时结束
    # 将消费者进程做成守护进程
    p4.daemon = True
    p5.daemon = True
    p4.start()
    p5.start()

    p1.join()
    p2.join()
    p3.join()
    # 为了保证在队列join时不再有数据重新加入到队列
    # 生产者队列join 时必须保证 生产者进程全部终止
    q.join()
    print('Foo')  # 队列的join操作使 父进程一定在队列值被取完才结束

线程

线程: 进程其实一个资源单位,而进程内的线程才是cpu上的执行单位
线程其实指的就是代码的执行过程

线程vs进程
1. 同一进程下的多个线程共享该进程内的资源
2. 创建线程的开销要远远小于进程

开启线程的两种方式

方式一Thread实例化

from threading import Thread
import time

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

if __name__ == '__main__':
    t=Thread(target=task,args=('线程1',))
    t.start()
    print('Foo')

方式二自定义Thread类

from threading import Thread
import time

class Mythread(Thread):
    def run(self):
        print('%s is running' %self.name)
        time.sleep(2)
        print('%s is done' %self.name)

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

守护线程

与守护进程方式类似

线程互斥锁

GIL全局解释器锁

GIL本质

就是一把互斥锁,相当于执行权限,每个进程内都会存在一把GIL,同一进程内的多个线程
必须抢到GIL之后才能使用Cpython解释器来执行自己的代码,即同一进程下的多个线程无法实现并行
但是可以实现并发

在Cpython解释器下,如果想实现并行可以开启多个进程

GIL存在的意义
因为Cpython解释器的垃圾回收机制不是线程安全的

无论是多线程还是多进程都能实现并发

在多核的前提下

计算密集型时 使用多进程

IO密集型 使用多线程

CPU密集型

  1. 处理数据(计算)

IO密集型

等待数据

  1. 查询数据库

  2. 请求网络资源

  3. 读写文件

# 依据程序花费的时间是在CPU上还是在等待数据上判断这个程序是CPU密集型还是IO密集型

IO操作主要分两类: 网络IO 和 磁盘IO

死锁现象与递归锁

当线程一需要acquire的锁已经被线程二acquire到,而与此同时线程二需要acquire的锁恰巧被线程一acquire到,这样两个线程彼此拿到对方需要acquire的锁就会出现死锁现象

结局方案可以使用递归锁

递归锁

from threading import Thread, Lock, RLock
import time

# mutexA=Lock()  # 会出现死锁现象
# mutexB=Lock()
mutexB = mutexA = RLock()


class Mythead(Thread):
    def run(self):
        self.f1()
        self.f2()

    def f1(self):
        mutexA.acquire()
        print('%s 抢到A锁' % self.name)
        mutexB.acquire()
        print('%s 抢到B锁' % self.name)
        mutexB.release()
        mutexA.release()

    def f2(self):
        mutexB.acquire()
        print('%s 抢到了B锁' % self.name)
        time.sleep(2)
        mutexA.acquire()
        print('%s 抢到了A锁' % self.name)
        mutexA.release()
        mutexB.release()


if __name__ == '__main__':
    for i in range(100):
        t = Mythead()
        t.start()

信号量

其实本质上是锁,Lock是单锁,信号量是指定多把锁,也就是说通过信号量指定多个数线程可以访问相同资源,一般情况下读操作可以有多个,但写操作同时只有一个

from threading import Thread,Semaphore
import time,random
sm=Semaphore(5)

def task(name):
    sm.acquire()
    print('%s 正在上厕所' %name)
    time.sleep(random.randint(1,3))
    sm.release()

if __name__ == '__main__':
    for i in range(20):
       t=Thread(target=task,args=('路人%s' %i,))
       t.start()

Evevt事件

wait方法:可以让线程在此处阻塞

set方法:可以释放wait处的阻塞

这样就可以实现通过一个线程控制其他线程的开启和阻塞 如下例

from threading import Thread, Event
import time

event = Event()


def light():
    print('红灯正亮着')
    time.sleep(3)
    event.set()  # 绿灯亮


def car(name):
    print('车%s正在等绿灯' % name)
    event.wait()  # 等灯绿
    print('车%s通行' % name)


if __name__ == '__main__':
    # 红绿灯
    t1 = Thread(target=light)
    t1.start()
    # 车
    for i in range(10):
        t = Thread(target=car, args=(i,))
        t.start()

线程queue的三种形式

import queue

#先进先出
queue.Queue()
#后进先出->堆栈
queue.LifoQueue()
#优先级,优先级用数字表示,数字越小优先级越高
queue.PriorityQueue()
import queue

#先进先出
queue.Queue()
q=queue.Queue(3)
q.put(1)
q.put(2)
q.put(3)
print(q.get()) 
print(q.get())
print(q.get())

#后进先出->堆栈
queue.LifoQueue()
q=queue.LifoQueue(3)
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())


# queue.PriorityQueue() #优先级

#优先级,优先级用数字表示,数字越小优先级越高
q=queue.PriorityQueue(3)
q.put((10,'a'))
q.put((-1,'b'))
q.put((100,'c'))
print(q.get())
print(q.get())
print(q.get())

进程池与线程池

进程池

from concurrent.futures import ProcessPoolExecutor
import os,time,random


def task(i):
    print('%s Foo:%s  is runing'%(os.getpid(),os.getppid()))
    time.sleep(random.randint(1,3))
    print('%s is done'%(os.getpid()))
    return i

def parse(future):
    print('%s'%future.result())

if __name__ == '__main__':
    p = ProcessPoolExecutor(max_workers=4)
    for i in range(20):
        # 同步提交任务
        # res = p.submit(task,).result()
        # print(res)
        # 异步提交  通常异步调用配合回调函数使用 异步回调
        future = p.submit(task,i)
        future.add_done_callback(parse)

线程池

不仅仅是数量控制,可以获取线程状态、任务状态、线程返回值等信息

线程池模块 ThreadPollExecutor

线程池使用过程

  1. 实例化线程池

  2. 提交任务,会有个返回对象,submit是不会堵塞,立即返回

  3. 让主线程等待线程执行完成

  4. 关闭线程池

获取状态信息  线程对象

  1. 判断是否执行完        .done()

  2. 获取任务执行结果,堵塞    .result()

  3. 取消任务

posted @ 2019-05-10 10:51  会飞的空心菜  阅读(148)  评论(0)    收藏  举报