多道技术 进程 线程 协程 GIL锁 同步异步 高并发的解决方案 生产者消费者模型

本文基本内容

多道技术 进程 线程 协程 并发 多线程 多进程 线程池 进程池

GIL锁 互斥锁 网络IO 同步 异步等

实现高并发的几种方式

协程:单线程实现并发

一 多道技术

产生背景 所有程序串行 导致资源浪费

多道技术的目的就是让多个程序并发执行 同时处理多个任务 提高计算机效率

1.1 空间复用 时间复用

空间复用

指的是 同一时间 内存中加载多个不同程序数据,

每个进程间内存区域相互隔离,物理层面的隔离 

时间复用 切换+保存

切换条件
1 一个进程执行过程中遇到了IO操作 切换到其他进程
2 运行时间过长,会被操作系统强行剥夺执行权力
单纯的切换不够,必须在切换前保存当前的状态,以便于恢复执行

 

二 进程

2.1进程概念

顾名思义,进程即正在执行的一个过程。进程是对正在运行程序的一个抽象。进程是操作系统中最基本、重要的概念

一个程序可以多次执行 产生多个进程,但是进程之间相互独立

2.2 阻塞 非阻塞 并行 并发 同步 异步 (重点)

阻塞 : 程序遇到io操作是就进入了阻塞状态     

​	本地IO input      print     sleep    read  write       

​	网络IO recv  send

非阻塞: 程序正常运行中 没有任何IO操作   就处于非阻塞状态  

阻塞 非阻塞 说的是程序的运行状态

并行 并发

并发: 多个任务看起来同时在处理 ,本质上是切换执行   速度非常快    

并行: 多个任务真正的同时执行    必须具备多核CPU  才可能并行  

并发  并行  说的是 任务的处理方式

同步 异步

 

所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。
  所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。多道技术 进程  线程 协程  GIL锁   同步异步  高并发的解决方案   生产者消费者模型

2.3 进程三种状态

多道技术 进程  线程 协程  GIL锁   同步异步  高并发的解决方案   生产者消费者模型

 

(1)就绪(Ready)状态
当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。
(2)执行/运行(Running)状态当进程已获得处理机,其程序正在处理机上执行,此时的进程状态称为执行状态。
(3)阻塞(Blocked)状态正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。

 

2.4 进程的两种使用方式 Process

1 实例化Process类

