一、操作系统介绍
1.1操作系统的功能
什么是操作系统——操作系统就是一个协调、管理和控制计算机硬件资源和软件资源的控制程序
- 封装了复杂的操作硬件的接口,提供给应用程序使用
- 管理CPU上运行的多个应用程序进程,使多个进程对硬件的竞争变得有序
1.2操作系统发展史
第一代计算机(1940~1955):真空管和穿孔卡片
程序员预约排队,每人固定时间独享计算机
第二代计算机(1955~1965):晶体管和批处理系统
程序员的程序批量进行输入、计算、输出,依旧是串行进程
1401机负责输入输出,7094机负责计算
第三代计算机(1965~1980):集成电路芯片和多道程序设计
多道技术:
多道技术中的多道指的是多个程序,多道技术的实现是为了解决多个程序竞争或者说共享同一个资源(比如cpu)的有序调度问题,解决方式即多路复用,多路复用分为时间上的复用和空间上的复用
通过在A程序进行I/O时的间隙去解决B程序的计算,最大化CPU效率,核心在于切之前将进程的状态保存下来,这样才能保证下次切换回来时,能基于上次切走的位置继续运行
- 空间上的复用
将内存分为几部分,每个部分放入一个程序,这样,同一时间内存中就有了多道程序(需要物理层面的对内存进行分区)
- 时间上的复用(复用一个cpu的时间片)
当一个程序在等待I/O时(或者执行时间过长),CPU切换出去,另一个程序可以使用cpu,如果内存中可以同时存放足够多的作业,则cpu的利用率可以接近100%
分时操作系统
本质——多个联机终端+多道技术
由于第一代操作系统程序员可以独享计算机,基于CTTS(内存物理分区保护),通过计算机联机多个终端供多个程序员使用,计算机CPU采用多道技术来回切换,使每个终端程序员都能够感受到独享计算机的体验。
第四代计算机(1980~至今):个人计算机
二、并发编程之多进程
1、进程理论
1.1.什么是进程
进程:正在进行的一个过程或者说一个任务。而负责执行任务则是cpu
1.2.进程与程序的区别
程序仅仅只是一堆代码而已,而进程指的是程序的运行过程
需要强调的是:同一个程序执行两次,那也是两个进程,比如打开暴风影音,虽然都是同一个软件,但是一个可以播放苍井空,一个可以播放饭岛爱
1.3.并发与并行
- 并发:是伪并行,即看起来是同时运行。单个cpu+多道技术就可以实现并发
- 并行:同时运行,只有具备多个cpu才能实现并行
1.4.进程的创建
- 系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)
- 一个进程在运行过程中开启了子进程(如nginx开启多进程,os.fork,subprocess.Popen等)
- 用户的交互式请求,而创建一个新进程(如用户双击暴风影音)
- 一个批处理作业的初始化(只在大型机的批处理系统中应用)
1.5.进程的状态
- 运行——应用程序正在被CPU执行的过程
- 阻塞——由于应用程序在I/O过程或者CPU占用时间过长而被操作系统夺回CPU权限
- 就绪——应用程序I/O完成或者被夺回CPU权限后的等待过程,随时可能被CPU执行
2、开启子进程的两种方式
2.1.multiprocessing模块(from multiprocessing import Process)
multiprocessing模块用来开启子进程,并在子进程中执行我们定制的任务(比如函数)
Process类(创建进程的类)
Process类是multiprocessing模块下的一个功能
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__":
# obj = Process(target=task,args = ("子进程1",)) 必须加逗号形成元组
obj = Process(target=task,kwargs = {"name":"子进程1"})
obj.start()
print("主进程")
输出:
主进程
子进程1 is running
子进程1 is done
- Process实例化的时候target等于你需要执行子进程的函数名称(不加括号执行),args是使用元组的形式给子进程传参(必须要加逗号),kwargs是以字典的形式给子进程传参(关键参数)
- 当obj.start()时是父进程告诉操作系统开始执行子进程,后续的速度由操作系统管理,但紧接着几乎是同时执行了print(“主进程”)代码,子进程还未来得及执行,所以会在主进程之后再执行
- 注意:在windows中Process()必须放到# if _ name_ == ‘_ main_’:下
2.2.通过继承Process类自行改写(必须实现run方法)
from multiprocessing import Process
import time
class Myprocess(Process):
def __init__(self,name):
super().__init__() #Process父类中的__init__可能有很多代码,直接继承
self.name = name
def run(self): #需要执行的子进程必须要有run这个方法
print("%s is running" % self.name)
time.sleep(3)
print("%s is done" % self.name)
if __name__ == "__main__":
obj = Myprocess("子进程")
obj.start() #本质上会调用叫run的方法
print("主进程")
输出:
主进程
子进程1 is running
子进程1 is done
2.3.查询子进程ID(pid)
查询子进程process id——os.getpid()
查询父进程——os.getppid()
from multiprocessing import Process
import time,os
def task():
print('%s is running,parent id is <%s>' %(os.getpid(),os.getppid()))
time.sleep(3)
print('%s is done,parent id is <%s>' %(os.getpid(),os.getppid()))
if __name__ == '__main__':
p=Process(target=task,)
p.start()
print('主',os.getpid(),os.getppid())
输出
主 16536 3196
9304 is running,parent id is <16536>
9304 is done,parent id is <16536>
- 主进程的父进程是pycharm应用程序的进程
- cmd中查询进程id命令:tasklist | findstr pycharm
2.4.僵尸进程与孤儿进程
- 僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
- 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
注意:任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理;孤儿进程在父进程死后会自动由init进程托管,循环清除孤儿进程的数据
2.5.Process对象的其他属性和方法
p.join——让主进程阻塞,等待子进程完成
from multiprocessing import Process
import time
def task(name):
print("%s is running"%name)
time.sleep(2)
if __name__ == "__main__":
p = Process(target=task,args=("子进程",))
p.start()
p.join()
print("主程序结束")
输出
主程序结束
子进程 is running
- 如果将p.join()注释掉,那主程序先会打印主程序结束,再打印子进程 is running,因为子进程要sleep两秒主进程已经结束了
注意:p.join只会让主进程等待,当同时有其他子进程运行时,其他子进程并不会等待
例如:
def task(name,n):
print("%s is running"%name)
time.sleep(n)
if __name__ == "__main__":
start_time = time.time()
p1 = Process(target=task,args=("子进程1",3))
p2 = Process(target=task,args=("子进程2",2))
p3 = Process(target=task,args=("子进程3",1))
p1.start()
p2.start()
p3.start()
p1.join()
p2.join()
p3.join()
print("主程序结束,等待%s"%(time.time()-start_time))
输出
子进程1 is running
子进程2 is running
子进程3 is running
主程序结束,等待3.268291711807251
- 等待时长并不等于3+2+1,因为p1.join的期间p2和p3实际已经运行完成,所以等待时长应该等于最大用时进程的时长
- 多个进程开启可以用for循环打开
p_list=[p1,p2,p3,p4]
for p in p_list:
p.start()
for p in p_l:
p.join()
p.is_alive 判断进程是否还在运行
p.terminate 强制终止进程p(但不会清除进程数据)
def task(name):
print("%s is running"%name)
if __name__ == "__main__":
start_time = time.time()
p = Process(target=task,args=("子进程",))
p.start()
print(p.is_alive()) #第一次判断
p.terminate()
print(p.is_alive()) #第二次判断
输出
True
True
- 第一次判断程序刚启动,为True
- 第二次判断由于刚刚执行了p.terminate(只是给操作系统发出终止指令),操作系统还没来得及关闭子进程,就执行了第二次判断返回True,如果此时等待2秒,则操作系统已终止子进程,返回False
p.pid——功能等于os.getpid()
p.name——给进程命名
def task(name):
pass
if __name__ == "__main__":
start_time = time.time()
p = Process(target=task,name="Myprocess",args=("子进程",)) #在实例化Process类时给到name参数
p.start()
print(p.name,p.pid)
2.6.基于多进程实现并发套接字通信
服务端代码:
def talk(conn): #需要传conn参数
while True:
data = conn.recv(1024).decode("utf-8")
conn.send(data.upper().encode("utf-8"))
conn.close()
def run_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 9999))
server.listen(5)
while True:
conn, addr = server.accept()
p = Process(target=talk,args=(conn,)) #生成一个子进程处理
p.start()
server.close()
if __name__ == "__main__":
run_server()
客户端代码:
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",9999))
while True:
msg = input(">>>")
client.send(msg.encode("utf-8"))
server_response = client.recv(1024).decode("utf-8")
print(server_response)
3、守护进程(p.daemon = True)
应用场景——当主程序结束后不需要子进程时,就应该将子进程设置为守护进程
- 特点一:守护进程会在主进程代码执行结束后就终止
- 特点二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children
def task(name):
print("%s is running"%name)
time.sleep(2)
if __name__ == "__main__":
p = Process(target=task,args="子进程")
p.daemon = True
p.start()
print("主进程结束")
输出
主进程结束
- p.daemon = True设置p为守护进程
- 主进程打印“主进程结束”时,由于速度过快就已经结束子进程,所以还没有执行子进程中的打印任务
4、互斥锁
- 虽然多个进程有自己独立的内存空间,但是它们共享一个打印终端,随着进程执行速度的不同,会在打印端错乱,这时就要加互斥锁
- 互斥锁的原理,就是把并发改成串行,降低了效率,但保证了数据安全不错乱
- 互斥锁可以理解为多人上厕所,一个人进去后就会上锁,解锁后下一个继续
from multiprocessing import Process,Lock
import os,time
def task1(mutex):
mutex.acquire()
print("进程1")
mutex.release()
def task2(mutex):
mutex.acquire()
print("进程2")
mutex.release()
if __name__ == "__main__":
mutex = Lock()
p1 = Process(target=task1,args=(mutex,))
p2 = Process(target=task2,args=(mutex,))
p1.start()
p2.start()
输出
进程1
进程2
- 需要导入multiprocessing 模块下的Lock对象
- 主程序要实例化一个Lock对象
- 加锁mutex.acquire(),解锁mutex.release()
加锁解锁可以用with mutex:简写
with lock: #相当于lock.acquire(),执行完自代码块自动执行lock.release()
fun()
互斥锁与join的区别:
- join是完全将子进程进行串行运行
- 互斥锁可以在子进程中间加,类似于抢火车票,查票是各个子进程并发进行的,而购票就要加锁串行进行,又例如读取文件可以并发,但是写操作要串行
IPC(Intel Process Communication)
5.队列
5.1 什么是队列?——Queue类
互斥锁的缺点:
- 效率低(共享数据基于文件,而文件是硬盘上的数据)
- 需要自己加锁处理
为了解决这一问题,队列和管道都是将数据存放于内存中,而队列又是基于(管道+锁)实现的
创建Queue类:
Queue([maxsize]):创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递
需要注意:
1、队列内存放的是消息而非大数据(文件)
2、队列占用的是内存空间,不填写maxsize即便是无大小限制也受限于内存大小
from multiprocessing import Queue
p = Queue(3)
p.put({"name":"alex"})
p.put([1,2,3])
p.put("Hello")
print(p.full())
print(p.get())
print(p.get())
print(p.get())
print(p.empty())
输出
True
{'name': 'alex'}
[1, 2, 3]
Hello
True
- Queue(3)实例化,3是最大队列数,不填写默认无限制
- p.put将数据放进管道,当超过3次程序会由于锁卡住
- p.get将数据取出管道,当超过3次程序会由于锁卡住
- p.full()判断管道是否满,p.empty()判断管道是否为空
6.生产者消费者模型
应用场景
程序中有两类角色:
- 一类负责生产数据(生产者)
- 一类负责处理数据(消费者)
优点 - 平衡生产者与消费者之间的速度差
- 程序解开耦合(生产者与消费者之间通过管道连接)
def producer(name,p):
for i in range(3):
time.sleep(1)
food = "%s%s"%(name,i)
print("%s店生产了%s%s"%(name,name,i))
p.put(food)
def cosumer(name,p):
while True:
time.sleep(2)
food = p.get()
if food == None:
break
print("%s吃了%s"%(name,food))
if __name__ == "__main__":
p = Queue()
p1 = Process(target=producer,args=("包子",p,)) #创建生产者1
p2 = Process(target=producer,args=("面包",p,)) #创建生产者2
c1 = Process(target=cosumer,args=("张三",p,)) #创建消费者1
c2 = Process(target=cosumer,args=("李四",p,)) #创建消费者2
p1.start()
p2.start()
c1.start()
c2.start()
p1.join() #等待生产者生产完成
p2.join()
p.put(None) #生产者结束后传None进管道(等于结束信号),消费者收到后结束进程
p.put(None) #因为有2个消费者,所以要传2次None
print("主程序结束")
输出
包子店生产了包子0
面包店生产了面包0
包子店生产了包子1
面包店生产了面包1
张三吃了包子0
李四吃了面包0
包子店生产了包子2
面包店生产了面包2
主程序结束
张三吃了包子1
李四吃了面包1
张三吃了包子2
李四吃了面包2
7.JoinableQueue
上述代码需要主动put(None)告诉管道没有数据传进来了,可以用JoinableQueue配合守护进程来简化
def producer(name,q):
for i in range(3):
time.sleep(1)
food = "%s%s"%(name,i)
print("%s店生产了%s%s"%(name,name,i))
q.put(food)
q.join() #生产完后等待消费者处理完所有管道数据结束
def cosumer(name,q):
while True:
time.sleep(2)
food = q.get()
print("%s吃了%s"%(name,food))
q.task_done() #通知生产者数据处理完成
if __name__ == "__main__":
q = JoinableQueue() #实例化JoinableQueue对象
p1 = Process(target=producer,args=("包子",q,))
p2 = Process(target=producer,args=("面包",q,))
c1 = Process(target=cosumer,args=("张三",q,))
c2 = Process(target=cosumer,args=("李四",q,))
c1.daemon = True #设置成守护进程,随着主程序结束死掉
c2.daemon = True
p1.start()
p2.start()
c1.start()
c2.start()
p1.join()
p2.join()
print("主程序结束") #由于主程序等待p1和p2结束,那么p1,p2结束的条件是q.join完成(也就是消费者数据处理结束),那么可以将消费者作为守护进程随着主进程一起死掉
- q.task_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常
- q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止
三、并发编程之多线程
1、线程理论
操作系统比喻成一个公司,进程就是各个部门(财务/销售),线程就是部门内的员工(销售部门有线上/线下)
特点:
- 每启动一个进程,进程内至少有一个线程
- 进程本身只是一个资源单位,本身并不能运行,进程内开的线程才是真正的运行单位
- 一个进程内可以开启多个线程,并且多个线程共享开发数据,但跨进程的线程不能共享数据
- 启动一个进程的资源开销大于启动一个线程
2、开启线程的两种方式
2.1 threading模块Thread类
from threading import Thread
import time
def task(name):
time.sleep(1)
print("%s is running"%name)
if __name__ == "__main__":
t = Thread(target=task,args=("线程1",))
t.start()
print("主线程(属于主进程)")
输出
主线程(属于主进程)
线程1 is running
- 注意:该py文件一旦打开就开启了一个进程一个线程,t.start()又开启了一个线程,所以总共有1个进程,2个线程
2.2 通过继承Thread类自行改写(必须实现run方法)
class Mythread(Thread):
def __init__(self,name):
super().__init__()
self.name = name
def run(self):
time.sleep(1)
print("%s is running"%self.name)
# if __name__ == "__main__":
t = Mythread("线程1",)
t.start()
print("主线程(属于主进程)")
3、进程与线程的区别
- 开进程的开销远大于开线程
线程的t.start()要比进程的p.start快很多,因为进程开始需要向系统申请内存空间 - 进程之间有独立的内存空间,而线程共享一个内存空间
- 进程可以利用多核优势,多个进程同时进行;线程同一时间由于GIL限制只能执行一个线程,属于并发
进程演示:
n = 100
def task(name,no):
global n
n = no #进程修改了自己内存空间里的全局变量,不影响主进程
print(name,n,id(n))
if __name__ == "__main__":
p1 = Process(target=task,args=("进程1",1))
p2 = Process(target=task,args=("进程2",2))
p1.start()
p2.start()
print("主进程",n,id(n))
输出
主进程 100 1602735584
进程1 1 1602734000
进程2 2 1602734016
线程演示:
n = 100
def task(name):
global n
n = 1 #线程修改全局变量会影响主进程的n
if __name__ == "__main__":
t1 = Thread(target=task,args=("线程1",))
t1.start()
print("主进程",n)
输出
主进程 1
- pid
进程之间的pid不同,而线程都属于一个进程内,所以它们的pid都相同
def task():
print("线程1:%s"%os.getpid())
if __name__ == "__main__":
t1 = Thread(target=task)
t1.start()
print("主线程:%s"%os.getpid())
输出
线程1:16588
主线程:16588
4、Thread对象的其他属性或方法
4.1 threading模块提供的一些方法:
-
threading.currentThread(): 返回当前的线程变量
其实t = Thread()实例化的时候赋值给了t,所以t = currentThread() -
threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
-
threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果
from threading import Thread,currentThread,enumerate,activeCount
import time
def task():
time.sleep(1)
print("线程1 is running")
if __name__ == "__main__":
t1 = Thread(target=task)
t1.start()
print(enumerate()) #打印当前运行线程列表
print(activeCount()) #打印当前运行线程数量
t1.join() #等待t1线程结束
print("主线程")
输出
[<_MainThread(MainThread, started 9012)>, <Thread(Thread-1, started 15484)>]
2
线程1 is running
主线程
4.2 Thread实例对象的方法
- isAlive(): 返回线程是否活动的。
- getName(): 返回线程名。
- setName(): 设置线程名。
def task():
time.sleep(1)
print("线程1:%s"%currentThread().getName()) #获得当前线程的名字
if __name__ == "__main__":
t1 = Thread(target=task)
t1.start()
#t1.setName("新的线程名") #修改t1这个线程的名字
print("主线程:%s"%currentThread().getName())
输出
主线程:MainThread #主线程叫这个
线程1:Thread-1 #其他线程都是平行关系,没有父子之分,用数字命名
5、守护线程
一个进程开启就会开启一个主线程,主线程的结束代表着进程结束,所以主线程结束后会等待其他非守护线程结束才会停止
守护线程会在主线程结束时立即结束
def task1():
time.sleep(1)
print("线程1 is running")
def task2():
time.sleep(2)
print("线程2 is running")
if __name__ == "__main__":
t1 = Thread(target=task1)
t2 = Thread(target=task2)
t2.daemon = True
t1.start()
t2.start()
print("主线程")
输出
主线程
线程1 is running
- 主线程结束后等待t1结束,t1等待1秒后主线程结束,守护线程t2随即结束,所以不会打印线程2 is running
6、线程互斥锁
from threading import Thread,Lock #导入Lock
import time
n = 100
def task():
global n
#mutex.acquire() #在修改n之前加上锁
temp = n #记录好n的值
time.sleep(1)
n = temp - 1
#mutex.release() #修改完解锁
if __name__ == '__main__':
mutex = Lock() #创建互斥锁
t_l = []
for i in range(100):
t = Thread(target=task)
t_l.append(t)
t.start()
for t in t_l:
t.join()
print("全局变量n等于:",n)
输出
全局变量n等于: 99
- for循环100个线程后,都停留在睡0.1秒时间内,只记录了n的值为temp(值为100)
- 在0.1秒后都进行减法,都是temp - 1就是100-1=99,所以输出n等于99
- 要解除竞争数据的情况就加上mutex锁,改并发为串行,但确保数据准确
7、GIL(global interpreter lock解释器锁)
7.1 GIL概念
- python是由C语言编写的Cpython解释器
- 打开一个py文件就是打开一个进程,会创建一个内存空间,存进去py文件代码还有Cpython解释器的代码
- 这个进程中包含多个进程以及python垃圾回收线程,这些线程都需要输入到Cpython解释器代码中执行
- 所以对于解释器来说同一时间只能处理一个线程,无法利用多核优势(多进程可以并行运行)
7.2 GIL与自定义Lock
- GIL的锁只是保护解释器,防止类似垃圾回收线程与自己的线程争抢,导致数据不安全
- 针对自己要保护的数据,需要自己加锁
多线程执行的步骤:
- 第一步:线程1抢到GIL锁,将自己的代码导入解释器执行
- 第二步:线程1抢到自定义Lock锁,但是遇到time.sleep阻塞,交出GIL锁
- 第三步:线程2抢到GIL锁,执行到自定义锁时发现在线程1那儿,阻塞住交出GIL锁
- 第四步:线程1又抢到GIL锁,也有Lock锁,继续执行代码完成,释放Lock锁,释放GIL锁
- 第五步:线程2又抢到GIL锁,也获得Lock锁,继续执行代码
总结:GIL锁只是保护解释器的,Lock锁时用户保护自己数据的
7.3 GIL与多线程
选择多进程还是多线程?
程序分为计算密集型和I/O密集型
-计算密集型推荐使用多进程,利用多核优势,并行执行程序提高效率(CPU的功能就是运算)
- I/O密集型推荐使用多线程,因为多数时间在等待I/O,CPU运算量不大,避免多进程的内存开销
计算密集型程序:用多进程(如金融分析)
def task():
res = 0
for i in range(100000000):
res *= i
if __name__ == '__main__':
#print(os.cpu_count()) #查询本机CPU数,本机为8核
p_list = []
start_time = time.time() #记录开始时间
for i in range(8):
# p = Process(target=task) #耗时15.8秒
p = Thread(target=task) #耗时75.2秒
p_list.append(p)
p.start()
for p in p_list:
p.join()
end_time = time.time() #记录结束时间
used_time = end_time - start_time
print("耗时%s"%used_time)
I/O密集型程序:用多线程如(socket,爬虫,web)
def task():
time.sleep(2)
if __name__ == '__main__':
#print(os.cpu_count()) #查询本机CPU数,本机为8核
p_list = []
start_time = time.time() #记录开始时间
for i in range(400):
# p = Process(target=task) #耗时20.6秒,多耗在创建新进程上
p = Thread(target=task) #耗时2.08秒
p_list.append(p)
p.start()
for p in p_list:
p.join()
end_time = time.time() #记录结束时间
used_time = end_time - start_time
print("耗时%s"%used_time)
8、死锁与递归锁
8.1 死锁现象
指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象
mutexA = Lock()
mutexB = Lock()
def task(name):
#先抢A锁
mutexA.acquire()
print("%s抢到A锁了"%name)
#再抢B锁
mutexB.acquire()
print("%s抢到B锁了"%name)
#释放B锁
mutexB.release()
#释放A锁
mutexA.release()
"""再来一次相反的顺序"""
#先抢B锁
mutexB.acquire()
print("%s抢到B锁了"%name)
time.sleep(0.1) #让B锁在它手上拿一会
#再抢A锁
mutexA.acquire()
print("%s抢到A锁了"%name)
#释放A锁
mutexA.release()
#释放B锁
mutexB.release()
if __name__ == '__main__':
for i in range(4):
t = Thread(target=task,args=("线程%s"%i,))
t.start()
输出:
线程0抢到A锁了
线程0抢到B锁了
线程0抢到B锁了
线程1抢到A锁了
卡住了!!!!
- 程序卡死的原因在于线程0拿着B锁要抢A锁,线程1拿着A锁要抢B锁,互相卡住
- 当创建2把锁的时候就要考虑到互相之间的限制
8.2 递归锁(RLock)
互斥锁只能acquire一次,下次acquire就会卡住
递归锁可以连续acquire多次,每acquire一次计数器+1,只有计数为0时,才能被抢到acquire
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源
from threading import Thread,RLock
import os,time
mutexA = mutexB = RLock()
def task(name):
#先抢A锁
mutexA.acquire()
print("%s抢到A锁了"%name)
#再抢B锁
mutexB.acquire()
print("%s抢到B锁了"%name)
#释放B锁
mutexB.release()
#释放A锁
mutexA.release()
"""再来一次相反的顺序"""
#先抢B锁
mutexB.acquire()
print("%s抢到B锁了"%name)
time.sleep(0.1) #让B锁在它手上拿一会
#再抢A锁
mutexA.acquire()
print("%s抢到A锁了"%name)
#释放A锁
mutexA.release()
#释放B锁
mutexB.release()
if __name__ == '__main__':
for i in range(4):
t = Thread(target=task,args=("线程%s"%i,))
t.start()
- 导入RLock:from threading import Thread,RLock
- 两把锁设置为同一个RLock:mutexA = mutexB = RLock()
9、信号量(Semaphore)
信号量也是一把锁,可以指定信号量为5,对比互斥锁同一时间只能有一个任务抢到锁去执行,信号量同一时间可以有5个任务拿到锁去执行,如果说互斥锁是合租房屋的人去抢一个厕所,那么信号量就相当于一群路人争抢公共厕所,公共厕所有多个坑位,这意味着同一时间可以有多个人上公共厕所,但公共厕所容纳的人数是一定的,这便是信号量的大小
from threading import Thread,currentThread,Semaphore
import time
def toilet():
with sm:
print("%s正在上厕所"%currentThread().name)
time.sleep(2)
if __name__ == '__main__':
sm = Semaphore(3) #创建信号量,定义最大有锁线程数为3
for i in range(10):
t = Thread(target=toilet)
t.start()
- Semaphore管理一个内置的计数器,
- 每当调用acquire()时内置计数器+1;
- 调用release() 时内置计数器-1;
- 计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()
10、Event事件
由于线程是并发进行的,当A线程的执行需要确认B线程的状态时可以用Event
event.isSet():返回event的状态值;
event.wait():如果 event.isSet()==False将阻塞线程;
event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
event.clear():恢复event的状态值为False。
from threading import Thread,Event
import time
event = Event() #实例化Event
def teacher(name):
print("老师%s正在讲课"%name)
time.sleep(5)
event.set()
print("老师%s下课了"%name)
def student(name):
print("学生%s正在听课"%name)
event.wait() #可以传参,比如event.wait(2)代表只等待2秒,不管有没有set都执行后面代码
print("学生%s课间活动"%name)
if __name__ == '__main__':
stu1 = Thread(target=student,args=("Alex",))
stu2 = Thread(target=student,args=("Jack",))
teacher1 = Thread(target=teacher,args=("Egon",))
stu1.start()
stu2.start()
teacher1.start()
输出
学生Alex正在听课
学生Jack正在听课
老师Egon正在讲课
老师Egon下课了
学生Alex课间活动
学生Jack课间活动
- 学生课间休息是基于老师下课的
- 老师讲课完成event.set()发送下课指令
- 学生event.wait()等待老师下课指令,收到后继续进行后续代码
- event.wait()可以传参,比如event.wait(2)代表只等待2秒,不管有没有set都执行后面代码
利用Event尝试socket连接(次数小于3次)
from threading import Thread,Event,currentThread
import time
event = Event()
def conn():
n = 1
while not event.is_set(): #当event.set还未发出时,event.is_set()=False
if n == 4:
print("%s尝试太多次,连接失败"%currentThread().getName())
return
print("%s尝试连接第%s次"%(currentThread().getName(),n))
event.wait(2) #每次尝试都等待2秒
n += 1
print("%s连接成功"%currentThread().getName())
def check_connect():
print("%s检查连接"%currentThread().getName())
time.sleep(5) #检查耗时
event.set()
if __name__ == '__main__':
for i in range(2):
t = Thread(target=conn)
t.start()
check1 = Thread(target=check_connect)
check1.start()
11、定时器
11.1 定时器——指定n秒后执行某操作
from threading import Timer
def hello():
print("hello, world")
t = Timer(1, hello)
t.start() # 等待1秒后,hello函数将被执行
11.2 利用定时器写一个随机验证码
模仿网站验证码只有60秒时效
from threading import Timer
import string,random
class Code:
def __init__(self):
self.timer_make_code() #类实例化就执行制作
def timer_make_code(self):
self.current_code = self.make_code()
print(self.current_code)
self.t = Timer(5,self.timer_make_code) #5秒后再次执行创造验证码操作,相当于循环
self.t.start()
def make_code(self):
self.code = "".join(random.sample(string.ascii_lowercase + string.digits, 4)) #随机四位验证码
return self.code
def check_code(self):
while True:
input_code = input("请输入你的验证码:").strip()
if input_code == self.current_code:
print("验证通过")
self.t.cancel() #取消Timer的线程
break
obj = Code()
obj.check_code()
- Timer.cancel()是终止线程
12、线程queue
12.1 基础用法(先进先出)
import queue
q = queue.Queue(2) #2代表最大数据容量
q.put("第一个数据")
q.put("第二个数据")
print(q.get())
print(q.get())
输出
第一个数据
第二个数据
- 导入queue模块,并且实例化queue.Queue()
- 当put或者get量大于队列限制容量时,数据会卡住
q.put("第一个数据")
q.put("第二个数据")
q.put("第三个数据",block=True,timeout=3) #block代表存不存在阻塞,timeout代表等待时间,这里意思是存在阻塞并最多等待3秒
print(q.get())
print(q.get())
print(q.get(block=False,timeout=2))
输出报错:
queue.Full
12.2 queue.LifoQueue堆栈:last in fisrt out
q = queue.LifoQueue(2)
q.put("第一个数据")
q.put("第二个数据")
print(q.get())
print(q.get())
输出
第二个数据
第一个数据
12.3 queue.PriorityQueue优先级队列:存储数据时可设置优先级的队列
q = queue.PriorityQueue(3)
q.put((3,"第一个数据"))
q.put((2,"第二个数据"))
q.put((1,"第三个数据"))
print(q.get())
print(q.get())
print(q.get())
- q.put(数据优先级,数据本身)数据优先级数字越小,等级越高
12.4 线程queue.Queue与进程from multiprocessing.Queue import Queue的区别
-
from queue import Queue
这个是普通的队列模式,类似于普通列表,先进先出模式,get方法会阻塞请求,直到有数据get出来为止 -
from multiprocessing.Queue import Queue(各子进程共有)
这个是多进程并发的Queue队列,用于解决多进程间的通信问题。普通Queue实现不了。例如来跑多进程对一批IP列表进行运算,运算后的结果都存到Queue队列里面,这个就必须使用multiprocessing提供的Queue来实现
13、多线程实现并发的套接字
服务端:
import socket
from threading import Thread
def talk(conn):
while True:
try:
data = conn.recv(1024).decode("utf-8")
if not data:break
conn.send(data.upper().encode("utf-8"))
except ConnectionResetError:
break
conn.close()
def run_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 9999))
server.listen(5)
while True:
conn, addr = server.accept()
t = Thread(target=talk,args=(conn,)) #创建一个线程执行talk任务
t.start()
server.close()
if __name__ == "__main__":
run_server()
客户端:
import socket
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",9999))
while True:
msg = input(">>>")
client.send(msg.encode("utf-8"))
server_response = client.recv(1024).decode("utf-8")
print(server_response)
client.close()
14、进程池与线程池
- 使用多线程实现并发套接字通信时,有个缺点当客户端数量庞大的时候,服务端开启过多线程导致瘫痪,这时可以用线程池进行约束
- 本质上还是基于多进(线)程,只不过对开启进(线)程的数量加以限制
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
import os,time
def task(name):
print("name:%s pid:%s "%(name,os.getpid()))
time.sleep(2)
if __name__ == '__main__':
pool = ProcessPoolExecutor(2) #建立进程池
for i in range(10):
pool.submit(task,"Kerwin%s"%i) #异步调用
pool.shutdown(wait = True) #等待pool里的进程执行完毕
print("主")
输出:
name:Kerwin0 pid:22268
name:Kerwin1 pid:19524
name:Kerwin2 pid:22268
name:Kerwin3 pid:19524
name:Kerwin4 pid:19524
name:Kerwin5 pid:22268
name:Kerwin6 pid:22268
name:Kerwin7 pid:19524
name:Kerwin8 pid:19524
name:Kerwin9 pid:22268
主
- 导入进程池模块from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
- 建立进程池pool = ProcessPoolExecutor(2),2代表最大进程数,如果不填默认CPU核数
- 异步调用,只管将进程扔进池子,速度非常快
pool.submit(函数名,参数), - pool.shutdown()等待池子中所有进程结束
相当于进程池的pool.close()+pool.join()操作
wait=True,等待池内所有任务执行完毕回收完资源后才继续
wait=False,立即返回,并不会等待池内的任务执行完毕
但不管wait参数为何值,整个程序都会等到所有任务执行完毕
submit和map必须在shutdown之前
map(func, *iterables, timeout=None, chunksize=1) 取代for循环submit的操作
pool = ThreadPoolExecutor()
for i in range(10):
pool.submit(task,args)
可以转换成:
pool.map(task,range(10))
15、异步调用与回调机制
提交任务的两种方式:同步提交与异步提交
下面模拟卖水果的步骤:称重+算钱
15.1 同步调用
同步调用: 提交完任务后,就在原地等待任务执行完毕,拿到结果,再执行下一行代码,导致程序是串行执行
from concurrent.futures import ThreadPoolExecutor
import time,random
def weight(name):
weight = random.randint(1,10)
time.sleep(random.randint(1,3))
print("%s 拿了 %s kg 苹果"%(name,weight))
return {"name":name,"weight":weight}
def money(res):
name = res["name"]
weight = res["weight"]
money = weight*10
print("%s 付了 %s元"%(name,money))
if __name__ == '__main__':
pool = ThreadPoolExecutor()
weight1 = pool.submit(weight,"Alex").result()
money1 = money(weight1)
weight2 = pool.submit(weight,"Jack").result()
money2 = money(weight2)
输出
Alex 拿了 5 kg 苹果
Alex 付了 50元
Jack 拿了 4 kg 苹果
Jack 付了 40元
- pool.submit(weight,“Alex”).**result()**是取得weight函数的结算结果
- 同步调用必须等待Alex线程拿了苹果算好钱以后再去执行Jack的线程,效率慢
15.2 异步调用
异步调用:提交完任务后,不在原地等待任务执行完毕
from concurrent.futures import ThreadPoolExecutor
import time,random
def weight(name):
weight = random.randint(1,10)
time.sleep(random.randint(1,3))
print("%s 拿了 %s kg 苹果"%(name,weight))
return {"name":name,"weight":weight}
def money(res):
res = res.result()
name = res["name"]
weight = res["weight"]
money = weight*10
print("%s 付了 %s元"%(name,money))
if __name__ == '__main__':
pool = ThreadPoolExecutor()
pool.submit(weight,"Alex").add_done_callback(money)
pool.submit(weight,"Jack").add_done_callback(money)
- pool.submit(weight,“Alex”).add_done_callback(money),表示pool.submit(weight,“Alex”)执行完成后add_done_callback(money)继续执行money函数,同时将pool.submit(weight,“Alex”)作为参数传入money
- 所以money函数中要res = res.result()取得线程对象pool.submit(weight,“Alex”)的结果
四、并发编程之协程
1、协程介绍
协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine
协程是一种用户态(应用程序中的)的轻量级线程,即协程是由用户程序自己控制调度的。
并发的实现是基于:来回切换+保存状态,协程是模拟操作系统的工作自己实现的
2、协程实现与总结
操作系统只能监测线程整体的I/O阻塞再进行切换,协程可以实现自己进行同一个线程内的I/O切换
2.1 协程的实现(yield)
import time
def producer():
g = consumer()
next(g)
for i in range(10000000):
g.send(i)
def consumer():
while True:
res = yield
start_time=time.time()
producer()
end_time=time.time()
print(end_time-start_time) #2.1932969093322754秒
- 通过yield实现生成数据方和接收数据方并发进行,但实际这数据计算密集型程序,来回切换反而降低效率
2.2 协程的总结
- 必须在只有一个单线程里实现并发
- 修改共享数据不需加锁(因为协程本身不是并行,只是一个个进行,不需要加锁)
- 用户程序里自己保存多个控制流的上下文栈
- 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
3、greenlet模块
相比yield实现协程间的切换,greenlet提供的方法更加方便
from greenlet import greenlet
import time
def eat(name):
print("%s 正在吃苹果"%name)
g2.switch("Kerwin") #第二步:启动play
time.sleep(2)
print("%s 正在喝奶茶"%name)
g2.switch() #第四步:回到play
def play(name):
print("%s 正在看电影"%name)
g1.switch() #第三步:回到eat(第二次切换无需传参)
time.sleep(3)
print("%s 正在打王者荣耀"%name)
g1 = greenlet(eat) #实例化
g2 = greenlet(play)
g1.switch("Kerwin") #第一步:启动eat
输出:
Kerwin 正在吃苹果
Kerwin 正在看电影
Kerwin 正在喝奶茶
Kerwin 正在打王者荣耀
- 导入greenlet模块下的greenlet类
- g1 = greenlet(eat)实例化:eat是函数名称
- g1.switch(“Kerwin”):切换eat函数,第一次切换时需要加上参数,后续切换无需传参
4、gevent模块
- greenlet模块可以实现自主切换,但无法系统自动识别I/O阻塞
- gevent可以帮助自动识别I/O阻塞进行切换到其他协程的运算操作
不使用gevent模块的协程:
from greenlet import greenlet
import time
def eat(name):
print("%s 正在吃苹果"%name)
g2.switch("Kerwin")
time.sleep(2)
print("%s 正在喝奶茶"%name)
g2.switch("Jack")
def play(name):
print("%s 正在看电影"%name)
g1.switch()
time.sleep(3)
print("%s 正在打王者荣耀"%name)
start_time = time.time()
g1 = greenlet(eat)
g2 = greenlet(play)
g1.switch("Kerwin")
end_time = time.time()
print(end_time - start_time) #耗时5.001164674758911秒
使用gevent模块的协程:(自动遇到阻塞切换)
import gevent
import time
def eat(name):
print("%s 正在吃苹果"%name)
gevent.sleep(2) #相当于time.sleep,但gevent.sleep可以实现自动切换阻塞
print("%s 正在喝奶茶"%name)
def play(name):
print("%s 正在看电影"%name)
gevent.sleep(3)
print("%s 正在打王者荣耀"%name)
start_time = time.time()
g1 = gevent.spawn(eat,"Kerwin") #调用函数
g2 = gevent.spawn(play,"Kerwin")
g1.join() #gevent属于异步调用,当主线程死后就死了,所以调用无法输出(还没来得及打开)
g2.join()
end_time = time.time()
print(end_time - start_time) #耗时3.019620895385742秒
- 导入gevent模块:import gevent
- g1 = gevent.spawn(eat,“Kerwin”):实例化并且调用函数,gevent.spawn(函数名,参数)
- g1.join():gevent.spawn属于异步调用开启,只负责调用,但是当主线程死了调用还没来得及打开,所以需要join等待
- gevent.sleep(3):等于time.sleep,但是time.sleep并不能让gevent模块抓取阻塞,除非用gevent下的monkey功能:monkey.patch_all()顶头写
使用gevent模块的monkey功能:(自动抓取阻塞,不依赖gevent.sleep)
monkey.patch_all()顶头写
from gevent import monkey
import gevent
import time
monkey.patch_all() #执行一下
def eat(name):
print("%s 正在吃苹果"%name)
time.sleep(2) #monkey.patch_all()可以抓取到阻塞进行切换到运算
print("%s 正在喝奶茶"%name)
def play(name):
print("%s 正在看电影"%name)
time.sleep(3)
print("%s 正在打王者荣耀"%name)
start_time = time.time()
g1 = gevent.spawn(eat,"Kerwin")
g2 = gevent.spawn(play,"Kerwin")
g1.join()
g2.join() #可以合并成gevent.joinall([g1,g2])
end_time = time.time()
print(end_time - start_time) #耗时3.0203864574432373
- 导入模块:from gevent import monkey
- monkey.patch_all()顶头写,需要执行一下,对所有打补丁
- 实现了协程下的并发效果(不是并行)
- gevent.joinall([g1,g2]):g1.join()和g2.join()可以合并成gevent.joinall([g1,g2])
- 我们可以用threading.current_thread().getName()来查看每个g1和g2,查看的结果为DummyThread-n,即假线程
基于gevent模块实现并发的套接字通信
服务端:
import socket
import gevent
from gevent import monkey;monkey.patch_all() #打补丁,自动切换阻塞
def server():
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",9999))
server.listen(5)
while True:
conn,addr = server.accept()
gevent.spawn(talk,conn) #启动一个协程去执行任务
def talk(conn):
while True:
try:
data = conn.recv(1024).decode("utf-8")
conn.send(data.upper().encode("utf-8"))
except ConnectionResetError:
break
conn.close()
if __name__ == '__main__':
g = gevent.spawn(server)
g.join() #由于是异步调用,等待确保server执行起来
客户端:
import socket
from threading import Thread,currentThread
def client():
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",9999))
client.send(("%s say hello"%currentThread().getName()).encode("utf-8"))
server_response = client.recv(1024).decode("utf-8")
print(server_response)
client.close()
if __name__ == '__main__':
for i in range(10):
t = Thread(target=client) #开启10个线程去连接服务端
t.start()
五、I/O模型
1、IO模型介绍
基于network IO网络IO背景下的五种I/O模型:
- blocking IO 阻塞I/O
- nonblocking IO 非阻塞I/O
- IO multiplexing 多路复用I/O
- asynchronous IO 异步I/O
- signal driven IO 信号驱动IO(实际不常用)
网络IO经历的两个阶段:
- 等待数据阶段(wait)
- 将数据从内核(操作系统缓存)拷贝到进程中(copy)
2、阻塞I/O模型
socket数据传输过程中服务端的accept()和recv()都是阻塞型I/O,会经历:客户端——操作系统(client)——网络延迟——操作系统(server)——服务端
解决方案:
- 多线程,优点:线程之间的阻塞不影响其他线程运行;缺点:当客户端量大时服务端计算机承受不了
- 线程池,优点:限制了同时运行线程在计算机运算能力内;缺点:当上千万客户端时,线程池相对较小
3、非阻塞IO
设计程序不断地针对阻塞询问操作系统,当收到blocking error的时候就去干自己的事,过一会再来询问,当收到消息了就继续下面的事儿,否则继续去干自己的事
非阻塞IO模型下的socket服务端:
from socket import *
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1',8083))
server.listen(5)
server.setblocking(False)
rlist=[] #read_list收列表
wlist=[] #write_list发列表
while True:
try:
conn, addr = server.accept()
rlist.append(conn)
print(rlist)
except BlockingIOError:
# print('干其他的活')
#收消息
del_rlist = []
for conn in rlist:
try:
data=conn.recv(1024)
if not data:
del_rlist.append(conn)
continue
wlist.append((conn,data.upper()))
except BlockingIOError: #未连接则继续循环
continue
except Exception:
conn.close()
del_rlist.append(conn)
#发消息
del_wlist=[]
for item in wlist:
try:
conn=item[0]
data=item[1]
conn.send(data)
del_wlist.append(item)
except BlockingIOError:
pass
for item in del_wlist:
wlist.remove(item)
for conn in del_rlist:
rlist.remove(conn)
server.close()
- server.setblocking(False) 将程序设置成非阻塞(默认为True)
- except BlockingIOError: 由于setblocking为False,所以一旦连接不上不会阻塞,而会报错
- 本质上通过人为地反复循环得到是否阻塞的结果,再去做自己的事情
- 缺点:1.任务响应慢:因为当得到未连接成功会去干完自己的事再来回询问第二次,中间可能已经连接成功;2.CPU占用率非常高,类似死循环
4、多路复用IO(事件驱动IO)
中介select一直等待操作系统答复,得到数据准备好了发给服务端,服务端只需要进行copy data阶段(wait data已经在select阶段完成了),相比于阻塞IO实际上重复了操作系统通知select,select通知服务端,服务端再去call操作系统的步骤;但优势在于存在多个客户端时只需1个中介select即可(select可以同时检测多个套接字的IO行为)
多路复用IO模型下的socket服务端:
from socket import *
import select #导入socket模块
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1',8083))
server.listen(5)
server.setblocking(False) #全部设置为非阻塞模式
print('starting...')
rlist=[server,] #执行收IO行为的套接字对象列表
wlist=[] #执行发IO行为的套接字对象列表
wdata={}
while True:
rl,wl,xl=select.select(rlist,wlist,[],0.5) #[]代表报异常的列表,0.5代表间隔时间(每隔0.5秒问一次)
print('rl',rl) #一旦收IO对象列表或者发IO对象列表中的元素得到了操作系统响应,就会反馈在rl和wl列表里
print('wl',wl)
for sock in rl: #遍历得到操作系统响应的收IO行为对象
if sock == server:
conn,addr=sock.accept()
rlist.append(conn) #增加到收IO列表,下次select一并监听操作系统响应
else:
try:
data=sock.recv(1024)
if not data:
sock.close()
rlist.remove(sock)
continue
wlist.append(sock)
wdata[sock]=data.upper()
except Exception:
sock.close()
rlist.remove(sock)
for sock in wl: #遍历得到操作系统响应的发IO对象
data=wdata[sock]
sock.send(data)
wlist.remove(sock)
wdata.pop(sock)
server.close()
缺点:select是带着列表的形式去操作系统那儿监听的,一旦数据过大对操作系统来说会慢(循环所有元素)
5、异步IO
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel(内核)的角度,当它收到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
效率最高
- 对比阻塞IO,它向操作系统发出申请后就去做其他事情
- 对比非阻塞IO,它不用反复询问操作系统响应
六、socketserver模块
socketserver使用模式:
- 第一步:创建继承socketserver.BaseRequestHandler的类,定义handle方法
class Myserver(socketserver.BaseRequestHandler):
def handle(self):
pass - 第二步:开启多线程,实例化对象
server = socketserver.ThreadingTCPServer((“127.0.0.1”,8888),Myserver)
实例化过程,需要传参(IP端口,自定义的Myserver类) - 第三步:打开对象的serve_forver功能
server.serve_forever()
class Myserver(socketserver.BaseRequestHandler):
def handle(self):
#填写服务端需要并发的业务逻辑(代码)
while True:
recv_data = self.request.recv(1024).decode("utf-8") #self.request等于conn对象
print(recv_data)
send_data = recv_data.upper().encode("utf-8")
self.request.send(send_data)
server = socketserver.ThreadingTCPServer(("127.0.0.1",8888),Myserver)
#模块帮助执行了1.self.socket 2.self.socket.bind 3.self.socket.listen(5)
server.serve_forever()
- 导入socketserver模块
- self.request等于conn对象