并发编程整合版

简介

包括以下知识

进程与程序的基本知识

  • 程序是什么?

    程序就是一堆文件.
    
  • 进程是什么?

    进程就是一个正在执行的文件/程序,抽象的概念.
    
  • 进程被谁执行?

    cpu最终运行你的程序
    操作系统调度作用将你的磁盘上的程序加载到内存,然后交由CPU去处理.一个cpu正在运行的一个程序,就叫开启了一个进程.
    

操作系统的定义与作用

  • 操作系统的定义

    操作系统是存在于硬件与软件之间,管理,协调,控制软件与硬件的交互.
    
  • 操作系统的作用:

    1:将一些丑陋复杂的硬件操作封装成美丽的接口,便于使用.
    2: 多个进程抢占一个资源时,合理的调度分配多个进程与cpu的关系,让其有序化,
    

操作系统的发展史

操作系统(计算机)的发展史(多道技术)
第一代电子计算机(1940~1955)
二战时期,推出的电子计算机 手工操作.预定.类似于插线板的程序,计入机房,一个人独资享有计算机2个小时,各种硬件的操作插线与你的程序结合.
特点: 没有操作系统的概念,所有的硬件连接都是自己完成.
优点: 一个人独享.
缺点:一个人享用浪费资源.
所有的程序都是串行处理.

第二代计算机:磁带存储,批处理系统.(1955~1965)
优点: 节省了程序员连接个硬件的操作的时间.
缺点:不能独享计算机,不能在程序运行时修复bug.
cpu还是串行处理.

第三代计算机,集成电路,多道程序系统.
第三代计算机解决一个问题: 人工中途参与磁盘传递工作,以及输入输出设备不同机的问题.
第三代计算机解决第二个问题: 多道技术.

多道技术

多道技术要求物理层面实现进程之间内存的隔离
多道技术解决了什么:
    时间上的复用 将串行变为并发
    空间上的复用.一个内存允许加载多个进程.

阻塞
	IO阻塞,recv, accept, read input,write, sleep等等,都是阻塞.
ps:如果三个进程全部都没有IO阻塞,多道技术就会影响最终的效率.

进程的理论

串行 并发 并行 阻塞 非阻塞

串行: 所有的进程由cpu一个一个的解决.
并发:单个cpu,同时执行多个进程(来回切换的),看起来像是同时运行.
并行:多个cpu,真正的同时运行多个进程.
阻塞:遇到IO才叫阻塞.
非阻塞: 没有IO.
进程3个状态 运行 就绪 阻塞

创建进程过程

进程的创建.
想开启多个进程,必须是一个主进程,开启多个子进程.
unix: fork创建子进程.
windows:操作系统调用CreateProcess处理进程的创建.
linux, windows: 由主进程开启子进程:
相同点: 原则:主进程开启子进程两个进程都有相互隔离的独立的空间,互不影响.
不同点:
  linux: 子进程空间的初始数据完全是从主(父)进程copy一份.
  windows: 子进程空间的初始数据完全是从主(父)进程copy一份,但是有所不同.

利用python开启一个进程

第一种 利用python中模块Process类创建 通过调用multiprocessing模块下面的Process类方法

from multiprocessing import Process
def task(name):
    print('这是我自己定义的函数')
    print(name)
if __name__ == '__main__':
    p=Process(target=task,args=(('mssq',)))#target对应的是函数名 args必须对应的是元祖
    p.start()
    print('我是主进程')
# 遇到的问题
# 为什么主进程永远比子进程快?
# 因为子进程创建需要时间
# p是什么 p.start()又是什么
# p是你实列化出来的一个对象 pstart()是发出这个命令开启子进程 子进程的创建必须依赖与主进程
# 他会完全copy一份数据到子进程空间

第2种 自己定义一个类 继承与Process 重写run 方法 run方法里面是自己写的逻辑代码 方式二借助process类,自定义一个类(继承Process),从而造一个对象 并重新run方法

from multiprocessing import Process
class MYprocess(Process):
    def __init__(self,name):
        super().__init__()
        self.name=name
    def run(self):
        print(666)
        print(self.name)
if __name__ == '__main__':
    p=MYprocess('sq')
    p.start()
    print('主进程')
# 为什么为定义name 打印的是 MYprocess-1 ?
# 因为引用了 Process 类中的name
# 如果想要自己传参 为啥自己定义def __init__(self,name):还会报错?
# 因为Process类中的有些东西我们要用到所以不可以覆盖他 我们利用super().__init__()
# 既运行父类__init__还运行 自己的init

小陷阱

会copy主空间资源
from multiprocessing import Process
x = 1
def task():
    global x
    x=66
    print(x)
print(f'----------------{x}')
if __name__ == '__main__':
    p=Process(target=task,args=(()))
    p.start()
    p.join()
    print(f'{x}--------------')
    
    
打印
----------------1
----------------1
66
1--------------

验证进程之间的空间隔离