from multiprocessing import Process
import os def task(name): # 因为父进程中 关键字给了属性 则需要有一个参数来接收 name可以用name接收一下
print("%s进程run!" %name)
print("%s进程over!" % name) if __name__ == '__main__':
p = Process(target=task,args=('Tom',))
# 实例化一个Process类对象
# 可以为子进程起个名字 比如tom 但是注意 必须是元祖
print('father进程:%s' % os.getpid()) # 打印下父进程的pip看看
p.start() # 需要交给系统去执行 start去执行
print('父进程RUN!') # 这时先运行父进程代码 然后再去执行子进程代码 """
windows 和 linux 开启进程的方式不同
首先相同之处都是 需要将数据copy一份给子进程 这样子进程才知道要干什么
linux 会将父进程的所有数据 完全copy
windows 会copy 一部分数据 同时会导入py文件来执行 这样一来递归开进程
linux 拿到父进程知道代码位置 继续执行
建议都加上判断 可以保证两个平台都能用 记住:
开启进程的代码 都把它放到 if __name__ == "__main__": 中即可 """

  

2 继承Process类 自定义run类来实现自定义的功能

import os
from multiprocessing import Process
class MyProcess(Process): # 继承Process类
def __init__(self,name):
super().__init__() # 这时需要super 因为要覆盖run方法
self.name = name
# 继承Procee覆盖run方法将要执行任务发到run中
def run(self):
print(self.name)
print("子进程 %s running!" % os.getpid())
print("子进程 %s over!" % os.getpid()) if __name__ == '__main__':
# 创建时 不用再指定target参数了
p = MyProcess("rose")
p.start()
print("父进程over!") .第二种方式中,必须将要执行的代码放到run方法中,子进程只会执行run方法其他的一概不管

  

进程中的join的方法

join在进程中的用法就是 让子进程运行完毕之后再去运行父进程

#  join就是让子进程运行完了 再去执行父进程

from multiprocessing import Process
name = 'rose'
def task():
global name
name = 'tom'
print(name)
print('子进程在run!') if __name__ == '__main__':
p = Process(target=task)
p.start()
p.join()
print(name)
print('父进程run!') # tom
# 子进程在run!
# rose
# 父进程run!

  

Process的其他用法

from multiprocessing import  Process
import time,os
def task():
print("121121")
# time.sleep(10)
# print("over")
# print(os.getppid())
exit(1000) if __name__ == '__main__':
p = Process(target=task,name="rose")
p.start() # 懒加载优化机制 如果没有调用start 那么该对象将不会被创建
time.sleep(1)
# p.join() # 等待子进程结束
# p.terminate() # 终止进程
# print(p.name) # 进程的名称
# print(p.is_alive()) #是否存活
# p.terminate() # 与start一样 都是给操作系统发送指令 所以会有延迟
# print(p.pid)
# print(p.exitcode) # 获取退出码

  

2.5 孤儿进程 僵尸进程 (了解)

孤儿进程:指的是开通子进程之后,父进程结束了,而子进程还没运行结束,那这个子进程就成为了孤儿进程,但是这个进程还有有存在的必要的,它没有父进程,接管方就变成了操作系统.
例子; 比如qq打开浏览器 qq关闭了 浏览器还在运行 僵尸进程:僵尸进程指的是,当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被操作系统接管,子进程退出后操作系统会回收其占用的相关资源! 僵尸进程的危害:
由于子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束. 那么会不会因为父进程太忙来不及wait子进程,或者说不知道 子进程什么时候结束,而丢失子进程结束时的状态信息呢? 不会。因为UNⅨ提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就必然可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放. 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生[僵死进程],将因为没有可用的进程号而导致系统不能产生新的进程. 此为僵尸进程的危害,应当避免

  

2.6 守护进程(了解)

什么是守护进程 

​	进程是一个正在运行的程序
​ 守护进程也是一个普通进程
​ 意思是一个进程可以守护另一个进程

  

from multiprocessing import Process
import time # 妃子的一生
def task():
print("入宫了.....")
time.sleep(50)
print("妃子病逝了......") if __name__ == '__main__':
# 康熙登基了
print("登基了.....") # 找了一个妃子
p = Process(target=task) # 设置为守护进程 必须在开启前就设置好
p.daemon = True
p.start() # 康熙驾崩了
time.sleep(3)
print("故事结束了!")

  

2.7 进程间的通信(重点)

IPC(Inter-Process Communication,进程间通信

1 通信方式

	管道: 只能单向通讯,数据都是二进制  

​	文件: 在硬盘上创建共享文件
​ 缺点:速度慢
​ 优点:数据量几乎没有限制 ​ socket:
​ 编程复杂度较高 ​ 共享内存:必须由操作系统来分配 要掌握的方式*****
​ 优点: 速度快
​ 缺点: 数据量不能太大

2 共享内存方式(重点)

Queue队列 帮我们处理了锁的问题 重点

 

from multiprocessing import Queue
# 创建队列 不指定maxsize 则没有数量限制
q = Queue(3)
# 存储元素
# q.put("abc")
# q.put("hhh")
# q.put("kkk") # print(q.get())
# q.put("ooo") # 如果容量已经满了,在调用put时将进入阻塞状态 直到有人从队列中拿走数据有空位置 才会继续执行 #取出元素
# print(q.get())# 如果队列已经空了,在调用get时将进入阻塞状态 直到有人从存储了新的数据到队列中 才会继续 # print(q.get())
# print(q.get()) #block 表示是否阻塞 默认是阻塞的 # 当设置为False 并且队列为空时 抛出异常
q.get(block=True,timeout=2) # 会报错此时 因为取不出来了 # block 表示是否阻塞 默认是阻塞的 # 当设置为False 并且队列满了时 抛出异常
# q.put("123",block=False,)
# timeout 表示阻塞的超时时间 ,超过时间还是没有值或还是没位置则抛出异常 仅在block为True有效

  

Manager类方法 了解

from multiprocessing import Process,Manager,Lock
import time def task(data,l):
l.acquire()
num = data["num"] #
time.sleep(0.1)
data["num"] = num - 1
l.release() if __name__ == '__main__':
# 让Manager开启一个共享的字典
m = Manager()
data = m.dict({"num":10}) l = Lock() for i in range(10):
p = Process(target=task,args=(data,l))
p.start() time.sleep(2)
print(data)
# 需要强调的是 Manager创建的一些数据结构是不带锁的 可能会出现问题 

3 joinablequeue (了解)

from multiprocessing import JoinableQueue
# join 是等待某个任务运行完毕 able是可以的意思 Queue是队列 综合起来 就是一个可以被join的队列 继承自Queue j = JoinableQueue()
# j.put('123') # 放入信息
# print(j.get()) # 获取信息
# j.task_done() # 告知队列这次数据取完了 不是表示任务全部处理完成 而是 取出来某个数据处理完成 j.put('wsx')
j.put('222')
j.put('333')
print('取走了%s'% j.get())
j.task_done()
j.task_done()
# j.task_done()
j.join() # 只有取完了 才可以继续进行 没取完则一直等 # :结论 task_done() 和.put()次数要对应起来 即放入几次数据要有几次告知信息 

三 锁 (重点)

3.1 互斥锁

互斥锁   互相排斥的锁,我在这站着你就别过来,(如果这个资源已经被锁了,其他进程就无法使用了)

需要强调的是: 锁 并不是真的把资源锁起来了,只是在代码层面限制你的代码不能执行

1 为什么需要互斥锁

问题: 

并发将带来资源的竞争问题  当多个进程同时要操作同一个资源时,将会导致数据错乱的问题

解决方案

1 join的方法 (不完善)

弊端
1.把原本并发的任务变成了串行,避免了数据错乱问题,但是效率降低了,这样就没必要开子进程了
2.原本多个进程之间是公平竞争,join执行的顺序就定死了,这是不合理的

2 给公共资源加互斥锁

2 锁和join的区别

	1.
​ join是固定了执行顺序,会造成父进程等待子进程
​ 锁依然是公平竞争谁先抢到谁先执行,父进程可以做其他事情 ​ 2.最主要的区别:
​ join是把进程的任务全部串行
​ 锁可以锁任意代码 一行也可以 可以自己调整粒度

案例

from multiprocessing import Process,Lock
import time,random def task1(lock):
# 要开始使用了 上锁
lock.acquire() #就等同于一个if判断
print("hello iam jerry")
time.sleep(random.randint(0, 2))
print("gender is boy")
time.sleep(random.randint(0, 2))
print("age is 15")
# 用完了就解锁
lock.release() def task2(lock):
lock.acquire()
print("hello iam owen")
time.sleep(random.randint(0,2))
print("gender is girl")
time.sleep(random.randint(0,2))
print("age is 48")
lock.release() def task3(lock):
lock.acquire()
print("hello iam jason")
time.sleep(random.randint(0,2))
print("gender is women")
time.sleep(random.randint(0,2))
print("age is 26")
lock.release() if __name__ == '__main__':
lock = Lock() p1 = Process(target=task1,args=(lock,))
p2 = Process(target=task2,args=(lock,))
p3 = Process(target=task3,args=(lock,)) p1.start()
# p1.join() p2.start()
# p2.join() p3.start()
# p3.join() # print("故事结束!")

  

# 结果  没加join 三者是争夺cpu资源的  谁先抢到就谁先显示  加join之后  就是按顺序来的
hello iam owen
gender is girl
age is 48
hello iam jerry
gender is boy
age is 15
hello iam jason
gender is women
age is 26

  

3.2 死锁问题

死锁问题
当程序出现了不止一把锁,分别被不同的线程持有, 有一个资源要想使用必须同时具备两把锁
这时候程序就会进程无限卡死状态 ,这就称之为死锁
例如:
要吃饭 必须具备盘子和筷子 但是一个人拿着盘子 等筷子 另一个人拿着筷子等盘子 如何避免死锁问题
锁不要有多个,一个足够
如果真的发生了死锁问题,必须迫使一方先交出锁
import time
from multiprocessing import Lock
from threading import Thread,current_thread # 盘子
lock1 = Lock() # 筷子
lock2 = Lock() def eat1():
lock1.acquire()
print("%s抢到了盘子" % current_thread().name)
time.sleep(0.5)
lock2.acquire()
print("%s抢到了筷子" % current_thread().name) print("%s开吃了!" % current_thread().name)
lock2.release()
print("%s放下筷子" % current_thread().name) lock1.release()
print("%s放下盘子" % current_thread().name) def eat2():
lock2.acquire()
print("%s抢到了筷子" % current_thread().name) lock1.acquire()
print("%s抢到了盘子" % current_thread().name) print("%s开吃了!" % current_thread().name) lock1.release()
print("%s放下盘子" % current_thread().name)
lock2.release()
print("%s放下筷子" % current_thread().name) if __name__ == '__main__': t1 = Thread(target=eat1) t2 = Thread(target=eat2) t1.start()
t2.start() '''
Thread-1抢到了盘子
Thread-2抢到了筷子
程序一直卡在这
'''

  

3.3 递归所(可重入锁)

Rlock  称之为递归锁或者可重入锁     Rlock不是用来解决死锁问题的

与Lock唯一的区别:
Rlock同一线程可以多次执行acquire 但是执行几次acquire就应该对应release几次 如果一个线程已经执行过acquire 其他线程将无法执行acquire
from threading import RLock, Lock, Thread

# l = Lock()
# l.acquire()
# print("1")
# l.acquire()
# print("2") l = RLock()
# l.acquire()
# print("1")
# l.acquire()
# print("2") def task():
l.acquire()
print("子run......")
l.release() # 主线程锁了一次
l.acquire()
l.acquire() l.release()
l.release()
t1 = Thread(target=task)
t1.start()

3.4 信号量 (了解)

"""
信号量 了解 可以现在被锁定的代码 同时可以被多少线程并发访问
Lock 锁住一个马桶 同时只能有一个
Semaphore 锁住一个公共厕所 同时可以来一堆人 用途: 仅用于控制并发访问 并不能防止并发修改造成的问题
""" from threading import Semaphore, Thread
import time s = Semaphore(5)
def task():
s.acquire()
print("子run")
time.sleep(3)
print("子over")
s.release() for i in range(10):
t = Thread(target=task)
t.start()

3.5 GIL锁(重点)

Global Interpreter Lock  全局解释器锁
在 Cpython中,这个全局解释器锁 或者 称为GIL,是一个互斥锁. 是为了防止多个本地线程同一时间执行python字节码,
这个锁是非常重要的因为Cpython的内存管理是非线程安全的, ,然而这个GIL有存在的必要性, 因为有很多已经存在的代码,需要依赖这个锁 非线程安全 即 多个线程访问同一个资源,会有有问题
线程安全 即 多个线程访问同一个资源,不会有问题

  

1 内存管理机制(重点)

垃圾回收机制 

python中不需要手动管理内存

引用计数 

a = 10      10地址次数计数为1

b = a          计数2

b = 1          计数1

a = 0	  计数0     

当垃圾回收启动后会将计数为0的数据清除掉,回收内存  

分代回收  

自动垃圾回收其实就是说,内部会有一个垃圾回收线程,会在某一时间运行起来,开始清理垃圾

这是可能会产生问题,例如线程1申请了内存,但是还没有使用CPU切换到了GC,GC将数据当成垃圾清理掉了 

 为了解决这个问题,Cpython就给解释器加上了互斥锁!

   

2 GIL锁的加锁与解锁时机

加锁: 只有有一个线程要使用解释器就立马枷锁  

释放:

​	该线程任务结束

​	该线程遇到IO

​	该线程使用解释器过长    默认100纳秒

  

3 解决GIL锁的方案(重点)

区分任务类型
如果是IO密集使用多线程
如果是计算密集使用多进程

4 GIL锁与自定义锁的关系

GIL保护的是解释器级别的数据安全,比如对象的引用计数,垃圾分代数据等等,具体参考垃圾回收机制详解。

都是互斥锁
为什么有了GIL还需要自己加锁
GIL是加在解释器上的,只能锁住,解释器内部的资源,但是无法锁住我们自己开启资源 例如: 我们自己开启了一个json文件,多个线程要同时访问, 如果第一个线程写入数据写到一半,执行超时了,另一个线程过来接着写,就会产生问题,
自己开启共享资源还得自己所锁 像是申请内存 保存数据等等就不需要我们程序员考虑了 GIL已经搞定了

  

四 生产者消费者模型 (重点)

概念: 模型就是解决问题的思路 产生数据一方称之为生产者 处理数据的一方称之为消费者

问题: 生产者和消费者 在处理数据速度上不一致 就会导致一方需要等待一方

解决方案

将双方分开来.一专门负责生成,一方专门负责处理

这样一来数据就不能直接交互了 双方需要一个共同的容器

生产者完成后放入容器,消费者从容器中取出数据

这样就解决了双发能力不平衡的问题,做的快的一方可以继续做,不需要等待另一方

案例

from multiprocessing import Process,Queue
import random,time def eat(q):
for i in range(1,5):
# 要消费
rose = q.get()
time.sleep(random.randint(0, 2))
print(rose,"吃完了!") # 生产任务
def make_rose(q):
for i in range(1,5):
# 再生产
time.sleep(random.randint(0, 2))
print("第%s盘青椒肉丝制作完成!" % i)
rose = "第%s盘青椒肉丝" % i
# 将生成完成的数据放入队列中
q.put(rose) if __name__ == '__main__':
# 创建一个共享队列
q = Queue()
make_p = Process(target=make_rose,args=(q,))
eat_p = Process(target=eat,args=(q,)) make_p.start()
eat_p.start()

  

五 线程(重点)

线程概念

线程是操作系统最小的运算调度单位,被包含在进程中,一个线程就是一个固定的 执行流程   

1 线程和进程的关系(重点)

1 进程:  是一个正在运行的程序, 是操作系统将数据加载到内存中去运行,那么进程就产生了,那么可以理解进程是是一个资源单位,其包含运行程序所需要的所有资源. 

2 线程:是操作系统最小运算调度单位,是被包含在进程当中的 ,任务都是由线程执行的,

3:线程是依附进程存活的,没有线程,进程的资源是无法被调动执行的 ,所以一个进程至少包含一个线程,那这个线程也称之为主线程.(主线程是由操作系统启动一个程序时,就自动会为其创建一个主线程)

4:线程也可以由程序后期取开启 ,这时开启的线程叫做子线程

2 为什么需要多线程

目的:为了提高效率  (多线程处理数据比单个线程处理速度要快的多)
开线程的的开销比开进程的开销小得多

3 如何使用线程

主要语法和进程一样

区别在进程开启子进程方法必须在main下面. 线程可以任意位置开启 因为内存中的进程时相互隔离的 必须加以判断 而线程是在进程当中的 数据可以共享的

创建线程的两种方式

第一种方法 实例化Thread类

import time
from threading import Thread,current_thread def task():
print('子线程',current_thread())
print('子线程 runing!')
time.sleep(0.1)
print('子线程over') # 第一种方法 实例化Thread类
if __name__ == '__main__':
t = Thread(target=task)
t.start()
print('主进程',current_thread())
print('主进程runing!')
time.sleep(0.1)
print('主进程over')
# current_thread() 指的时当前的线程 # 此时结果顺序是不定的 哪个线程先抢到谁就先执行 部分

  

第二种方法继承类Thread 覆盖其中的run方法

from threading import Thread,current_thread

class MyThread(Thread):
def run(self):
print(current_thread())
print('子线程runing') m =MyThread()
m.start()
print(current_thread())
print('主线程over') # 子线程和主线程开启顺序不定 谁先

  

4 线程的特点

1:线程开启的资源占用小  (与开进程做对比)

2:同一程序中   线程之间数据是共享的  

3: 线程之间没有父子之分,等级是平等的 所有线程pip都是一样的

  

from threading import Thread
from multiprocessing import Process
import time def task():
pass if __name__ == '__main__':
t_time = time.time()
for i in range(100):
p = Thread(target=task)
# p = Process(target=task)
p.start() print(time.time()-t_time)
# 进程的开销时间远远大于线程

  

5 线程的和进程的选择

属于计算密集时   多进程比较好
IO密集时 多线程比较好

6 守护线程(了解)

import random
import time
from threading import Thread def task():
print('子线程running!')
time.sleep(5)
print('子线程over!') if __name__ == '__main__': t= Thread(target=task)
t.daemon = True
# 设置守护进程 现在子线程守护主线程 当主线程挂了 子线程就不在运行了 所以主线程over之后子线程('子线程over!')这句就不走了
t.start()
print('主线程running!' )
time.sleep(random.randint(1,3))
print('主线程over!') # 运行结果
#子线程running!
# 主线程running!
# 主线程over! # 注意点 此题中如果还有其他子线程 且没有进行守护 那么主线程要等到那个子线程结束才能结束 此时守护的那个子线程再算结束

  

7 线程的安全问题

因为线程之间数据是共享的  数据共享意味着多个线程操作同一个资源  那么就有可能导致数据库的一个标错乱  那么这是可以导入线程的互斥锁   和进程的互斥锁类似

 

# 通过10次子线程将10 变成0  那么就有多个子线程操作同一个资源  防止错乱 就得加上锁  那么主线程也得先让子线程运行完采取执行 需要join

import time
from threading import Thread,Lock,enumerate
look= Lock() # 建锁
num = 10
def task():
global num
look.acquire()
a = num
time.sleep(0.1)
num = a-1
look.release() for i in range(10):
t = Thread(target=task)
t.start()
# print(enumerate()) 会显示所有线程名称
for t in enumerate()[1:]: # 返回当前活动的所有Thread对象的列表。
t.join() print(num)

  

 

7 线程队列 (了解)

1.Queue 先进先出队列

与多进程中的Queue使用方式完全相同,区别仅仅是不能被多进程共享。

 

from queue import Queue

q =  Queue(3)
q.put(1)
q.put(2)
q.put(3)
print(q.get(timeout=1))
print(q.get(timeout=1))
print(q.get(timeout=1)) # 分别打印 1 2 3

  

2.LifoQueue 后进先出队列

该队列可以模拟堆栈,实现先进后出,后进先出

from queue import LifoQueue

lq = LifoQueue()

lq.put(1)
lq.put(2)
lq.put(3) print(lq.get())
print(lq.get())
print(lq.get())
'''
3
2
1
'''

 

3.PriorityQueue 优先级队列

该队列可以为每个元素指定一个优先级,这个优先级可以是数字,字符串或其他类型,但是必须是可以比较大小的类型,取出数据时会按照从小到大的顺序取出

from queue import PriorityQueue

pq = PriorityQueue()
# 数字优先级
pq.put((10,"a"))
pq.put((11,"a"))
pq.put((-11111,"a")) print(pq.get())
print(pq.get())
print(pq.get())
# 字符串优先级
pq.put(("b","a"))
pq.put(("c","a"))
pq.put(("a","a")) print(pq.get())
print(pq.get())
print(pq.get()) '''
结果从小到大
(10, 'a')
(11, 'a')
('a', 'a')
('b', 'a')
('c', 'a'
'''

  

8 线程事件Event (重点)

什么是事件

事件表示在某个时间发生了某个事情的通知信号,用于线程间协同工作。

因为不同线程之间是独立运行的状态不可预测,所以一个线程与另一个线程间的数据是不同步的,当一个线程需要利用另一个线程的状态来确定自己的下一步操作时,就必须保持线程间数据的同步,Event就可以实现线程间同步

  

Event介绍

事件表示在某个时间发生了某个事情的通知信号,用于线程间协同工作。

因为不同线程之间是独立运行的状态不可预测,所以一个线程与另一个线程间的数据是不同步的,当一个线程需要利用另一个线程的状态来确定自己的下一步操作时,就必须保持线程间数据的同步,Event就可以实现线程间同步

  

可用方法

event.isSet():返回event的状态值;
event.wait():将阻塞线程;直到event的状态为True
event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
event.clear():恢复event的状态值为False。

  

# 每次尝试链接等待1秒,尝试次数为3次
from threading import Event,Thread
import time e = Event()
def start():
global boot
print("正正在启动服务器.....")
time.sleep(5)
print("服务器启动完成!")
e.set() def connect():
for i in range(1,4):
print("第%s次尝试链接" % i)
e.wait(1)
if e.isSet():
print("链接成功")
break
else:
print("第%s次链接失败" % i)
else:
print("服务器未启动!") Thread(target=start).start()
Thread(target=connect).start() '''
正正在启动服务器.....
第1次尝试链接
第1次链接失败
第2次尝试链接
第2次链接失败
第3次尝试链接
第3次链接失败
服务器未启动!
服务器启动完成!
'''

  

9 epoll 使用

注意点

epoll只适用于 Unix/Linux操作系统

产生背景

select监控多个socket时会产生等待队列  另外会遍历所有socket

  

epoll解决的问题

1.避免频繁的对等待队列进行操作 

2.避免遍历所有socket

  

所以对于第一个问题epoll,采取的方案是,将对等待队列的维护和,阻塞进程这两个操作进行拆分,

相关代码如下

import socket,select
server = socket.socket()
server.bind(("127.0.0.1",1688))
server.listen(5) #创建epoll事件对象,后续要监控的事件添加到其中
epoll = select.epoll()
#注册服务器监听fd到等待读事件集合
epoll.register(server.fileno(), select.EPOLLIN) # 需要关注 server这个socket的可读事件 # 等待事件发生
while True:
for sock,event in epoll.poll():
pass

  

epoll相关函数

import select # 导入select模块

epoll = select.epoll() # 创建一个epoll对象

epoll.register(文件句柄,事件类型) # 注册要监控的文件句柄和事件

事件类型:

  select.EPOLLIN    可读事件

  select.EPOLLOUT   可写事件

  select.EPOLLERR   错误事件

  select.EPOLLHUP   客户端断开事件

epoll.unregister(文件句柄)   销毁文件句柄

epoll.poll(timeout)  当文件句柄发生变化,则会以列表的形式主动报告给用户进程,timeout

                     为超时时间,默认为-1,即一直等待直到文件句柄发生变化,如果指定为1

                     那么epoll每1秒汇报一次当前文件句柄的变化情况,如果无变化则返回空

epoll.fileno() 返回epoll的控制文件描述符(Return the epoll control file descriptor)

epoll.modfiy(fineno,event) fineno为文件描述符 event为事件类型  作用是修改文件描述符所对应的事件

epoll.fromfd(fileno) 从1个指定的文件描述符创建1个epoll对象

epoll.close()   关闭epoll对象的控制文件描述符

  

案例

#coding:utf-8
#客户端
#创建客户端socket对象
import socket
clientsocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#服务端IP地址和端口号元组
server_address = ('127.0.0.1',1688)
#客户端连接指定的IP地址和端口号
clientsocket.connect(server_address) while True:
#输入数据
data = raw_input('please input:')
if data == "q":
break
if not data:
continue
#客户端发送数据
clientsocket.send(data.encode("utf-8"))
#客户端接收数据
server_data = clientsocket.recv(1024)
print ('客户端收到的数据:',server_data)
#关闭客户端socket
clientsocket.close()

  

 

服务器:

# coding:utf-8
import socket, select server = socket.socket()
server.bind(("127.0.0.1", 1688))
server.listen(5) msgs = [] fd_socket = {server.fileno(): server}
epoll = select.epoll()
# 注册服务器的 写就绪
epoll.register(server.fileno(), select.EPOLLIN) while True:
for fd, event in epoll.poll():
sock = fd_socket[fd]
print(fd, event)
# 返回的是文件描述符 需要获取对应socket
if sock == server: # 如果是服务器 就接受请求
client, addr = server.accept()
# 注册客户端写就绪
epoll.register(client.fileno(), select.EPOLLIN)
# 添加对应关系
fd_socket[client.fileno()] = client # 读就绪
elif event == select.EPOLLIN:
data = sock.recv(2018)
if not data:
# 注销事件
epoll.unregister(fd)
# 关闭socket
sock.close()
# 删除socket对应关系
del fd_socket[fd]
print(" somebody fuck out...")
continue print(data.decode("utf-8"))
# 读完数据 需要把数据发回去所以接下来更改为写就绪=事件
epoll.modify(fd, select.EPOLLOUT)
#记录数据
msgs.append((sock,data.upper()))
elif event == select.EPOLLOUT:
for item in msgs[:]:
if item[0] == sock:
sock.send(item[1])
msgs.remove(item)
# 切换关注事件为写就绪
epoll.modify(fd,select.EPOLLIN)

 

六 线程池进程池(重点)

池就是容器 本质上就是一个存储进程或线程的列表  

6.1 线程池 

好处

1.自动管理线程的开启和销毁
2.自动分配任务给空闲的线程
3.可以线程开启线程的数量 保证系统稳定
信号量中是限制同时并发多少,但是线程已经全都建完了

  

6.2为什么需要进程/线程池?

在很多情况下需要控制进程或线程的数量在一个合理的范围,例如TCP程序中,一个客户端对应一个线程,虽然线程的开销小,但肯定不能无限的开,否则系统资源迟早被耗尽,解决的办法就是控制线程的数量。

线程/进程池不仅帮我们控制线程/进程的数量,还帮我们完成了线程/进程的创建,销毁,以及任务的分配

  

6.3 进程池线程池的使用(重点)

1.创建池子

2.submit 提交任务 

3.pool.shutdown() # 等待所有任务全部完毕 销毁所有线程 后关闭线程池

​	关闭后就不能提交新任务了

 

进程池

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
import time,os # 创建进程池,指定最大进程数为3,此时不会创建进程,不指定数量时,默认为CPU和核数
pool = ProcessPoolExecutor(3) def task():
time.sleep(1)
print(os.getpid(),"working..") if __name__ == '__main__':
for i in range(10):
pool.submit(task) # 提交任务时立即创建进程 # 任务执行完成后也不会立即销毁进程
time.sleep(2) for i in range(10):
pool.submit(task) #再有新任务是 直接使用之前已经创建好的进程来执行

 

线程池

首先要明确,TCP是IO密集型,应该使用线程池

 

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
from threading import current_thread,active_count
import time,os # 创建线程池,指定最大线程数为3,此时不会创建线程,不指定数量时,默认为CPU核数*5
pool = ThreadPoolExecutor(3)
print(active_count()) # 只有一个主线 def task():
time.sleep(1)
print(current_thread().name,"working..") if __name__ == '__main__':
for i in range(10):
pool.submit(task) # 第一次提交任务时立即创建线程 # 任务执行完成后也不会立即销毁
time.sleep(2) for i in range(10):
pool.submit(task) #再有新任务时 直接使用之前已经创建好的线程来执行

  

 

七 同步 异步 阻塞非阻塞

同步-异步 指的是提交任务的方式

同步指调用:发起任务后必须在原地等待任务执行完成,才能继续执行

异步指调用:发起任务后必须不用等待任务执行,可以立即开启执行其他操作

同步会有等待的效果但是这和阻塞是完全不同的,阻塞时程序会被剥夺CPU执行权,而同步调用则不会!

  

阻塞非阻塞 指的是程序的运行状态

阻塞:当程序执行过程中遇到了IO操作,在执行IO操作时,程序无法继续执行其他代码,称为阻塞!

非阻塞:程序在正常运行没有遇到IO操作,或者通过某种方式使程序即时遇到了也不会停在原地,还可以执行其他操作,以提高CPU的占用率

  

同步会有等待的效果但是这和阻塞是完全不同的,阻塞时程序会被剥夺CPU执行权,而同步调用则不会!

很明显异步调用效率更高,但是任务的执行结果如何获取呢

实现异步调用的方式一

from concurrent.futures import ThreadPoolExecutor
from threading import current_thread
import time pool = ThreadPoolExecutor(3)
def task(i):
time.sleep(0.01)
print(current_thread().name,"working..")
return i ** i if __name__ == '__main__':
objs = []
for i in range(3):
res_obj = pool.submit(task,i) # 异步方式提交任务# 会返回一个对象用于表示任务结果
objs.append(res_obj) # 该函数默认是阻塞的 会等待池子中所有任务执行结束后执行
pool.shutdown(wait=True) # 从结果对象中取出执行结果
for res_obj in objs:
print(res_obj.result())
print("over") '''
ThreadPoolExecutor-0_1ThreadPoolExecutor-0_2 working..
ThreadPoolExecutor-0_0 working..
working..
1
1
4
over
前面三行顺序是不定的
'''

  

实现异步调用的方式二

from concurrent.futures import ThreadPoolExecutor
from threading import current_thread
import time pool = ThreadPoolExecutor(3)
def task(i):
time.sleep(0.01)
print(current_thread().name,"working..")
return i ** i if __name__ == '__main__':
objs = []
for i in range(3):
res_obj = pool.submit(task,i) # 会返回一个对象用于表示任务结果
print(res_obj.result()) #result是同步的一旦调用就必须等待 任务执行完成拿到结果
print("over")

 

八 异步回调(重要)

什么是异步回调

指的是再发起一个异步任务的同时指定一个函数   在异步任务完成时会自动调用这个函数 并且会传入Future对象 ,通过Future对象的result()获取执行结果

 

为什么需要异步

之前在使用线程池或进程池提交任务时,如果想要处理任务的执行结果则必须调用result函数或是shutdown函数,而它们都是是阻塞的,会等到任务执行完毕后才能继续执行,这样一来在这个等待过程中就无法执行其他任务,降低了效率,所以需要一种方案,即保证解析结果的线程不用等待,又能保证数据能够及时被解析,该方案就是异步回调

  

异步回调的使用

先来看一个案例:
在编写爬虫程序时,通常都是两个步骤:
​ 1.从服务器下载一个网页文件
​ 2.读取并且解析文件内容,提取有用的数据 按照以上流程可以编写一个简单的爬虫程序
要请求网页数据则需要使用到第三方的请求库requests可以通过pip或是pycharm来安装,在pycharm中点击settings->解释器->点击+号->搜索requests->安装

  

import requests,re,os,random,time
from concurrent.futures import ProcessPoolExecutor def get_data(url):
print("%s 正在请求%s" % (os.getpid(),url))
time.sleep(random.randint(1,2))
response = requests.get(url)
print(os.getpid(),"请求成功 数据长度",len(response.content))
#parser(response) # 3.直接调用解析方法 哪个进程请求完成就那个进程解析数据 强行使两个操作耦合到一起了
return response def parser(obj):
data = obj.result()
htm = data.content.decode("utf-8")
ls = re.findall("href=.*?com",htm)
print(os.getpid(),"解析成功",len(ls),"个链接") if __name__ == '__main__':
pool = ProcessPoolExecutor(3)
urls = ["https://www.baidu.com",
"https://www.sina.com",
"https://www.python.org",
"https://www.tmall.com",
"https://www.mysql.com",
"https://www.apple.com.cn"]
# objs = []
for url in urls:
# res = pool.submit(get_data,url).result() # 1.同步的方式获取结果 将导致所有请求任务不能并发
# parser(res) obj = pool.submit(get_data,url) #
obj.add_done_callback(parser) # 4.使用异步回调,保证了数据可以被及时处理,并且请求和解析解开了耦合
# objs.append(obj) # pool.shutdown() # 2.等待所有任务执行结束在统一的解析
# for obj in objs:
# res = obj.result()
# parser(res)
# 1.请求任务可以并发 但是结果不能被及时解析 必须等所有请求完成才能解析
# 2.解析任务变成了串行,

  

总结:异步回调使用方法就是在提交任务后得到一个Futures对象,调用对象的add_done_callback来指定一个回调函数,

如果把任务比喻为烧水,没有回调时就只能守着水壶等待水开,有了回调相当于换了一个会响的水壶,烧水期间可用作其他的事情,等待水开了水壶会自动发出声音,这时候再回来处理。水壶自动发出声音就是回调。

注意:

1. 使用进程池时,回调函数都是主进程中执行执行
2. 使用线程池时,回调函数的执行线程是不确定的,哪个线程空闲就交给哪个线程
3. 回调函数默认接收一个参数就是这个任务对象自己,再通过对象的result函数来获取任务的处理结果

  

九 协程

1 协程定义

协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。

 

2 协程优缺点

优点

#1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
#2. 单线程内就可以实现并发的效果,最大限度地利用cpu
# 协程都是异步任务

缺点

#1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程来尽可能提高效率
#2. 协程本质是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

  

3 gevent使用

gevent能够实现协程  既可以检测IO 又能实现单线程并发

下载模块pip install gevent

  

  

## 协程的使用

​	gevent   需要自己安装  

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

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

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

  

import gevent

def f1():
for i in range(3):
print ('run func: f1, index: %s ' % i)
gevent.sleep(0) def f2():
for i in range(3):
print ('run func: f2, index: %s ' % i)
gevent.sleep(0) t1 = gevent.spawn(f1)
t2 = gevent.spawn(f2)
gevent.joinall([t1, t2])

 

run func: f1, index: 0
run func: f2, index: 0
run func: f1, index: 1
run func: f2, index: 1
run func: f1, index: 2
run func: f2, index: 2 f1和f2是交叉打印信息的,因为在代码执行的过程中,我们人为使用gevent.sleep(0)创建了一个阻塞,gevent在运行到这里时就会自动切换函数切换函数。也可以在执行的时候sleep更长时间,可以发现两个函数基本是同时运行然后各自等待。 在实际运用的过程中,我们如果有需要通过人为sleep来增加时间间隔或者确保部分逻辑安全的时候,此处使用就很方便了。当然,更多时候我们还是在需要进行网络请求的时候使用gevent:

  

 

需要注意:

1.如果主线程结束了 协程任务也会立即结束。

2.monkey补丁的原理是把原始的阻塞方法替换为修改后的非阻塞方法,即偷梁换柱,来实现IO自动切换

​	必须在打补丁后再使用相应的功能,避免忘记,建议写在最上方

 

4 猴子补丁

本质就是把原本阻塞的代码 悄悄换成非阻塞代码  

例如 Queue.get(block=false)

​	当执行get而取不到值时 会抛出异常  只需捕获异常  然后在发生时切换到其他任务 

​	就可以实现遇到IO 切换任务

5 协程的使用场景

IO密集型任务的问题

  

 

十 解决高并发的方案(重点)

1 cnd加速

一般大公司用

简单说就是把静态资源放到别人服务器上

全称:Content Delivery Network或Content Ddistribute Network,即内容分发网络

基本思路:

尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。

目的:

解决因分布、带宽、服务器性能带来的访问延迟问题,适用于站点加速、点播、直播等场景。使用户可就近取得所需内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度和成功率。

控制时延无疑是现代信息科技的重要指标,CDN的意图就是尽可能的减少资源在转发、传输、链路抖动等情况下顺利保障信息的连贯性。

CDN就是扮演者护航者和加速者的角色,更快准狠的触发信息和触达每一个用户,带来更为极致的使用体验。

  

2 集群化部署

例如:django+uwsgi+nginx
详细部署内容请点击这里:https://www.cnblogs.com/alex3714/p/6538374.html

 

3 后台使用Mysql+Redis

mysql是持久化存储,存放在磁盘里面,检索的话,会涉及到一定的IO,为了解决这个瓶颈,于是出现了缓存,比如现在常用的 redis。首先,用户访问缓存,如果未命中,就去访问mysql,之后将mysql中的数据复制到缓存中。

redis是缓存,并且是驻留在内存中运行的,这大大提升了高数据量web访问的访问速度。redis提供了大量的数据结构,比如string、list、set、hashset、sorted set这些,之后用户的访问就能直接从Redis的内存中去取数据了,那内存的读取速度远远大于硬盘

 

4 数据库的优化

1.sql的优化

2.索引的优化

3.分库分表

4.读写分离

   

5 使用缓存或者中间件

redis  可以用于缓存 常用的轻量的放到redis中
rabbitmq 消息中间件

6 分布式+异步

celery:就是一个分布式异步的解决方案
具体参考博客 https://www.cnblogs.com/alex3714/p/6351797.html

 

7 语言层面

例如:在一些并发量更多的地方,该用golang语言编写

  

 

  

 

上一篇:守护、互斥锁、IPC和生产者消费者模型


下一篇:yii_wiki_145_yii-cjuidialog-for-create-new-model (通过CJuiDialog来创建新的Model)