Python3基础之学习笔记(九)-线程-进程-协程

1. 线程与进程

进程:是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

线程进程小结:

  • 所有在同一个进程里的线程是共享同一块内存空间的。
  • 线程共享内存空间,进程的内存是独立的。
  • 同一个进程的线程之间可以直接交流,两个进程想通信,必须通过一个中间代理来实现。
  • 创建新线程很简单, 创建新进程需要对其父进程进行一次克隆。
  • 一个线程可以控制和操作同一进程里的其他线程,但是进程只能操作子进程。
  • python多线程不适合cpu密集操作型的任务(计算任务),适合io操作密集型的任务

1.1 多线程

1.1.1 多线程之创建

简单创建多线程

import threading
import time
def run(n):
    print('task:',n)
    time.sleep(2)
def main():
    t1=threading.Thread(target=run,args=('t1',))#创建线程1
    t2=threading.Thread(target=run, args=('t2',))#创建线程2
    t1.start()#运行线程1
    t2.start()#运行线程2
if __name__ == "__main__":
    main()

使用类创建多线程

import threading
import time
class MyThread(threading.Thread):
    def __init__(self,n):
        super().__init__()
        self.n=n
    def run(self):
        print('task:',self.n)
        time.sleep(2)
def main():
    t1=MyThread('t1')
    t2=MyThread('t2')
    t1.start()
    t2.start()
if __name__ == "__main__":
    main()
 

1.1.2 多线程之join用法

一个python程序本身是一个线程,叫主线程,当启动一个子线程时,不管子线程有没有执行结束,主线程是继续执行的,主线程与子线程是并行的。

thread.join():在子线程完成运行之前,该子线程的父线程(一般就是主线程)将一直存在,也就是只有子线程运行完之后主线程才运行。

import threading
import time
class MyThread(threading.Thread):
    def __init__(self,n,sleep_time):
        super().__init__()
        self.n=n
        self.sleep_time=sleep_time
    def run(self):
        print('task:',self.n)
        time.sleep(self.sleep_time)
        print('task:%s执行完毕'%self.n)
def main():
    t1=MyThread('t1',2)
    t2=MyThread('t2',4)
    t1.start()
    t2.start()
    t1.join()#t1线程执行完之后才会执行主线程
    print('主线程执行完毕')
if __name__ == "__main__":
    main()
 
'''
执行结果:
 
task: t1
task: t2
task:t1执行完毕
主线程执行完毕
task:t2执行完毕
 
'''

1.1.3 多线程之守护线程

守护线程:如果在程序中将子线程设置为守护线程,则该子线程会在主线程结束时自动退出,设置方式为thread.setDaemon(True),要在thread.start()之前设置,默认是false的,也就是主线程结束时,子线程依然在执行。

import threading
import time
class MyThread(threading.Thread):
    def __init__(self,n,sleep_time):
        super().__init__()
        self.n=n
        self.sleep_time=sleep_time
    def run(self):
        print('task:',self.n)
        time.sleep(self.sleep_time)
        print('task:%s执行完毕'%self.n)
def main():
    t1=MyThread('t1',2)
    t2=MyThread('t2',4)
    t1.setDaemon(True)#设置t1线程为守护线程,也就是主线程退出,t1线程直接退出
    t2.setDaemon(True)#设置t2线程为守护线程,也就是主线程退出,t2线程直接退出
    t1.start()
    t2.start()
    print('主线程执行完毕')
if __name__ == "__main__":
    main()
'''
执行结果为:
 
task: t1
task: t2
主线程执行完毕
 
'''

1.1.4 多线程之线程锁

如果有多个线程同时操作一个对象,如果没有很好地保护该对象,会造成程序结果的不可预期 ,Threading模块为我们提供了一个类,Threading.Lock,锁。我们创建一个该类对象,在线程函数执行前,“抢占”该锁,执行完成后,“释放”该锁,则我们确保了每次只有一个线程占有该锁。这时候对一个公共的对象进行操作,则不会发生线程不安全的现象了。

import threading
def run(n):
    lock.acquire()#上锁,让其他线程无法操作
    print('task:',n)
    global num
    num+=1
    lock.release()#释放锁,让其他线程可以操作
lock=threading.Lock()#定义锁
num=0
def main():
     t1=threading.Thread(target=run,args=('t1',))#创建线程1
     t2=threading.Thread(target=run, args=('t2',))#创建线程2
     t1.start()#运行线程1
     t2.start()#运行线程2
     t1.join()
     t2.join()
     print('num=',num)
 
if __name__ == "__main__":
    main()

递归锁,防止出现锁死现象