# 我们定义一个全局变量(不可变的 str 可变的list),让其子进程进行修改
# 观察其现象,发现 进程之间的空间隔离 子进程是完全复制一份到子空间 也不同与深ccpy 因为id不同
from multiprocessing import Process
name='骚强'
l1=['骚强']
def task():
    global name
    global l1
    name='你会变'
    l1.append('believe')
    print(f'子进程name  {name} id{id(name)}')
    print(f'子进程 l1  {l1} id {id(l1)}')
if __name__ == '__main__':
    p=Process(target=task,args=(()))#target对应的是函数名 args必须对应的是元祖
    p.start()
    p.join()
    print(f'主进程name  {name} id{id(name)}')
    print(f'主进程 l1  {l1} id {id(l1)}')

获取进程其他属性(进程pid)

p = Process(target=task,args=('骚强',),name='alex')  # name是进程名
# p.terminate()  # 杀死子进程  ***
# print(p.is_alive())   # ***
# print(p.name)#进程名 不可以在任务打印 只可以主线程
# print(f'子进程:{os.getpid()}')
# print(f'主进程:{os.getppid()}')
终端获取pid
tasklist
tasklist| findstr 进程名

实列

from multiprocessing import Process
import time
import os
def task(name):
    print(44)
    print(f'------------------{name}')
    print(f'子进程:{os.getpid()}')
    print(f'主进程:{os.getppid()}')

if __name__ == '__main__':
    p=Process(target=task,args=(('传参1',)),name='线程名')
    p1=Process(target=task,args=(('传参2',)))
    p.start()
    p1.start()
    print(p.name)
    print(p1.name)

进程对象join方法

join让主进程等待我这个进程结束之后,在执行主进程. 相当与阻塞
from multiprocessing import Process
import time
def task1():
    print(f'我是task1 我睡了1秒')
    time.sleep(1)
def task2():
    print(f'我是task2 我睡了2秒')
    time.sleep(2)
def task3():
    print(f'我是task3 我睡了3秒')
    time.sleep(3)
if __name__ == '__main__':
    start_time=time.time()
    p1=Process(target=task1,args=(()))
    p2=Process(target=task2,args=(()))
    p3=Process(target=task3,args=(()))
    p1.start()
    p1.join()
    p2.start()
    p2.join()
    p3.start()
    print(f'主进程运行完了 总时间{time.time()-start_time}')

僵尸 孤儿 守护 进程(liunx中)

# linux(mac)环境下才强调的两个概念,windows下没有.
基于unix环境(linux,macOS)
为什么会产生僵尸进程??
主进程需要等待子进程结束之后,主进程才结束
主进程时刻监测子进程的运行状态,当子进程结束之后,一段时间之内,将子进程进行回收.
为什么主进程不在子进程结束后马上对其回收呢?
主进程与子进程是异步关系.主进程无法马上捕获子进程什么时候结束.
如果子进程结束之后马上再内存中释放资源,主进程就没有办法监测子进程的状态了.
unix针对于上面的问题,提供了一个机制.
所有的子进程结束之后,立马会释放掉文件的操作链接,内存的大部分数据,但是会保留一些内容: 进程号,结束时间,运行状态,等待主进程监测,回收.

孤儿进程

父进程由于某种原因结束了,但是你的子进程还在运行中,这样你的这些子进程就成了孤儿进程.你的父进程如果结束了,你的所有的孤儿进程就会被init进程的回收,init就变成了你的父进程,对你进行回收.

僵尸进程

僵尸进程: 所有的子进程结束之后,在被主进程回收之前,都会进入僵尸进程状态  #僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被init接管,子进程退出后init会回收其占用的相关资源
僵尸进程有无危害???
如果父进程不对僵尸进程进行回收(wait/waitpid),产生大量的僵尸进程,这样就会占用内存,占用进程pid号.
僵尸进程如何解决???
父进程产生了大量子进程,但是不回收,这样就会形成大量的僵尸进程,解决方式就是直接杀死父进程,将所有的僵尸进程变成孤儿进程进程,由init进行回收.

守护进程

守护进程不可以有子进程 否则抛异常
被守护进程运行完成 同时守护进程 同时完结 不管守护进程还有没有执行完
守护进程先完结就没有守护的意义
#  p.daemon = True  # 将p子进程设置成守护进程,守护主进程,只要主进程结束,子进程无论执行与否,都马上结束.一定要在发起前
# 必须在发起前守护 多用于窗口化
下面验证了守护进程运行完就完了 对被守护的没影响


实列

from multiprocessing import Process
def task():
    print('a')
if __name__ == '__main__':
    p=Process(target=task,args=(()))
    p.daemon=True
    p.start()
    p.join()
    print('11111111111111111111111')
    print(p.is_alive())
    print('主')

互斥锁

(保证数据安全, 自己加锁容易出现死锁.)

为了保证数据安全而出现的一把锁,当多个进程抢占一个资源时 将并发变成了串行  谁先抢到锁先执行 如果这个程序遇到了io阻塞 操作系统会使cpu切换到其他进程,当切换到其他进程时发现他没有这吧锁,就会跳过他从而保证了数据安全     
注意锁必须要同一把   多用于 抢购 依次执行

互斥锁与join区别共同点?

# 互斥锁与join区别共同点? (面试题)
# 共同点: 都是完成了进程之间的串行.
# 区别: join认为控制的进程串行,互斥锁是随机的抢占资源.保证了公平性

