并发编程

多道技术:

空间复用:

同一时间在内存中加载不同的数据,其内存之间是相互隔离。

时间上的复用:切换加保存。

切换的两种情况:1一个进程遇到IO操作会切换到另一个进程

2.时间片用完了也会被强行切换,切换的时候会记录状态。

多道技术的出现使计算机由串行执行任务变成并发执行任务。

进程:

进程就是一个运行的程序,一个程序运行可以产生多个进程,但进程与进程之间是相互隔离的,

它也是一个资源单位,包含运行程序所需要的所有资源。

为什么使用进程?

  为了并发的执行多个任务。

两种使用方式:

  创建Process实例

  继承Process类,覆盖run方法

注意:开启进程的代码必须放在判断下面,因为windows开启子进程时,会导入代码执行一遍来回去要执行的任务。

守护进程:

  在被守护进程结束时,守护进程也会随之结束。当然守护进程也可以提前结束。

常用属性和方法:

   join 提高子进程的优先级,让子进程先于父进程执行代码,父进程的代码必须等被join的子进程的代码执行完才会执行。

   is_alive 进程是否存活

   pid 查看进程id

   terminate  终止进程

     exitcode  获取进程的退出码

   name 查看进程的名字

   daemon 设置为守护进程

僵尸和孤儿进程:

孤儿  父进程先于子进程结束,子进程会被操作系统接管,孤儿进程有其存在的意义。

僵尸进程  :在linux中有一个机制,可以保证父进程在任何时候都可以访问到子进程的一些信息,所以子进程结束后并不会立即清除所有数据,这时候就是僵尸进程。

僵尸进程会占用一些系统资源,需要父进程调用waitpid来进行清理,python会自动清理僵尸进程。

IPC

进程间通讯

因为每个进程之间内存是物理隔离的,很多时候我们需要将数据讲给另外一个进程,例如:美团要将支付信息交给支付宝

1.共享文件

  特点:数据量没有什么限制,但读写速度慢

2.共享内存区域

  特点:数据量小,但读写速度快

3.管道

  特点:只能单向通讯。而且要二进制解码

4.socket

  特点:代码结构复杂

主要方式:共享内存:

  1.Manager 提供一系列常用的数据结构,但是没有处理锁的问题

  2.进程Queue 是一种特殊的容器,队列,规定了先进先出,并且支持IPC,已经处理好了锁了。

互斥锁:

相互排斥的锁 mutex

其本质就是一个标志,不是说锁住了代码,只是限制了代码是否能够执行

为什么需要锁:因为多个进程使用同一个资源时会造成数据错乱

特点:加锁会使进程由并发变成串行,提高了安全性但降低了效率。

与join的区别:

join是把整个进程代码全部变为串行,这样失去了并发的本意,而且限制了主进程的代码执行

锁是想锁哪里就锁哪里,锁的部分变成的串行,其他仍然并发,且不限制主进程代码的执行。

锁的粒度越小效率越高。

消费者生产者模型:

   要解决的问题:生产者与消费者处理能力不平衡,如果串行执行任务,效率极低。

解决的方案:

  1.将生产者与消费者分开耦合

  2.将双方并发执行

  3.提供一个共享的容器

  4.生产者将数据放入容器

  5.消费者从容器中取数据

多线程:

线程:cpu最小的执行单位,操作系统最小的调度单位,一个固定的执行流程的总称。

一个进程至少包含一个线程,称之为主线程,是由操作系统自动开启的。

运行过程中自己开启的线程称为子线程

线程之间是平等的,没有父子之分,而且线程的开启代码可以放在任何位置,不需要放在判断下面。

特点:线程的创建开销比进程小。2.同一个进程内的多个线程可以共享同一个进程内的资源。不同进程的线程也是隔离的。

使用方式:和进程的使用方式一样,只是代码可以放到任意位置

常用方法:

  currentthread()获取当前的线程对象

  active_cournt 获取存活的线程个数

  enumerate()获取所有运行中的线程对象。

线程队列:

queue:

  Queue 普通队列,作用和joinablequeue一样,但不能作为IPC使用

  LifoQueue 先进后出,后进先出,堆栈

  priorityQueue 优先级队列,里面必须是可以被运算的,打印出来,越小的优先级越高

线程锁:

lock互斥锁

RLock递归锁,同一个线程可以多次加锁,但还是要按照规定几次加锁对应几次解锁

信号量 semaphore 可以限制同一时间多少线程可以同时并发执行

死锁:当一个资源的访问,需要具备多把锁时,然而不同的锁被不同线程持有了,陷入相互等待中。

  1.尽量使用一个锁,设置超时释放手里的锁

  2.抢锁时,按照顺序抢

GIL互斥锁:

全局解释器锁,是锁解释器的,因为cpython的内存管理是非线程安全的,它本质上也是一个互斥锁,为了防止多个本地线程同时执行python的字节码。

有了这把锁,线程要执行代码必须先抢锁,谁抢到就谁先执行。

优点:解决了cpython的内存管理的线程安全。

缺点:多个线程不能并行执行,失去了多核的优势。

为什么不处理它,因为去掉这个锁的话,会有很多代码需要重构,并且需要程序自己来处理很多的安全问题,这样成本太大,而且很复杂、

如何避免性能影响?

首先判断任务是IO密集型还是计算密集型

IO密集型使用多线程

计算密集型使用多进程

它与自定义锁的区别:

它只保证解释器级别的数据安全,比如引用计数,如果我们自己开启了一些不属于解释器的资源,比如共享文件,那么还是需要我们自己加锁来保证安全。

