一 客户端/服务器架构
即C/S架构,包括
1、硬件C/S架构(打印机)
2、软件B/S架构(web服务)
C/S架构与Socket的关系:
我们学习Socket就是为了完成C/S的开发
二 OSI七层
引子:
计算机组成原理:硬件、操作系统、应用软件三者组成。
具备以上条件后,计算机就可以工作,如果你要和别人一起玩,那你就需要上网了。互联网的核心就是由一堆协议组成,协议就是标准。
为什么学习Socket之前要先了解互联网协议?
1、C/S架构的软件(应用软件属于应用层)是基于网络进行通信的
2、网络的核心即一堆协议,协议即标准,想开发一款基于网络通信的软件,就必须遵循这些标准
OSI七层:
三 Socket层
四 Socket是什么
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,在设计模式中,Socket其实就是一个门面模式,它把负责的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入学习理解TCP/UDP协议,Socket已经为我们封装好了,我们只需要遵循Socket的规定去编程,写出的程序自然就是遵循TCP/UDP标准的。
五 套接字发展史及分类
套接字起源于20世纪70年代加利福尼亚大学伯克利分校版本的Unix,即人们所说的BSD Unix。因此,有时人们也把套接字成为“伯克利套接字”或“BSD套接字”。一开始,套接字被设计用在一台主机上多个应用程序之间的通信,这也被称作进程间通许或IPC。套接字有两种(或者称为两个种族),分别是基于文件型和就网络型。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
UNIX一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器上,可以通过访问同一文件系统间接完成通信。
基于网络类型的套接字家族
套接字家族的名字:AF_INET
还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,Python支持很多地址家族,但是由于我们只关心网络编程,所以大部分时候我们只使用AF_INET(AF:Address Family;INET:Internet)
六 套接字工作流程
生活中,你要打电话给一个朋友,先拨号,朋友听到电话铃声响后接打电话,这时你和你的朋友就建立起了连接,就可以讲话了,等交流结束,挂断电话结束此次通话。
利用Socket模拟生活中打电话:
服务端
客户端
服务器和客户端无限循环发送消息:
服务器端
客户端
Socket收发消息原理图:
若重启服务端时,可能会遇到:Address already in use;这个是由于服务端扔然存在四次挥手的time_wait状态占用地址
解决方案:
方法一
方法二
七 基于UDP的套接字
udp服务端
1 ss = socket() # 创建一个服务器的套接字 2 ss.bind() # 绑定服务器套接字 3 while True: # 服务器无限循环 4 cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送) 5 ss.close() # 关闭服务器套接字
udp客户端
1 cs = socket() # 创建客户套接字 2 while True: 3 cs.sendto()/cs.recvfrom() # 对话(发送/接收) 4 cs.close() # 关闭客户套接字
基于UDP的套接字:
udp服务端
udp客户端
八 什么是粘包?
注:只有TCP有粘包现象,UDP永远不会粘包
一个socket收发消息的原理图:
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
- TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
- UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
- tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
粘包解决方案:
方法一、
基于TCP服务端
基于TCP客户端
方法二、
基于TCP服务端
基于TCP客户端
九 利用socketserver实现并发
基于TCP服务端:
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 import socketserver 4 5 6 class MyServer(socketserver.BaseRequestHandler): 7 def handle(self): 8 print('conn is:', self.request) # <==>conn 9 print('addr is:', self.client_address) # <==> addr 10 while True: 11 try: 12 # 收消息 13 data = self.request.recv(1024) 14 if not data:break 15 print('收到客户端的消息是', data) 16 # 发消息 17 self.request.sendall(data.upper()) 18 except Exception as EX: 19 print('错误提示:',EX) 20 break 21 22 23 if __name__ == '__main__': 24 s = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyServer) # 多线程;第一个参数,地址+端口;第二个参数,类 25 # s = socketserver.ForkingTCPServer(('127.0.0.1', 8080), MyServer) # 多进程;多进程的开销大于多线程 26 s.serve_forever()
基于TCP客户端:
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 from socket import * 4 ip_port = ('127.0.0.1', 8080) 5 back_log = 5 6 buffer_size = 1024 7 tcp_client = socket(AF_INET, SOCK_STREAM) 8 tcp_client.connect(ip_port) 9 while True: 10 msg = input('>>').strip() 11 if not msg:continue 12 if msg == 'quit':break 13 tcp_client.send(msg.encode('utf-8')) 14 data = tcp_client.recv(buffer_size) 15 print('收到服务端发来的消息:', data.decode('utf-8')) 16 tcp_client.close()
基于UDP服务端
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 import socketserver 4 5 6 class MyServer(socketserver.BaseRequestHandler): 7 def handle(self): 8 print(self.request) 9 print('收到客户端的消息是:', self.request[0].upper()) 10 self.request[1].sendto(self.request[0].upper(), self.client_address) 11 12 13 if __name__ == '__main__': 14 s = socketserver.ThreadingUDPServer(('127.0.0.1', 8080), MyServer) # 第一个参数,地址+端口;第二个参数,类 15 s.serve_forever()
基于UDP客户端
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 from socket import * 4 ip_port = ('127.0.0.1', 8080) 5 buffer_size = 1024 6 udp_client = socket(AF_INET, SOCK_DGRAM) # SOCK_DGRAM:数据报式套接字 7 while True: 8 msg = input('>>').strip() 9 udp_client.sendto(msg.encode('utf-8'), ip_port) 10 data, addr = udp_client.recvfrom(buffer_size) 11 print(data.decode('utf-8')) 12 print(addr)
十 认证客户端的合法性
如果想在分布式系统中实现一个简单的客户端链接认证功能,又不像SSL那么复杂,那么可以利用hmac+加盐的方法来实现。
服务端
客户端(合法,其他均为非法)
十一 FTP服务器
实现如下功能:
1、用户加密认证
2、每个用户都有自己的家目录,且只能访问自己的家目录
3、允许用户在ftp server上随意切换目录(cd)
4、允许用户查看当前目录下的所有文件(ls)
5、允许上传和下载文件
6、文件传输过程中显示进度条
7、支持文件的断点续传
ftp server
目录结构:
ftp_server.py
accounts.cfg
settings.py
main.py
server.py
ftp client
目录结构
ftp_client.py
十二 进程与线程
1、为什么要有操作系统?
现代计算机系统是由一个或者多个处理器、内存、硬盘、打印机、键盘、鼠标和显示器等组成的。网络接口以及各种其他输入/输出设备组成的复杂系统,每位程序员不可能掌握所有系统实现的细节,并且管理优化这些部件是一件具有挑战性极强的工作。所以,我们需要为计算机安装一层软件,成为操作系统,任务就是用户程序性提供一个简单清晰的计算机模型,并管理以上设备。
定义:操作系统是一个用来协调、管理和控制计算机硬件和软件资源的系统程序,它位于硬件和应用程序之间。程序是运行在系统上的具有某种功能的软件,比如:浏览器,音乐播放器等。
操作系统内部的定义:操作系统的内核是一个管理和控制程序,负责管理计算机的所有物理资源,其中包括:文件系统、内存管理、设备管理、进程管理。
2、什么是进程?
假如有两个程序A和B,程序A在执行到一半的过程中,需要读取大量的数据输入(I/O操作),而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源。是不是在程序A读取数据的过程中,让程序B去执行,当程序A读取完数据之后,让程序B暂停,然后让程序A继续执行?当然没问题,但这里有一个关键词:切换;既然是切换,那么这就涉及到了状态的保存,状态的恢复,加上程序A与程序B所需要的系统资源(内存,硬盘,键盘等等)是不一样的。自然而然的就需要有一个东西去记录程序A和程序B分别需要什么资源,怎样去识别程序A和程序B等等,所以就有了一个叫进程的抽象。
定义:
进程就是一个程序在一个数据集上的一次动态执行过程。进程一般由程序、数据库、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成。数据集则是程序在执行过程中所需要使用的资源。进程控制块用来记录的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
本质上就是一段程序的运行过程(抽象的概念)
3、什么是线程?
线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,让进程内并发成为可能。
4、进程与线程区别
1、一个程序至少有一个进程,一个进程至少有一个线程(进程可以理解成线程的容器)
2、进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
3、线程在执行过程中与进程还是有区别的,每个独立的线程有一个程序运行的入口,顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
4、进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位;线程是进程的一个实体,是CPU调度和分源的基本单位,它是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源,只拥有一点运行中必不可少的资源(如程序计数器,一组寄存器和钱),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。一个进程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。
5、线程:最小的执行单元(实例);进程:最小的资源单位
5、Python的GIL(全局解释锁;Global Interpreter Lock)
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
上面的核心意思:无论你启多少个线程,你有多少个CPU,Python在执行的时候会淡定的在同一时刻只允许一个线程运行。
6、 线程的两种调用方式
threading 模块建立在thread 模块之上。thread模块以低级、原始的方式来处理和控制线程,而threading 模块通过对thread进行二次封装,提供了更方便的api来处理线程。
调用方式:
方式一、
threading
方式二、
继承方式调用线程
join():在子线程完成运行之前,这个子线程的父线程将一直被阻塞。
setDaemon(True):
将线程生命为守护线程,必须在start()方法调用之前设置,如果不设置为守护线程,程序会被无限挂起。这个方法基本和join是相反的。当我们在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程就分兵两路,分别运行,那么当主线程完成想退出时,会验证子线程是否完成。如果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是,只要主线程完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以用setDaemon方法了。
其他方法:
1 # run():用于表示线程活动的方法 2 # start():启动线程活动 3 # isAlive():返回线程是否活动的,返回布尔值,True/False 4 # getName():返回线程名字 5 # setName():设置线程名字 6 7 threading模块提供的一些方法: 8 # threading.currentThread():返回当前的线程变量 9 # threading.enumerate():返回一个包含正在运行的线程的list。正在运行指线程启动后-结束前,不包括启动前和终止后的线程 10 # threading.activeCount():返回正在运行的线程数量,与len(threading.enumerate())有相同的结果
其他方法演示
7、同步锁(lock)
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 import threading 4 import time 5 num = 100 6 7 8 def sub(): 9 global num 10 print('ok') 11 lock.acquire() # 加锁 12 temp = num 13 time.sleep(0.001) 14 num = temp-1 15 lock.release() # 释放锁 16 17 18 li = [] 19 lock = threading.Lock() 20 for i in range(100): 21 t1 = threading.Thread(target=sub) 22 t1.start() 23 li.append(t1) 24 for l in li: 25 l.join() 26 print(num)
注:多个线程都在同时操作同一个共享资源,所以造成了资源破坏(join会造成串行,失去线程的意义),可以通过同步锁来解决这种问题。
8、递归锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都在使用,所有这两个线程在无外力作用下将一直等待下去。
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 import threading 4 import time 5 6 7 class MyThread(threading.Thread): 8 9 def actionA(self): 10 r_lock.acquire() 11 print(self.name, 'gotA', time.ctime()) # 重写线程后的self.name --->线程的名字 12 time.sleep(2) 13 r_lock.acquire() 14 print(self.name, 'gotB', time.ctime()) 15 time.sleep(1) 16 r_lock.release() 17 r_lock.release() 18 19 def actionB(self): 20 r_lock.acquire() 21 print(self.name, 'gotB', time.ctime()) # 重写线程后的self.name --->线程的名字 22 time.sleep(2) 23 r_lock.acquire() 24 print(self.name, 'gotA', time.ctime()) 25 time.sleep(1) 26 r_lock.release() 27 r_lock.release() 28 29 def run(self): 30 self.actionA() 31 self.actionB() 32 33 34 if __name__ == '__main__': 35 r_lock = threading.RLock() 36 li = [] 37 for t in range(3): 38 t = MyThread() 39 t.start() 40 li.append(t) 41 42 for i in li: 43 i.join() 44 45 print('end')
为了支持在同一线程中多次请求同一资源,Python提供了“可重入锁”:threading.Rlock。Rlock内部维护着一个Lock和counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
9、同步对象(Event)
An event is a simple synchronization object;the event represents an internal flag,
and threads can wait for the flag to be set, or set or clear the flag themselves.
event = threading.Event()
# a client thread can wait for the flag to be set
event.wait()
# a server thread can set or reset it
event.set()
event.clear()
If the flag is set, the wait method doesn’t do anything.
If the flag is cleared, wait will block until it becomes set again.
Any number of threads may wait for the same event.
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 import threading 4 import time 5 6 7 class Boss(threading.Thread): 8 def run(self): 9 print('Boss:今天加班到22:00!\r\n') 10 print(event.isSet()) # False 11 event.set() 12 time.sleep(6) 13 print('Boss:可以下班了,明天放假!\r\n') 14 print(event.isSet()) 15 event.set() 16 17 18 class Worker(threading.Thread): 19 def run(self): 20 event.wait() # 一旦event被设定,等同于pass 21 print('Worker:唉···命真苦!\r\n') 22 time.sleep(1) 23 event.clear() 24 event.wait() 25 print('Worker:OhYeah!\r\n') 26 27 28 if __name__ == '__main__': 29 event = threading.Event() 30 threads = [] 31 for i in range(5): 32 threads.append(Worker()) 33 threads.append(Boss()) 34 for t in threads: 35 t.start() 36 for t in threads: 37 t.join() 38 print('end')
10、信号量
信号量用来控制线程并发数的,BoundedSemaphore或Semaphore管理一个内置的计数器,每当调用acquire()时-1,调用release()时+1。计数器不能小于0,当计数器为0时,acquire()将阻塞线程至同步锁状态,直到其他线程调用release()。(类似于停车位的概念)BoundedSemaphore与Semaphore的唯一区别在于前者将在调用release()时检查计数器的值是否超过了计数器的初始值,如果超过了将抛出一个异常。
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 import threading 4 import time 5 6 7 class MyThread(threading.Thread): 8 def run(self): 9 if semaphore.acquire(): 10 print(self.name, '\r') 11 time.sleep(5) 12 semaphore.release() 13 14 15 if __name__ == '__main__': 16 semaphore = threading.Semaphore(5) 17 threads = [] 18 for i in range(100): 19 threads.append(MyThread()) 20 for t in threads: 21 t.start()
11、队列(queue)
列表是不安全的数据结构:
不安全的列表
queue队列类的方法:
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 import queue # 线程队列 4 q = queue.Queue() # 创建q对象,同步实现的。队列长度可为无限或者有限。可通过Queque的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限 5 q.put(12) # 将一个值放到队列,调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;第二个block为可选参数,默认为1.如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空处一个数据单元。如果block为0,put方法将引发Full异常 6 q.put('alex') 7 q.put({"age": 15}) 8 print(q.get()) # 将一个值从队列中取出,调用队列对象的get()方法,从对头删除并返回一个实例。可选参数为block,默认为True。如果队列为空且block为True,get()就使调用线程暂停,直至有项目可用。如果队列为空且block为False,队列将引发Empty异常 9 print(q.qsize()) # 返回队列的大小 10 print(q.empty()) # 判断队列是否为空,返回布尔值,True/False 11 print(q.full()) # 判断队列是否已经满了,返回布尔值,True/False 12 q.join() # 实际上意味着等到队列为空,再执行别的操作 13 14 ''' 15 Queue模块的三种队列及构造函数 16 1、Python Queue模块的FIFO队列,先进先出 class queue.Queue(maxsize) 17 2、LIFO 类似于堆,即先进后处。 class queue.LifoQueue(maxsize) 18 3、优先级队列,级别越低月先出来。 class queue.PriorityQueue(maxsize) 19 '''
生产者消费者模型
为什么要使用生产者和消费者模式?
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
什么是生产者消费者模式?
生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
生产者和消费者举例
12、并发&并行
并发:指系统具有处理多个任务(动作)的能力
并行:指系统具有同时处理多个任务(动作)的能力
13、同步&异步
同步:当进程执行到一个IO(等待外部数据)的时候你等
异步:当进程执行到一个IO(等待外部数据)的时候你不等;一直等到数据接收完成,在回来处理
14、任务类型
IO密集型:Python的多线程是有意义的
计算密集型:Python的多线程就不推荐,可以采用多进程+协程
16、多进程模块( multiprocessing)
M
is a package that supports spawning processes using an API similar to the threading module. The ultiprocessing
package offers both local and remote concurrency,effectively side-stepping the Global Interpreter Lock by using subprocesses instead of threads. Due to this, the multiprocessing
module allows the programmer to fully leverage multiple processors on a given machine. It runs on both Unix and Windows.multiprocessing
由于GIL的存在,Python中的多线程其实并不是真正的多线程,如果想充分地使用多核CPU的资源,在Python中大部分情况下需要使用多进程。multiprocessing包是Python中的多进程管理包。与threading.Thread类似,它可以利用multiprocessing.Process对象来创建一个进程。该进程可以运行在Python程序内部编写的函数。该Process对象与Thread对象的用法,也有start(),run(),join()的方法。此外multiprocessing包中也有Lock/Event/Semaphore/Condition类(这些对象可以像多线程那样,通过参数传递给各个进程),用以同步进程,其用法与threading包中的同名类一致。所以,multiprocessing的很大一部分与threading使用同一套API,只不过换到了多进程的情景。
调用方式一:
方式一
调用方式二:
方式二
Process类
构造方法:
Process([group [, target [, name [, args [, kwargs]]]]])
group:线程组,目前还没有实现,库引用中提示必须是None
target:要执行的方法
name:进程名
args/kwargs:要传入方法的参数
实例方法:
is_alive():返回进程是否在运行
join([timeout]):阻塞当前上下文环境的进程,直到调用此方法的进程终止或到达指定的timeout(可选参数)
start():进程准备就绪,等待CPU调度
run():start()调用run方法,如果实例进程时未指定传入target,这start执行t默认run()方法
terminate():不管任务是否完成,立即停止工作进程
属性:
daemon:和线程的setDeamon功能一样
name:进程名字
pid:进程号
17、进程的通信
进程队列Queue
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 import queue 4 import multiprocessing 5 6 7 def Foo(q): 8 q.put(123) 9 q.put(456) 10 11 12 if __name__ == '__main__': 13 q = multiprocessing.Queue() # 注意:此处需用进程队列,不能用线程队列,即q=queue.Queue() 14 p = multiprocessing.Process(target=Foo, args=(q,)) 15 p.start() 16 print(q.get()) 17 print(q.get())
管道
The Pipe()
function returns a pair of connection objects connected by a pipe which by default is duplex (two-way). For example:
View Code
Managers
Queue和pipe只是实现了数据交互,并没实现数据共享,即一个进程去更改另一个进程的数据。
A manager object returned by Manager()
controls a server process which holds Python objects and allows other processes to manipulate them using proxies.
A manager returned by Manager()
will support types list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value and Array. For example:
View Code
进程同步
Without using the lock output from the different processes is liable to get all mixed up.
View Code
进程池
进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进程,那么程序就会等待,直到进程池中有可进程为止。
进程池中两个方法:
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 from multiprocessing import Process, Pool 4 import time, os 5 6 7 def Foo(i): 8 time.sleep(1) 9 print('i = \r', i) 10 11 12 def Bar(arg): # 此处arg=Foo()函数的返回值 13 print('pgid-->%s\r' % os.getpid()) 14 print('ppid-->%s\r' % os.getppid()) 15 print('logger:%s\r' % arg) 16 17 18 if __name__ == '__main__': 19 pool = Pool(5) 20 Bar(1) 21 print('------------\r') 22 for i in range(10): 23 # pool.apply(func=Foo, args=(i,)) # 同步接口 24 # pool.apply_async(func=Foo, args=(i,)) 25 pool.apply_async(func=Foo, args=(i,), callback=Bar) # callback-->回调函数:就是某个动作或者函数执行成功后再去执行的函数
26 pool.close() 27 pool.join() # join和close位置不能反 28 print('end\r')
十三 协程
协程:又称微线程,英文名:Coroutine,本质上是一个线程
优点1:协程具有极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制。因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
优点2:不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法就是多进程+协程,即充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
yield简单的实现--->协程
Greenlet
greenlet是一个用C实现的协程模块,相比与Python自带的yield,它可以使你在任意函数之间随意切换,而不需把这个函数先声明为generator。(注:需要用pip安装包;pip install gevent)
greenlet
gevent
gevent
十四 缓存I/O
缓存I/O又被称作标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内的缓冲区拷贝到应用程序的地址空间。用户空间没法直接访问内核空间的,内核态到用户态的数据拷贝。
缓存I/O的缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存开销是非常大的。
I/O发生时设计的对象和步骤:
对于一个network IO(以read举例),他会涉及到两个系统对象,一个是调用这个IO的process(or thread),另一个就是系统内核(kernel)。当一个read操作发生时,它会经历两个阶段:
1、等待数据准备(Waiting for the data to be read)
2、将数据从内核拷贝到进程中(Copying the data from the kernel to the process)
注:这两点很重要,因为这些IO Mode的区别就是在这两个阶段上各有不同的情况。
blocking IO(阻塞IO,Linux下)
在Linux中,默认情况下所有的socket都是blocking,一个典型的读操作大概流程图:
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一阶段:准备数据。对于network IO来说,很多时候数据在一开始还没到达(如:还没收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就将数据从kernel中拷贝到用户内存,然后kernel。所以,blocking IO的特点就是在IO执行的两个阶段都被block了。
non-blocking IO(非阻塞IO,Linux下)
在Linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行时大概的流程:
从上图可以看出,当用户进程发出read时,如果kernel中的数据还没准备好,那么它并不会block用户进程,而是立即返回一个error。从用户进程角度讲来讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了。所以,用户进程其实是需要不断的主动询问kernel数据好了没有。
非阻塞-socket-server
非阻塞-socket-client
IO multiplexing(IO多路复用)
有些地方也称为这种IO方式为event driven IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程,大概流程图:
当用户进程调用了select,那么真个进程会被block。而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这时用户进程再调用read操作,将数据从kernel拷贝到用户进程。(如果处理的连接数不是很多的话,使用select/epoll的web server不一定比使用multi-threading+blocking IO的web server性能更好,可能延迟更大;select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接)
注:
1、select函数返回结果中如果有文件可读了,那么进程就可以同故宫调用accept()或recv()来让kernel将位于内核中准备到的数据copy到用户区。
2、select的优势在于可以处理多个连接,不适用于单个连接。
多路复用-select-server
多路复用-select-client
Asynchronous I/O(异步IO)
流程图:
从图中可以看出,用户进程发起read操作之后,立刻就开始去做其它的事。另一方面,从kernel的角度,当他受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
selectors模块:
selectors模块-server
客户端