互斥锁的实列

# 在生活中我们会遇到一些事情,必须在保证公平的情况下又可以保护数据安全
# 并发变为串行
# 以下是打印机实列 我们希望谁先抢到,谁先运行 且是依次执行
from multiprocessing import Process
from multiprocessing import Lock
import os
import time
import random
def task(lock):#模拟打印
    lock.acquire()#加锁
    print(f'{os.getpid()}打印开始了')
    time.sleep(random.randint(1,3))
    print(f'{os.getpid()}打印结束了')
    lock.release()#解锁
if __name__ == '__main__':
    lock=Lock()
    for i in range(4):
        p=Process(target=task,args=((lock,)))
        p.start()
# 遇到的问题
# 为什么加锁要同一把锁 不是同一把会怎么样?
# 锁就是一个凭证,当只有一把锁时 他保证了运行时的唯一行,如果大家都有了也就不可以约束了

进程之间的通信

  1. 基于文件+ 锁的形式: 效率低,麻烦.
  2. 基于队列: 推荐使用形式.
  3. 基于管道: 管道自己加锁, 底层可以会出现数据丢失损坏.

基于文件通信.

基于文件的抢票模型

# 虽然进程间内存级别不可以通讯 但是基于文件是可以通讯 但我们不常用
# 查表并发 买票串行
from multiprocessing import Process
from multiprocessing import Lock
import json
import time
import random
import os
def search():
    time.sleep(random.randint(1,3))
    with open('ticket.json',encoding='utf-8',mode='r')as f1:
        dic=json.load(f1)
        print(f'{os.getpid()}查看了票数剩余{dic["count"]}')

def paid():
    with open('ticket.json',encoding='utf-8',mode='r')as f1:
        dic=json.load(f1)
    if dic['count']>0:
        dic['count']-=1
        time.sleep(random.randint(1,3))
        with open('ticket.json',encoding='utf-8',mode='w') as f1:
            json.dump(dic,f1)
        print(f'用户{os.getpid()}购买成功')
    else:
        print(f'用户{os.getpid()}购买失败')
def task(lock):
    search()
    lock.acquire()
    paid()
    lock.release()

if __name__ == '__main__':
    lock = Lock()
    for i in range(4):
        p=Process(target=task,args=((lock,)))
        p.start()

基于队列通信.

队列

就是存在于内存中的一个容器,最大的一个特点; 队列的特性就是FIFO.完全支持先进先出的原则.

队列的属性和参数

引用的模块
from multiprocessing import Queue
队列的属性和参数
q = Queue(3)  #可以设置元素个数maxsize是默认参数 括号不填默认2147483647与系统有关
q.put(func)#插入数据 可以插入对象 与数据类型当插入过多到达队列个数会夯住可以设置参数报错
print(q.get())#读取队列数据 get一次读一次 单队列没数据了就会夯住
q.put(333,block=False)# 改成False 超过了设置数 不阻塞了 直接报错.
q.put(333,timeout=3)  # 延时报错,超过三秒再put不进数据,就会报错.
q.get(333,block=False)  # 改成False  取不出了 不阻塞了 直接报错.
q.get(333,timeout=3)  # 等3秒还 取不出了 不阻塞了 直接报错.

利用队列 通讯实列

# 小米:抢手环4.预期发售10个.
# 有100个人去抢.
from multiprocessing import Process
import time
from multiprocessing import Queue
import os
def task(q):
    try:
        q.put(os.getpid(),block=False)#队列满了报错
    except Exception:
        return

if __name__ == '__main__':
    q=Queue(10)#同一个队列
    for i in range(50):
        p=Process(target=task,args=((q,)))
        p.start()
    for i in range(1,11):
        print(f'排名{i}号的,用户{q.get()}')

队列间通信中生产者消费者模型.

本质 队列间通信
为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,**如果生产者处理速度很快**,**而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者**,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
多个进程实现并发的效果: 生产者消费者模型.
3个成员 生产者 消费者 容器
模型, 设计模式,归一化设计, 理论等等,教给你一个编程思路.如果以后遇到类似的情况,直接套用即可.
如果没有容器, 生产者与消费者强耦合性.不合理.所以我们要有一个容器,缓冲区.平衡了生产力与消费力.
什么是生产者
生产数据 把数据放在容器中 这个进程就是生产者
什么是消费者
在容器中取数据的进程 这个进程就是消费者
容器
就是缓冲区(队列).一个缓冲区,平衡了生产者和消费者的处理能力。.提高了效率

实列

from multiprocessing import Process
from multiprocessing import Queue
import random
import time
def task1(q):#生产者
    for i in range(6):
        time.sleep(random.randint(1,3))
        q.put(f'{i}号包子')
        print(f'生产者生产了{i}号包子')
def task2(q):#消费者
    while 1:
        try:
            time.sleep(random.randint(1, 3))
            q1=q.get(timeout=3)
            print(f'消费者取得{q1}')
        except Exception:
            return
if __name__ == '__main__':
    q=Queue()
    p1=Process(target=task1,args=((q,)))
    p2=Process(target=task2,args=((q,)))
    p1.start()
    p2.start()