加锁和释放:

  拿到解释器要执行的时候立即加锁

  遇到IO时解锁

  cpu时间片用完了,注意解释器的超时时间与cpu的超时时间不同,为100nm(纳秒)

进程池和线程池:

池:就是一个容器

线程池:就是存储线程的容器

为什么使用线程池:

  1.可以限制线程数量  通过压力测试来得出最大数量

  2.可以管理线程的创建和销毁

  3.可以负责任务的分配

进程池一样

使用:创建池,然后submit提交任务

异步任务将返回future对象,调用add_done_callback可以添加回调函数

在任务结束时还会自动调用回调函数并传入future本身,调用result()可以拿到任务的结果。

  不常用的两种方式

  shutdown 可以关闭线程池,会阻塞直到所有任务全部完成

  直接调用result 如果任务没有完成会进入阻塞状态、

异步和同步:

  同步是指提交任务后,必须等待任务执行完后的结果才能继续执行

  异步是指提交任务后,不需要等待任务执行完毕,可以去做其他事情

异步回调:

  本质上就是一个普通函数,该函数会在任务执行完成后自动被调用

  线程池,谁有空谁执行

  进程池,都是在父进程中回调

协程:

单线程实现并发,协程也称轻量级线程,也称微线程,可以由应用程序自己来控制调度。

好处:可以在一个任务遇到IO操作时,自主切换到自己进程中其他线程

  如果任务足够多的,就可以充分利用cpu 的时间片

缺点:

  仅适用于IO密集型任务,计算密集型,如果是单线程下的串行效率更高,建议使用多进程来处理

  当单个任务耗时较长时,协程效率反而不高

我们的目的就是尽可能的提高效率

  进程   线程  协程

  可以多进程 +线程+协程

协程对比多线程:

线程池可以解决一定的并发数量,但是如果并发量超过了机器能承受最大限制,线程池就出现瓶颈了

协程的使用:

gevent 需要自己安装

1.先打补丁 (本质是将原本阻塞的代码替换成非阻塞的代码)

2.gevent.spawn(任务) 来提交任务

3.必须保证主线不会结束 使用join 或是join all

IO模型:

模型就是解决某个问题的套路

IO问题:

输入输出

我要一个用户名用来执行登陆操作,问题用户名需要用户输入,输入需要耗时, 如果输入没有完成,后续逻辑无法继续,所以默认的处理方式就是 等

将当前进程阻塞住,切换至其他进程执行,等到按下回车键,拿到了一个用户名,再唤醒刚才的进程,将状态调整为就绪态

 

以上处理方案 就称之为阻塞IO模型

 

存在的问题:

当执行到recv时,如果对象并没有发送数据,程序阻塞了,无法执行其他任务

解决方案:

多线程或多进程,

当客户端并发量非常大的时候,服务器可能就无法开启新的线程或进程,如果不对数量加以限制 服务器就崩溃了

线程池或进程池

首先限制了数量 保证服务器正常运行,但是问题是,如果客户端都处于阻塞状态,这些线程也阻塞了

协程:

使用一个线程处理所有客户端,当一个客户端处于阻塞状态时可以切换至其他客户端任务

非阻塞IO模型

阻塞IO模型在执行recv 和 accept 时 都需要经历wait_data

非阻塞IO即 在执行recv 和accept时 不会阻塞 可以继续往下执行

 

如何使用:

将server的blocking设置为False 即设置非阻塞

 

存在的问题 :

这样一来 你的进程 效率 非常高 没有任何的阻塞

很多情况下 并没有数据需要处理,但是我们的进程也需要不停的询问操作系统 会导致CPU占用过高

而且是无意义的占用

import socket
import time

server = socket.socket()
server.bind(("192.168.13.103",1688))
server.listen()
server.setblocking(False) # 默认为阻塞    设置为False 表示非阻塞

# 用来存储客户端的列表
clients = []

# 链接客户端的循环
while True:
    try:
        client,addr = server.accept()   # 接受三次握手信息
        # print("来了一个客户端了.... %s" % addr[1])
        # 有人链接成功了
        clients.append(client)
    except BlockingIOError as e:
        # print("还没有人连过来.....")
        # time.sleep(0.5)
        # 服务你的客人去
        for c in clients[:]:
            try: # 可能这个客户端还没有数据过来
                # 开始通讯任务
                data = c.recv(2048)
                c.send(data.upper())
            except BlockingIOError as e:
                print("这个客户端还不需要处理.....",)

            except ConnectionResetError:
                # 断开后删除这个客户端
                clients.remove(c)
        print("=======================",len(clients))

多路复用
假设原本有30个socket 需要我们自己来处理, 如果是非阻塞IO模型,相当于从头开始问道尾,如果没有需要处理的
回过头来再次重复,

多路复用解决问题的思路,找一个代理即select,将你的socket交给select来检测
select 会返回 那些已经准备好的 可读或者可写的socket
我们拿着这些准备好的socket 直接处理即可

对比线程池
避免了开启线程的资源消耗
缺点:
同时检测socket不能超过1024


异步IO
阻塞IO recv accept 会将当前线程阻塞住 同步
非阻塞IO recv accept 不会阻塞当前线程 ,没有数据直接抛出异常
分析 属于同步还是异步?
recv (wait_data,copy_data) 设置为非阻塞之后 wait_data不会再阻塞
但是copy_data 也是IO操作 还是会阻塞
也属于同步

多路复用 也属于同步IO


同步 异步 任务的执行方式
同步IO 执行IO任务的方式
异步IO

异步IO
线程池中的submit 就是异步任务
异步的特点就是 立马就会返回
同步翻译为sync 异步async

 

原文地址:https://www.cnblogs.com/xinfan1/p/10999979.html