import threading, time
def run1():
    print("grab the first part data")
    lock.acquire()
    global num
    num += 1
    lock.release()
    return num
def run2():
    print("grab the second part data")
    lock.acquire()
    global num2
    num2 += 1
    lock.release()
    return num2
def run3():
    lock.acquire()
    res = run1()
    print('--------between run1 and run2-----')
    res2 = run2()
    lock.release()
    print(res, res2)
num, num2 = 0, 0
lock = threading.RLock()
for i in range(1):
    t = threading.Thread(target=run3)
    t.start()
 
#threading.active_count()获取当前运行的线程数量
while threading.active_count() != 1:
    print(threading.active_count())
else:
    print('----all threads done---')
    print(num, num2)

1.1.5 多线程之信号量

线程锁同时只允许一个线程更改数据,而信号量同时允许一定数量的线程更改数据,用来限制线程并发数量的。

import threading, time
def run(n):
    semaphore.acquire()
    time.sleep(1)
    print("run the thread: %s\n" % n)
    semaphore.release()
 
if __name__ == '__main__':
    semaphore = threading.BoundedSemaphore(5)  # 最多允许5个线程同时运行
    for i in range(22):
        t = threading.Thread(target=run, args=(i,))
        t.start()
while threading.active_count() != 1:
    pass
else:
    print('----all threads done---')
 

1.1.6 多线程之Event

Event用于线程间通信,即程序中的其一个线程需要通过判断某个线程的状态来确定自己下一步的操作,就用到了event对象,event对象默认为假(Flase),即遇到event对象在等待就阻塞线程的执行。

import time
import threading
event = threading.Event()
def lighter():
    count = 0
    event.set() #设置标志位,设置代表绿灯
    while True:
        if count >5 and count < 10: #改成红灯
            event.clear() #把标志位清了
            print("\033[41;1m红灯亮了....\033[0m")
        elif count >10:
            event.set() #变绿灯
            count = 0
        else:
            print("\033[34;1m绿灯亮了....\033[0m")
        time.sleep(1)
        count +=1
 
def car(name):
    while True:
        if event.is_set(): #判断标志位是否被设置,如果被设置代表绿灯
            print("[%s]正在行驶中·····"% name )
            time.sleep(1)
        else:
            print("前面是红灯,[%s]正在等待" %name)
            event.wait()#等待直到标志位改变
            print("\033[34;1m绿灯亮了,[%s]开始行驶\033[0m" %name)
light = threading.Thread(target=lighter,)
light.start()
car1 = threading.Thread(target=car,args=("Ferrari",))
car1.start()

1.1.7 多线程之队列

队列Queue是python标准库中的线程安全的队列(FIFO)实现,提供了一个适用于多线程编程的先进先出的数据结构,即队列,用来在生产者和消费者线程之间的信息传递 。

FIFO队列(先入先出)

Queue提供了一个基本的FIFO容器,使用方法很简单,maxsize是个整数,指明了队列中能存放的数据个数的上限。一旦达到上限,插入会导致阻塞,直到队列中的数据被消费掉。如果maxsize小于或者等于0,队列大小没有限制。

import queue
q = queue.Queue()
for i in range(5):
    q.put(i)
print('对列大小:',q.qsize())
while not q.empty():
    print(q.get())#如果队列里数据为空则等待
    #print(q.get_nowait())#如果队列里数据为空则抛出异常
'''
执行结果为:
对列大小: 5
0
1
2
3
4
 
'''

LIFO队列(先入后出)

LIFO即Last in First Out,后进先出。与栈的类似,使用也很简单,maxsize用法同上

import queue
q = queue.LifoQueue()
for i in range(5):
    q.put(i)
print('对列大小:',q.qsize())
while not q.empty():
    print(q.get())
 
'''
执行结果为:
对列大小: 5
4
3
2
1
0
 
'''

1.2 多进程

1.2.1 多进程之创建

import multiprocessing
import time
def run(name):
    time.sleep(2)
    print('Hello {0}'.format(name))
def main():
    p=multiprocessing.Process(target=run,args=('World',))
    p.start()
    p.join()
if __name__ == "__main__":
    main()

1.2.2 多进程之Queue

不同进程间内存是不共享的,要想实现两个进程间的数据交换,可以使用Queue,原理是将数据序列化和反序列化。

from multiprocessing import Queue,Process
def f(q):
    q.put([42,None,'hello'])
def main():
    q=Queue()
    p=Process(target=f,args=(q,))
    p.start()
    print(q.get())
    p.join()
if __name__ == "__main__":
    main()

1.2.3 多进程之Pipe

不同进程间内存是不共享的,要想实现两个进程间的数据交换,可以使用Pipe