互斥锁中死锁现象

第一种 加了2次同样的锁 只存在于互斥锁

from threading import Thread
from threading import Lock
def task(lock):
    lock.acquire()
    lock.acquire()
    print('66666')
    lock.release()
    lock.release()
if __name__ == '__main__':
    lock = Lock()
    t=Thread(target=task,args=((lock,)))
    t.start()

第2种 2个进程都想获取对方的锁 却不可能实现

from threading import Thread
from threading import Lock
import time
def fun1(lock_A,lock_B):
    lock_A.acquire()
    print(f'{t.name}拿到了A锁')
    lock_B.acquire()
    print(f'{t.name}拿到了B锁')
    lock_B.release()
    lock_A.release()
def fun2(lock_A,lock_B):
    lock_B.acquire()
    print(f'{t.name}拿到了B锁')
    time.sleep(0.1)
    lock_A.acquire()
    print(f'{t.name}拿到了A锁')
    lock_A.release()
    lock_B.release()
def task(lock_A,lock_B):
    fun1(lock_A,lock_B)
    fun2(lock_A,lock_B)
if __name__ == '__main__':
    lock_A = Lock()
    lock_B = Lock()
    for i in range(3):
        t=Thread(target=task,args=((lock_A,lock_B)))
        t.start()

可重复锁RLock

可重复锁,是线程相关的锁不管实列化多少次都是同一只把锁,

引用计数,只要计数不为0,其他线程不可以抢.

可以解决死锁现象

from threading import Thread
from threading import RLock
from multiprocessing import RLock
import time
def fun1(lock_A,lock_B):
    lock_A.acquire()
    print(f'{t.name}拿到了A锁')
    lock_B.acquire()
    print(f'{t.name}拿到了B锁')
    lock_B.release()
    lock_A.release()
def fun2(lock_A,lock_B):
    lock_B.acquire()
    print(f'{t.name}拿到了B锁')
    time.sleep(0.1)
    lock_A.acquire()
    print(f'{t.name}拿到了A锁')
    lock_A.release()
    lock_B.release()
def task(lock_A,lock_B):
    fun1(lock_A,lock_B)
    fun2(lock_A,lock_B)
if __name__ == '__main__':
    lock_B=lock_A = RLock()
    for i in range(5):
        t=Thread(target=task,args=((lock_A,lock_B)))
        t.start()

信号量Semaphore

本质就是一个计数器,用来为多个进程共享的数据结构提供受控访问。
控制并发数量

from threading import Semaphore
from threading import Thread
from threading import current_thread
import time
import random

def task(sem):
    sem.acquire()
    print(f'{current_thread().name}在运行中')
    time.sleep(random.randint(1,3))
    sem.release()

if __name__ == '__main__':
    sem=Semaphore(4)
    for i in range(50):
        t=Thread(target=task,args=((sem,)))
        t.start()

GIL全局解释器锁

GIL锁: 全局解释器锁. Cpython特有的一把互斥锁,自动加锁解锁将并发变成串行
同一时刻同一进程中只有一个线程被执行使用共享资源 ,牺牲效率,保证数据安全.
为什么加锁?
当时都是单核时代,而且cpu价格非常贵.
如果不加全局解释器锁, 开发Cpython解释器的程序员就会在源码内部各种主动加锁,解锁,非常麻烦,各种死锁现象等等.他为了省事儿,直接进入解释器时给线程加一个锁.

优点: 保证了Cpython解释器的数据资源的安全.
缺点: 单个进程的多线程不能利用多核.
Jpython没有GIL锁.
pypy也没有GIL锁.

GIL与lock锁的区别

相同点: 都是同种锁,互斥锁.
不同点:
GIL锁全局解释器锁,保护解释器内部的资源数据的安全.
GIL锁 上锁,释放无需手动操作. 只有遇到io阻塞就会释放
自己代码中定义的互斥锁保护进程中的资源数据的安全.
自己定义的互斥锁必须自己手动上锁,释放锁.

验证计算密集型IO密集型的效率

不能用多核会影响效率吗

看处理数据情况

我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:
方案一:开启四个进程
方案二:一个进程下,开启四个线程
#单核情况下,分析结果: 
  如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜
  如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜

#多核情况下,分析结果:
  如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜
  如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜

总结:
多核的前提下: 如果任务Io密集型: 多线程并发.
如果任务计算密集型: 多进程并发

验证纯计算型实列

from multiprocessing import Process
from threading import Thread
import time
# def task():
#     count=0
#     for i in range(100000000):
#         count += 1
# if __name__ == '__main__':
#     time_1=time.time()
#     l1=[]
#     for i in range(5):
#         p=Process(target=task,args=(()))
#         p.start()
#         l1.append(p)
#     for i in l1:
#         i.join()
#     print(f'总用时{time.time()-time_1}')#14.197925329208374


from threading import Thread
import time
def task():
    count=0
    for i in range(100000000):
        count += 1
if __name__ == '__main__':
    time_2=time.time()
    l1=[]
    for i in range(5):
        p=Thread(target=task,args=(()))
        p.start()
        l1.append(p)
    for i in l1:
        i.join()
    print(f'总用时{time.time()-time_2}')#25.88302206993103

