网络编程进阶

一、操作系统介绍

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())
输出
线程116588
主线程: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. 第一步:线程1抢到GIL锁,将自己的代码导入解释器执行
  2. 第二步:线程1抢到自定义Lock锁,但是遇到time.sleep阻塞,交出GIL锁
  3. 第三步:线程2抢到GIL锁,执行到自定义锁时发现在线程1那儿,阻塞住交出GIL锁
  4. 第四步:线程1又抢到GIL锁,也有Lock锁,继续执行代码完成,释放Lock锁,释放GIL锁
  5. 第五步:线程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的区别
  1. from queue import Queue
    这个是普通的队列模式,类似于普通列表,先进先出模式,get方法会阻塞请求,直到有数据get出来为止

  2. 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 协程的总结
  1. 必须在只有一个单线程里实现并发
  2. 修改共享数据不需加锁(因为协程本身不是并行,只是一个个进行,不需要加锁)
  3. 用户程序里自己保存多个控制流的上下文栈
  4. 附加:一个协程遇到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经历的两个阶段:
  1. 等待数据阶段(wait)
  2. 将数据从内核(操作系统缓存)拷贝到进程中(copy)
2、阻塞I/O模型

在这里插入图片描述
socket数据传输过程中服务端的accept()和recv()都是阻塞型I/O,会经历:客户端——操作系统(client)——网络延迟——操作系统(server)——服务端
解决方案:

  1. 多线程,优点:线程之间的阻塞不影响其他线程运行;缺点:当客户端量大时服务端计算机承受不了
  2. 线程池,优点:限制了同时运行线程在计算机运算能力内;缺点:当上千万客户端时,线程池相对较小
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对象
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值