from multiprocessing import Process,Pipe
def f(conn):
    conn.send([123,None,'hello'])
    conn.close()
def main():
    parent_conn,child_conn=Pipe()
    p=Process(target=f,args=(child_conn,))
    p.start()
    print(parent_conn.recv())
    p.join()
if __name__ == "__main__":
    main()

1.2.4 多进程之Manager

实现进程之间数据共享,可以使用Manger

from multiprocessing import Process,Manager
import os
def f(d,l):
    d[1]='1'
    d['2']=2
    l.append(os.getpid())
def main():
    with Manager() as m:
        d=m.dict()
        l=m.list(range(5))
        p_list=[]
        for i in range(10):
            p=Process(target=f,args=(d,l))
            p.start()
            p_list.append(p)
        for p in p_list:
            p.join()
        print(d)
        print(l)
if __name__ == "__main__":
    main()
 

1.2.5 多进程之Lock

from multiprocessing import Process,Lock
def f(lock,i):
    lock.acquire()
    print('hello ',i)
    lock.release()
def main():
    lock=Lock()
    for i in range(10):
        Process(target=f,args=(lock,i)).start()
if __name__ == "__main__":
    main()

1.2.6 多进程之进程池

from multiprocessing import Process,Pool
import time
import os
def foo(i):
    time.sleep(2)
    print('in process ',os.getpid())
    return i+100
def bar(arg):
    print('-->exec done:',arg)
def main():
    pool=Pool(5)#允许进程池同时放入5个进程
    for i in range(10):
        pool.apply_async(func=foo,args=(i,),callback=bar)#池中进程异步执行,bar是回调函数,foo函数执行完后执行bar函数
        #pool.apply()#池中进程同步执行
    print('end')
    pool.close()
    pool.join()#进程池中进程执行完毕后再关闭,如果注释掉,池关闭了,子进程不会执行
if __name__ == "__main__":
    main()

2. 协程

2.1 协程的概念

协程,又称微线程。协程是一种用户态的轻量级线程。

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态,每次过程重入 时,就相当于进入上一次调用的状态。

2.2 协程的优缺点

协程的优点:

  1. 无需线程上下文切换的开销,协程避免了无意义的调度,由此可以提高性能(但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力)。
  2. 无需原子操作锁定及同步的开销 。
  3. 方便切换控制流,简化编程模型 。
  4. 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

协程的缺点:

  1. 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
  2. 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序 。

2.3 实现协程

2.3.1 yield实现协程

def consumer(name):
    print("要开始吃包子了...")
    while True:
        print("\033[31;1m[consumer] %s\033[0m " % name)
        bone = yield
        print("[%s] 正在吃包子 %s" % (name, bone))
 
 
def producer(obj1, obj2):
    obj1.send(None)    # 启动obj1这个生成器,第一次必须用None  <==> obj1.__next__()
    obj2.send(None)    # 启动obj2这个生成器,第一次必须用None  <==> obj2.__next__()
    n = 0
    while n < 5:
        n += 1
        print("\033[32;1m[producer]\033[0m 正在生产包子 %s" % n)
        obj1.send(n)
        obj2.send(n)
if __name__ == '__main__':
    conA = consumer("消费者A")
    conB = consumer("消费者B")
    producer(conA, conB)

2.3.2 greenlet实现协程

Python的 greenlet就相当于手动切换,去执行别的子程序,在“别的子程序”中又主动切换回来。

from greenlet import greenlet
# greenlet 其实就是手动切换;gevent是对greenlet的封装,可以实现自动切换
def test1():
    print("123")
    gr2.switch()   # 切换去执行test2
    print("456")
    gr2.switch()   # 切换回test2之前执行到的位置,接着执行
def test2():   
    print("789")
    gr1.switch()   # 切换回test1之前执行到的位置,接着执行
    print("666")
gr1 = greenlet(test1)   # 启动一个协程 注意test1不要加()
gr2 = greenlet(test2)   #
gr1.switch()#切换到test1执行
 
'''
执行结果:
 
123
789
456
666
'''

2.3.3 gevent实现协程

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

​ gevent会主动识别程序内部的IO操作,当子程序遇到IO后,切换到别的子程序。如果所有的子程序都进入IO,则阻塞。

import gevent
def func1():
    print("func1 running")
    gevent.sleep(2)             # 内部函数实现io操作
    print("switch func1")
def func2():
    print("func2 running")
    gevent.sleep(1)
    print("switch func2")
def func3():
    print("func3  running")
    gevent.sleep(0)
    print("func3 done..")