验证io阻塞型实列

from multiprocessing import Process
from threading import Thread
import time
def task():
    count=0
    time.sleep(1)
    for i in range(666666):
        count += 1
if __name__ == '__main__':
    time_1=time.time()
    l1=[]
    for i in range(5):
        p=Process(target=task,args=(()))
        p.start()
        l1.append(p)
    for i in l1:
        i.join()
    print(f'总用时{time.time()-time_1}')#1.4887681007385254


# from threading import Thread
# import time
# def task():
#     count=0
#     time.sleep(1)
#     for i in range(100):
#         count += 1
# if __name__ == '__main__':
#     time_2=time.time()
#     l1=[]
#     for i in range(5):
#         p=Thread(target=task,args=(()))
#         p.start()
#         l1.append(p)
#     for i in l1:
#         i.join()
#     print(f'总用时{time.time()-time_2}')#1.0018517971038818

多线程实现socket通信

server端

import socket
from threading import Thread
def communicate(conn,addr):
    while 1:
        try:
            from_client_data = conn.recv(1024)
            print(f'来自客户端{addr[1]}的消息: {from_client_data.decode("utf-8")}')
            to_client_data = input('>>>').strip()
            conn.send(to_client_data.encode('utf-8'))
        except Exception:
            break
    conn.close()

if __name__ == '__main__':
    server = socket.socket()
    server.bind(('127.0.0.1', 8848))
    server.listen(5)
    while 1:
        conn, addr = server.accept()
        t = Thread(target=communicate, args=(conn, addr))#来一个开一个线程
        t.start()

client端

import socket
client = socket.socket()
client.connect(('127.0.0.1',8848))
while 1:
    try:
        to_server_data = input('>>>').strip()
        client.send(to_server_data.encode('utf-8'))
        
        from_server_data = client.recv(1024)
        
        print(f'来自服务端的消息: {from_server_data.decode("utf-8")}')
    except Exception:
        break
client.close()

基于多线程池的socket通信(TCP)

server

import socket
from threading import Thread
from concurrent.futures import ThreadPoolExecutor
def communicate(conn,addr):
    while 1:
        try:
            from_client_data = conn.recv(1024)
            print(f'来自客户端{addr[1]}的消息: {from_client_data.decode("utf-8")}')
            to_client_data = input('>>>').strip()
            conn.send(to_client_data.encode('utf-8'))
        except Exception:
            break
    conn.close()

if __name__ == '__main__':
    p = ThreadPoolExecutor()
    server = socket.socket()
    server.bind(('127.0.0.1', 8848))
    server.listen(5)
    while 1:
        conn, addr = server.accept()
        p.submit(communicate, conn, addr)

client

import socket
client = socket.socket()
client.connect(('127.0.0.1',8848))
while 1:
    try:
        to_server_data = input('>>>').strip()
        client.send(to_server_data.encode('utf-8'))
        
        from_server_data = client.recv(1024)
        
        print(f'来自服务端的消息: {from_server_data.decode("utf-8")}')
    except Exception:
        break
client.close()

进程池,线程池

什么是池

# 要在程序开始的时候,还没提交任务先创建固定数量的进程或线程 放在一个池子里,这就是池

为什么要用池?

# 如果先开好进程/线程,那么有任务之后就可以直接使用这个池中的数据了
# 并且开好的线程或者进程会一直存在在池中 处理完毕进程并不关闭,可以被多个任务反复利用
     # 这样极大的减少了开启\关闭\调度线程/进程的时间开销
# 池中的线程/进程个数控制了操作系统需要调度的任务个数,控制池中的单位
        # 有利于提高操作系统的效率,减轻操作系统的负担

开启一个池

from concurrent.futures import ProcessPoolExecutor#引用的模块
from concurrent.futures import ThreadPoolExecutor
import time
import os
import random

def task(name):
    print(name)
    print(f'{os.getpid()} 准备接客')
    time.sleep(random.randint(1,3))

if __name__ == '__main__':

    p = ThreadPoolExecutor()  # 线程默认cup数*5 ,进程cup数
    for i in range(23):
        p.submit(task,1)  # 给进程池放任务,传参

阻塞非阻塞,同步异步

在执行的角度

阻塞:程序运行时遇到了IO,程序挂起,cpu被切走.
非阻塞:程序没有遇到I0,程序遇到10但是我通过某种手段,让cpu 强行运行我的程序.

在发起的角度

同步:提交-个任务 自任务开始运行直到此任务结束(可能有I0),返回一个返回值之后,我在提交下一个任务.
异步:一次提交多个任务然后我就直接执行下一行代码.

返回值的回收方式

同步调用

# 同步调用 获取结果后在发出下一个
# 函数的返回值就是结果 一个任务是通过一个函数实现的,任务完成了他的返回值就是数的返回值.
# obj.result()必须使其等到这个任务完成后,返回了结果之后,在执行下一个任务.
# pool.shutdown(wait=True)
# shutdown: 让我的主进程等待进程池中所有的子进程都结束任务之后,在执行,有点类似与join.
# shutdown:在上一个进程池没有完成所有的任务之前,不允许添加新的任务,
from concurrent.futures import ProcessPoolExecutor
import time
import random
import os
def task(i):
    print(f'{os.getpid()}开始')
    time.sleep(random.randint(1,3))
    print(f'{os.getpid()}结束')
    return i
if __name__ == '__main__':
    pool=ProcessPoolExecutor(5)
    for i in range(10):
        obj=pool.submit(task,i)
        print(f'任务结果{obj.result()}')
    pool.shutdown(wait=True)
    print('我是主进程')

异步调用

第一种 异步调用 异步发出 所有任务完结后统一回收

#异步调用 异步发出 所有任务完结后统一回收
# 函数的返回值就是结果 一个任务是通过一个函数实现的,任务完成了他的返回值就是数的返回值.
# obj是一个动态对象,返回的当前的对象的状态,有可能运行中,可能(就绪阻塞),还可能是结束了.
# obj.result()必须使其等到这个任务完成后,返回了结果之后,在执行下一个任务.
# pool.shutdown(wait=True)
# shutdown: 让我的主进程等待进程池中所有的子进程都结束任务之后,在执行,有点类似与join.
# shutdown:在上一个进程池没有完成所有的任务之前,不允许添加新的任务,
from concurrent.futures import ProcessPoolExecutor
import time
import random
import os
def task(i):
    print(f'{os.getpid()}开始')
    time.sleep(random.randint(1,3))
    print(f'{os.getpid()}结束')
    return i
if __name__ == '__main__':
    pool=ProcessPoolExecutor(5)
    l1=[]
    for i in range(10):
        obj=pool.submit(task,i)
        l1.append(obj)
    pool.shutdown(wait=True)
    for i in l1:
        print(f'执行结果{i.result()}')
    print('我是主进程')

异步调用+回调函数 推导的

版本1

# 版本一 模拟爬虫:
#     1. 异步发出10个任务,并发的执行,但是统一的接收所有的任务的返回值.(效率低,不能实时的获取结果)
#     2. 分析结果流程是串行,影响效率.
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
import time
import random
import os
import requests
def task(url):
    ret=requests.get(url)
    if ret.status_code==200:
        return ret.text
def parse(content):
    print(f'池里面的id  ---{os.getpid()}')
    return len(content)
if __name__ == '__main__':
    url_list = [
            'http://www.baidu.com',
            'http://www.JD.com',
            'http://www.JD.com',
            'http://www.JD.com',
            'http://www.taobao.com',
            'https://www.cnblogs.com/jin-xin/articles/7459977.html',
            'https://www.luffycity.com/',
            'https://www.cnblogs.com/jin-xin/articles/9811379.html',
            'https://www.cnblogs.com/jin-xin/articles/11245654.html',
            'https://www.sina.com.cn/',

        ]
    pool=ThreadPoolExecutor(2)
    obj_list=[]
    for url in url_list:
        obj=pool.submit(task,url)
        obj_list.append(obj)
    pool.shutdown(wait=True)
    for res in obj_list:
        print(parse(res.result()))
    print(f'主{os.getpid()}')

版本2

针对版本一的缺点2,改进,让串行编程并发或者并行.
1 在开一个线程进程池,并发并行的处理. 再开一个线程进程池,开销大.
2 将原来的任务扩大,
版本二:
线程池设置4个线程, 异步发起10个任务,每个任务是通过网页获取源码+数据分析, 并发执行,
最后将所有的结果展示出来.
耦合性增强了.
并发执行任务,此任务最好是IO阻塞,才能发挥最大的效果
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
import time
import random
import os
import requests
def task(url):
    ret = requests.get(url)
    if ret.status_code == 200:
        return parse(ret.text)
def parse(content):
    return len(content)
if __name__ == '__main__':
    url_list = [
            'http://www.baidu.com',
            'http://www.JD.com',
            'http://www.JD.com',
            'http://www.JD.com',
            'http://www.taobao.com',
            'https://www.cnblogs.com/jin-xin/articles/7459977.html',
            'https://www.luffycity.com/',
            'https://www.cnblogs.com/jin-xin/articles/9811379.html',
            'https://www.cnblogs.com/jin-xin/articles/11245654.html',
            'https://www.sina.com.cn/',

        ]
if __name__ == '__main__':
    pool = ThreadPoolExecutor(4)
    obj_list = []
    for url in url_list:
        obj = pool.submit(task, url)
        obj_list.append(obj)
    pool.shutdown(wait=True)
    for res in obj_list:
        print(res.result())

版本3 最终版

# 版本三:
# 基于 异步调用回收所有任务的结果我要做到实时回收结果,
# 并发执行任务每个任务只是处理IO阻塞的,不能增加新的功能.
# 异步调用 + 回调函数 回调函数没有返回值 obj.add_done_callback(parse)
# obj是一个动态对象,返回的当前的对象的状态,有可能运行中,可能(就绪阻塞),还可能是结束了.
注意事项
    线程池设置4个线程, 异步发起10个任务,每个任务是通过网页获取源码, 并发执行,
    当一个任务完成之后,将parse这个分析代码的任务交由剩余的空闲的线程去执行,你这个线程继续去处理其他任务.
    如果进程池+回调: 回调函数由主进程去执行.
    如果线程池+回调: 回到函数由空闲的线程去执行.
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time
import random
import os
import requests
def task(url):
    ret = requests.get(url)
    if ret.status_code == 200:
        return ret.text
        