gevent.joinall([gevent.spawn(func1),
                gevent.spawn(func2),
                gevent.spawn(func3),
                ])
 
'''
执行结果为:
func1 running
func2 running
func3  running
func3 done..
switch func2
switch func1
'''

2.3.4 协程之爬虫

from urllib import request
import gevent,time
from gevent import monkey
monkey.patch_all()    # 把当前程序中的所有io操作都做上标记,否则不会进行切换。
def spider(url):
    print("GET:%s" % url)
    resp = request.urlopen(url)
    data = resp.read()
    print("%s bytes received from %s.." % (len(data), url))
 
urls = [
    "https://www.python.org/",
    "https://www.yahoo.com/",
    "https://github.com/"
]
 
start_time = time.time()
for url in urls:
    spider(url)
print("同步耗时:",time.time() - start_time)
 
async_time_start = time.time()
gevent.joinall([
    gevent.spawn(spider,"https://www.python.org/"),
    gevent.spawn(spider,"https://www.yahoo.com/"),
    gevent.spawn(spider,"https://github.com/"),
])
print("异步耗时:",time.time() - async_time_start)
 
'''
执行结果为:
GET:https://www.python.org/
48823 bytes received from https://www.python.org/..
GET:https://www.yahoo.com/
486325 bytes received from https://www.yahoo.com/..
GET:https://github.com/
61390 bytes received from https://github.com/..
同步耗时: 7.211819410324097
GET:https://www.python.org/
GET:https://www.yahoo.com/
GET:https://github.com/
48823 bytes received from https://www.python.org/..
82687 bytes received from https://github.com/..
494117 bytes received from https://www.yahoo.com/..
异步耗时: 3.1606600284576416
 
'''

2.3.5 协程之socket

服务端

import sys
import socket
import time
import gevent
from gevent import socket, monkey
monkey.patch_all()
def server(port):
    s = socket.socket()
    s.bind(('127.0.0.1', port))
    s.listen(500)
    while True:
        cli, addr = s.accept()
        gevent.spawn(handle_request, cli)#创建一个协程
def handle_request(conn):
    try:
        while True:
            data = conn.recv(1024)
            print("recv:", data)
            conn.send(data)
            if not data:
                conn.shutdown(socket.SHUT_WR)
    except Exception as  ex:
        print(ex)
    finally:
        conn.close()
if __name__ == '__main__':
    server(7000)

客户端

import socket
HOST = 'localhost'
PORT = 7000
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
while True:
    msg = bytes(input(">>:"), encoding="utf8")
    s.sendall(msg)
    data = s.recv(1024)
    #print(data)
    print('Received', repr(data))
s.close()
 

2.4 事件驱动

通常,我们写服务器处理模型的程序时,有三种方式。第一种是使用多进程处理请求,第二种使用多线程处理请求,第三种使用事件驱动处理请求。第一种方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单。第二种方式,由于要涉及到线程的同步,有可能会面临死锁等问题。第三种方式,在写应用程序代码时,逻辑比前面两种都复杂。综合考虑各方面因素,一般普遍认为第三种方式是大多数网络服务器采用的方式。目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。

事件驱动模型大体思路如下:

  1. 有一个事件(消息)队列;
  2. 鼠标按下时,往这个队列中增加一个点击事件(消息);
  3. 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
  4. 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;

事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。

单线程、多线程以及事件驱动编程模型的比较:

在单线程同步模型中,任务按照顺序执行。任务之间需要互相等待的话这就使得程序不必要的降低了运行速度。

在多线程版本中,这3个任务分别在独立的线程中执行。这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,因为这类程序不得不通过线程同步机制如锁、可重入函数、线程局部存储或者其他机制来处理线程安全问题。

在事件驱动版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时,注册一个回调到事件循环中,然后当I/O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽可能的得以执行而不需要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为,因为程序员不需要关心线程安全问题。

2.5 IO模式

IO模式,一次IO调用会经历两个阶段。一、等待数据阶段,将数据从网络或者是磁盘读取到系统内核(kennel) 二、将数据从内核拷贝到进程中。

基于这两个阶段,linux系统下面产生了五种网络网络模式方案。

  • 阻塞I/O(blocking IO)
  • 非阻塞I/O(nobokcing IO)
  • I/O多路复用。(I/O multiplexing)
  • 信号驱动
  • 异步I/O(async)

由于信号驱动使用较少,主要介绍其余四种模式。

2.5.1 阻塞I/O

在数据准备阶段和拷贝数据阶段都会阻塞。用户调用recefrom 以后会一直等待数据拷贝到用户内存未至。

2.5.2 非阻塞I/O