def parse(obj):
    print(len(obj.result()))


if __name__ == '__main__':

    # 开启线程池,并发并行的执行
    url_list = [
        'http://www.baidu.com',
        'http://www.JD.com',
        'http://www.JD.com',
        'http://www.JD.com',
        'http://www.taobao.com',
        'https://www.cnblogs.com/jin-xin/articles/7459977.html',
        'https://www.luffycity.com/',
        'https://www.cnblogs.com/jin-xin/articles/9811379.html',
        'https://www.cnblogs.com/jin-xin/articles/11245654.html',
        'https://www.sina.com.cn/',

    ]
    pool = ThreadPoolExecutor(4)
    for url in url_list:
        obj = pool.submit(task, url)
        obj.add_done_callback(parse)

from concurrent.futures import ThreadPoolExecutor
def task(x,y):
    return x*y
def task1(obj,):
    print(f'计算结果{obj.result()}--------')
if __name__ == '__main__':
    pool=ThreadPoolExecutor(5)
    for i in range(5):
        obj=pool.submit(task,5,6)
        obj.add_done_callback(task1)

回调函数总结

当一个任务完成之后,将parse这个分析代码的任务交由剩余的空闲的线程去执行,你这个线程继续去处理其他任务

如果进程池+回调: 回调函数由主进程去执行.

如果线程池+回调: 回到函数由空闲的线程去执行.

导入第3方模块

终端导入

添加环境变量把c:\Python36\Scripts添加到环境变量
在cmd输入 pip install requests
就可以添加 requests模块了

cpythom导入

file---Settings----Project:  ---Project Interpreter---旁边的加号

re模块的爬虫

#浏览器工作原理,向服务端发送一个请求,服务端验证你的请求,如果正确,给你的浏览器返回一个文件,
#浏览器接收到文件,将文件里面的代码渲染成你看到的漂亮美丽的模样。
#什么叫爬虫?
# 1.利用代码模拟一个浏览器,进行浏览器的工作流程得到一堆源代码.
# 2.对源代码进行数据清洗得到我想要数据.
import requests
ret=requests.get('http://www.baidu.com')
if ret.status_code == 200:
    print(ret.text)

线程队列

FIFO队列

# 1 FIFO队列 queue 先进先出 特殊参数 block=True, timeout=None
import queue
# q=queue.Queue(3)
# q.put(666)
# q.put(777)
# q.put(888)
# q.put_nowait() #没有数据就报错,可以通过try来搞
# print(q.get())
# print(q.get())
# print(q.get())
# q.get_nowait() #没有数据就报错,可以通过try来搞

LIFO 栈

# LIFO 栈. 先进后出 特殊参数 block=True, timeout=None
import queue
# q=queue.LifoQueue(3)
# q.put(666)
# q.put(777)
# q.put(888)
# print(q.get())
# print(q.get())
# print(q.get())

优先级队列

# 优先级对列 需要元组的形式,(int,数据) int 代表优先级,数字越低,优先级越高. get先去重要的
import queue
q=queue.PriorityQueue(3)
q.put((10,'拉级消息'))
q.put((-10,'重要消息'))
q.put((5,'一般消息'))
print(q.get())
print(q.get())
print(q.get())

事件Event

使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行

并发的执行某个任务 .多线程多进程,几乎同时执行.

一个线程执行到中间时遇到事件event通知另一个线程开始执行.

属性

# event = Event()  # 默认是False
# event.set()  # 改成了True
# event.wait()  # 轮询检测event是否为True,当其为True,继续下一行代码. 阻塞.
# event.wait(1)
# 设置超时时间,如果1s中以内,event改成True,代码继续执行.
# 设置超时时间,如果超过1s中,event没做改变,代码继续执行.
print(event.is_set()) 查询状态

第一版 没用 Event版

# 引入 
import time
from threading import Thread
from threading import current_thread
flag = False
def task():
    print(f'{current_thread().name} 检测服务器是否正常开启....')
    time.sleep(3)
    global flag
    flag = True

def task1():
    while 1:
        time.sleep(1)
        print(f'{current_thread().name} 正在尝试连接服务器.....')
        if flag:
            print('连接成功')
            return

if __name__ == '__main__':
    for i in range(4):
        t=Thread(target=task,)
        t.start()
    t4=Thread(target=task1,)
    t4.start()

第2版用了 Event

import time
from threading import Thread
from threading import current_thread
from threading import Event
def task():
    print(f'{current_thread().name} 检测服务器是否正常开启....')
    time.sleep(3)
    event.set()

def task1():
    while 1:
        time.sleep(1)
        print(f'{current_thread().name} 正在尝试连接服务器.....')
        event.wait()
        print(f'{current_thread().name} 连接成功')

if __name__ == '__main__':
    event = Event()
    for i in range(4):
        t=Thread(target=task,)
        t.start()
    t4=Thread(target=task1,)
    t4.start()


小列题

 一个线程监测服务器是否开始,
 另个一线程判断如果开始了,则显示连接成功,此线程只尝试连接3次,1s 一次,如果超过3次,还没有连接成功,则显示连接失败.
from threading import Thread
from threading import Event
from threading import current_thread
import time
event=Event()
def check():
    print(f'{current_thread().name}监测服务器是否开启')
    time.sleep(3)
    event.set()
    print(f'服务器已经开启...')
def connect():
    print(f'{current_thread().name}等待连接。。。')
    count=1
    while 1:
        event.wait(1)
        if count==4:
            print('机会没了')
            break
        if event.is_set():
            print(f'{current_thread().name}连接成功')
            break
        else:
            print(f'{current_thread().name}连接失败还有{3-count}次机会')
            count+=1
t1=Thread(target=check)
t2=Thread(target=connect)
t1.start()
t2.start()


协程的初识

协程本质上就是一个线程 一个线程实现并发.如果协程中处理的所有任务都遇到了阻塞 协程就会停止 只有阻塞完成会切回来 进程间是由操作系统调控cpu 而协程是由我们自己书写的程序调控的

单个cpu: 10个任务,让你给我并发的执行这个10个任务:

  1. 方式一:开启多进程并发执行, 操作系统切换+保持状态.
  2. 方式二:开启多线程并发执行,操作系统切换+保持状态.
  3. 方式三:开启协程并发的执行, 自己的程序 把控着cpu 在3个任务之间来回切换+保持状态.

协程他切换速度非常快,蒙蔽操作系统的眼睛,让操作系统认为cpu一直在运行你这一个线程(协程.)

单核心下处理多任务最好的方式

协程 开销小. 运行速度快. 协程会长期霸占cpu只执行我程序里面的所有任务.

并发的本质:就是切换+保持状态.

协程处理IO密集型, 计算密集型,还是串行好.

什么是协程? 单个线程并发的处理多个任务. 程序控制协程的切换+保持状态.

协程的特点:

  1. 必须在只有一个单线程里实现并发
  2. 修改共享数据不需加锁
  3. 用户程序里自己保存多个控制流的上下文栈(保持状态)
  4. 附加:一个协程遇到IO操作自动切换到其它协程

工作中:

​ 一般在工作中我们都是进程+线程+协程的方式来实现并发,以达到最好的并发效果,如果是4核的cpu,一般起5个进程,每个进程中20个线程(5倍cpu数量),每个线程可以起500个协程,大规模爬取页面的时候,等待网络延迟的时间的时候,我们就可以用协程去实现并发。 并发数量 = 5 * 20 * 500 = 50000个并发,这是一般一个4cpu的机器最大的并发数。nginx在负载均衡的时候最大承载量就是5w个

  单线程里的这20个任务的代码通常会既有计算操作又有阻塞操作,我们完全可以在执行任务1时遇到阻塞,就利用阻塞的时间去执行任务2。。。。如此,才能提高效率,这就用到了Gevent模块。

推导过程

yield实现协程

其中上述的第二种情况并不能提升效率,只是为了让cpu能够雨露均沾,实现看起来所有任务都被“同时”执行的效果,如果多个任务都是纯计算的,这种切换反而会降低效率。为此我们可以基于yield来验证。yield本身就是一种在单线程下可以保存任务运行状态的方法。

切换 + 保持状态

# 切换 + 保持状态
def gen():
    for i in range(10,1,-1):
        yield i

def func():
    obj = gen()
    for i in range(5):
        print(next(obj))
func()

总结协程特点:

  1. 必须在只有一个单线程里实现并发
  2. 修改共享数据不需加锁
  3. 用户程序里自己保存多个控制流的上下文栈
  4. 附加:一个协程遇到IO操作自动切换到其它协程(gevent模块来实现)

利用greenlet 切换 +保持状态

用greenlet模块可以非常简单地实现这20个任务直接的切换

真正的协程模块就是使用greenlet完成的切换

# 切换 +保持状态(遇到IO不会主动切换)
# switch()代表切换
from greenlet import greenlet
import time
def eat(name):
    print('%s eat 1' %name)  # 2
    g2.switch('taibai')  # 3
    time.sleep(3)
    print('%s eat 2' %name)  # 6
    g2.switch()  # 7

def play(name):
    print('%s play 1' %name)  # 4
    g1.switch()  # 5 切换
    print('%s play 2' %name)  # 8

g1=greenlet(eat)
g2=greenlet(play)

最终版本

​ Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

必须要jion 不然线程结束了
# g1.join()
# g2.join()
gevent.joinall([g1,g2])#与上面2个合并效果相同


# 最终版本:
import gevent## 切换 +保持状态(遇到IO不会主动切换)

from gevent import monkey
monkey.patch_all()  # 打补丁: 将下面的所有的任务的阻塞都打上标记 遇到就切换

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

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

g1 = gevent.spawn(eat,'egon')#固定写法
g2 = gevent.spawn(play,name='egon')

# g1.join()
# g2.join()
gevent.joinall([g1,g2])

posted @ 2020-01-10 17:22  一起奥利给  阅读(94)  评论(0编辑  收藏  举报