用户会一直调用recefrom,如果数据没有准备好。会返回一个ERROR。一直到数据准备好。因此在读数据阶段不会卡住。

2.5.3 IO多路复用

epoll模式,也就是IO多路复用模式(事件驱动模式)。一次性可以处理多个网络IO。调用select方法,整个进程会被阻塞,同时内核监视所有select负责的socket,直到其中一个有数据就会立即返回。然后拷贝数据。然后进入下一个循环 。I/O多路复用的特点事通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符有一个进入就绪状态,select()函数就返回。

2.5.4 异步IO

异步IO,异步I/O不会阻塞。当用户进程发起read操作以后。可以立刻开始做其他的事情(相当于告诉在内核注册一个事件。由内核进行监控,处理完以后内核主动通知)。而另一方面从kennel角度看当他收到一个async read以后,会立马回调。所以不会产生任何阻塞。当kennel准备好数据,将数据贝考到用户内存以后,会主动通知用户,告诉read操作已完成。

2.5.5 总结

阻塞IO和非阻塞IO区别:

调用blocking IO 会一直阻塞,知道read全部完成为止。而 nonblocking IO  在ready会立即返回。相同点在于二者在coy阶段都会卡住。

async和synch的区别:

二者区别在于拷贝数据阶段是否会阻塞。所以上面的blocking IO、nonblocking IO、I/O多路复用都属于同步I/0。而async任何阶段不会阻塞。

select poll epoll区别 :

select :他通过一个select()系统调用来监控多个文件描述符的数组。当select()返回以后,该文件描述符便会被内核修改表示位。使得进程可以获得文件描述符。从而进行后续读写操作。一、监视的文件描述符号存在最大数限制。可以通过修改内核参数来解决 2、由于select()会对所有的socket进行一次线性扫描。这也浪费了一定开销。随着文件描述符的增减。扫描时间线性增加。

poll:去掉了最大文件数限制。缺点和select一样。另外如果将就绪的文件告诉后台进程以后,进程没有读取。下次还会继续扫描。所以一般不会丢失消息。称之为水平触发。

epoll模式。用的最多的一种方式。同时支持水平触发和边缘触发(告诉进程描述符已经准备就绪,他只说一遍。如果程序未采取行动。下次将不会告知,这个叫边缘触发)。边缘触发较复杂。性能更高。优点:epoll相比于poll在于每次扫描只扫描活跃连接数。节省了开销。

2.6 select实现socket多并发

import socket
import queue
import select

def main():
	server=socket.socket()
	server.bind(('127.0.0.1',7000))
	server.listen(1000)
	server.setblocking(False)#设置为非阻塞
	inputDatas=[server,]
	outputDatas=[]
	msg_dic={}
	while True:
		#监测inputDatas里面的内容
		readList,writeList,exceptionList=select.select(inputDatas,outputDatas,inputDatas)
		for r in readList:
			if r is server:#如果是server代表来了个新连接
				client,addr=server.accept()
				print('来了个新连接',addr)
				inputDatas.append(client)
				msg_dic[client]=queue.Queue()#初始化一个队列,存要发送给客户端的数据
			else:#否则代表客户端发送来了数据
				data=r.recv(1024)
				print(data.decode())
				msg_dic[r].put(data)
				outputDatas.append(r)#放入返回的连接队列
		for w in outputDatas:
			w.send(msg_dic[w].get())
			outputDatas.remove(w)#确保下次循环时不返回已经处理完的连接
		for e in exceptionList:
			if e in outputDatas:
				outputDatas.remove(e)
			inputDatas.remove(e)
			del msg_dic[e]
if __name__ == "__main__":
	main()

2.7 selectors实现socket多并发

import selectors
import socket
sel = selectors.DefaultSelector()
def accept(sock, mask):
    conn, addr = sock.accept()  # Should be ready
    print('accepted', conn, 'from', addr,mask)
    conn.setblocking(False)
    sel.register(conn, selectors.EVENT_READ, read) #新连接注册read回调函数
def read(conn, mask):
    data = conn.recv(1024)  # Should be ready
    if data:
        print('echoing', repr(data), 'to', conn)
        conn.send(data)  # Hope it won't block
    else:
        print('closing', conn)
        sel.unregister(conn)
        conn.close()
sock = socket.socket()
sock.bind(('localhost', 9999))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept)#注册accept回调函数

while True:
    events = sel.select() #默认阻塞,有活动连接就返回活动的连接列表
    for key, mask in events:
        callback = key.data #accept
        callback(key.fileobj, mask) #key.fileobj=  文件句柄

猜你喜欢

转载自blog.csdn.net/GoldenKitten/article/details/86500120
今日推荐