进程
资源分配的基本单位,分配 内存,CPU等。
代码 + 用到的资源
并行 真的同时进行;并发 在一段时间里都运行,不是同时的。
会牵扯到 文件描述符,软连接 这些底层的系统知识。
Linux中 一切皆文件,用文件描述符标识每个文件对象,每个进程最多可以有9个文件描述符,前三个是保留的,用于标准输入输出,标准错误输出。
例如:对于一个套接字,实际上就是 一个文件的 文件描述符(012之外的),指向这个文件,
当使用多进程时,会复制一份程序和资源,用来处理这个套接字,那么就有了两个 链接 连接到这个文件,所以要分别调用 close() 才算是把这个套接字关闭。
而对于,多线程,则不会复制这个套接字。
多进程:
import multiprocessing
p = multiprocessing.Process(target = 函数名, args = (参数列表,))
p.start()
会耗费较多的资源。写时拷贝,就是共用代码,当不同的地方时,拷贝一份代码。
在子进程里产生的异常信息,不会显示
进程间通信:
对应单台计算机中的进程间通信的小样式:通过队列的方式来发送和接受消息。
def 函数名1(q):
q.put(数据)
def 函数名2(q):
q.get()
#创建一个队列,用于存放进程的数据
q = multiprocessing.Queue()
p1 = multiprocessing.Process(target = 函数名1, args = (q,))
p2 = multiprocessing.Process(target = 函数名2, args = (q,))
p1.start()
p2.start()
进程池:
当要创建的进程很多时,手动用上述方式创建较慢。可以用multiprocessing模块的Pool方法。
可以指定最大进程数,当没有达到时,有新请求就创建一个新的,当达到最大进程数时,就需要等待有进程结束,才会用之前的进程执行新任务。重复利用进程,进而消减了进程的创建和销毁消耗。
pool = multiprocessing.Pool(n) # 最多创建n个进程。
pool.apply_async(函数名,(参数列表,)) #可以调用 多余n个(可以用for循环来循环创建),来创建多个进程,
# 会利用进程池中的进程来完成任务
pool.close() # 关闭进程池,不会再接受新的请求
pool.join() # 等待pool 中的所有进程接受,放在close之后。就是避免主进程的代码执行完后的主进程结束。
线程
线程依存于进程中,开销小,但不利于资源的管理和保护 ,没有独立的内存资源。操作系统调度时间片的基本单位。
多线程:
import threading
t = threading.Thread(target = 函数名, args = (参数列表,))
t.start()
每调用一次,就会创建一个线程来执行这个函数。 threading.Thread 只是创建了一个子线程对象,只有这个对象调用start时才实际创建出子线程来执行函数代码。
查看当前有哪些线程在运行:
threading.enumerate()
会返回 描述线程的列表
当 线程运行的函数结束时,这个子线程也结束
如果主线程提前结束,那么运行的所有子线程也结束。
通过继承 Thread类来完成创建线程。
class MyThread(threading.Thread):
def run(self):
语句。。。
if __name__ = "__main__":
t = MyThread()
t.start()
当继承Thread类后,必须定义 run()方法,方这个类调用strat()时会自动调用run()方法。也就是说,可以在这个类中定义别的其他方法,然后放入run()方法中来调用,这样就可以运行多个函数啦。
多线程共享全局变量:
在一个函数中,对全局变量进行修改时,是否需要使用 global 进行声明,要看是否对全局变量的指向进行了修改。
如果是 全局变量 等于 一个新的值,那么就需要加global.
也可以把全局变量当做参数传入函数,也可以达到共享的效果。
共享数据可以为处理带来很大的方便,比如一个线程抓取数据,一个线程清洗数据,一个线程发送清洗好的数据。
共享全局变量可能带来的问题:
资源竞争问题。当一个线程正在处理某个全局变量时,另一个也去处理,(本该在前一个处理完的基础上再取值进行处理的),这样前一个处理完后会覆盖后一个处理的结果,这样就乱套了。
同步,协同步调,按预定的先后顺序执行。
当某个线程处理共享变量时 ,将其锁定,是其他线程不能处理,等该线程处理完,释放资源后, 其他线程才可以再锁定该资源来使用。
#创建锁
mutex = threading.Lock(0
#上锁,如果之间锁没被上,就上锁成功,如果之前锁在别处被上,此时 阻塞在这里等待锁被解开
muxtex.acquire()
执行处理全局变量的代码,被上下两个包围的代码越少越好,核心的共享资源处用锁就好
# 解锁
mutex.release()
协程
迭代器:
迭代是访问 集合元素的一种方式,迭代器是一个可以记住遍历到的位置的对象,迭代器只能从集合的第一个元素开始直到所有元素被访问完,迭代器只能往前不会后退。
迭代器中是 直接生成全部数据存放起来。
例如:range(10) ,在Python2中会立马返回一个列表,xrange(10) 什么时候调用什么时候返回。
除了用于 for , 对于 list,tuple 也可以接受迭代器,来把数据转换为相应的形式,也是得益于迭代器的特性。
一个创建迭代器的示例:
# coding=utf-8
# from collections import Iterable, Iterator
class MyIterableClass:
def __init__(self):
self.names = list()
def add(self, name):
self.names.append(name)
# 如果想要一个对象成为一个可迭代的对象,需要 实现__iter__方法,其需要返回一个迭代器
def __iter__(self):
return MyIterator(self)
class MyIterator: # 一个类中只要包含者两个方法iter和next,就是一个 迭代器
def __init__(self, obj):
self.obj = obj
self.current_num = 0
def __iter__(self):
pass
def __next__(self):
if self.current_num < len(self.obj.names):
ret = self.obj.names[self.current_num]
self.current_num += 1
return ret
else:
raise StopIteration
# 上述两个和为一
class MyIterableClassTwo:
def __init__(self):
self.names = list()
self.current_num = 0
def add(self, name):
self.names.append(name)
# 如果想要一个对象成为一个可迭代的对象,需要 实现__iter__方法,其需要返回一个迭代器
def __iter__(self):
return self
def __next__(self):
if self.current_num < len(self.names):
ret = self.names[self.current_num]
self.current_num += 1
return ret
else:
raise StopIteration
if __name__ == '__main__':
name_iter = MyIterableClassTwo()
name_iter.add("wl")
name_iter.add("96")
name_iter.add("jiang")
for name in name_iter:
print(name)
# 判断name_iter是否为可迭代对象
# print(isinstance(name_iter, Iterable))
# 判断 iter(name_iter) ,是否返回一个 迭代器
# print(isinstance(iter(name_iter), Iterator))
生成器:
是一种特殊的迭代器,只要 函数里 有 yield 关键字,那么这个函数就变成了生成器。调用时会创建并返回一个生成器对象。
在执行到 yield 行时,会返回 后面的值,并阻塞在这里,等下次再取值时,会重新从这一行后面开始执行。也就是每次next()时,才继续进行后面的数据生成。
# 生成斐波那契数列
def create_num(nums):
a,b = 0,1
current_num = 0
while current_num < nums:
yield a # 卡在这里,被再次next时,继续执行后面的 ,还可以 ret = yield a ,在生成器调用send()时,会把发来的数据,给到ret,然后生成器继续之前的运行。
a,b = b, a+b
current_num += 1
return “OK....” //当生成器把数据生成完时,在next()的捕获异常处,会收到返回值
fact = create_num(10)
num = next(fact)
num2 = fact.send(123或None)
生成器可以这样创建: (x*3 for x in range(10)) 返回的是一个生成器,调用时才会来生成需要的数据,如果把()换为 [ ] 会直接生成一个列表。
用生成器实现多任务: 并发的形式
import time
# 生成器 并发的多任务
def task1():
while True:
print("------1------")
time.sleep(1)
yield
def task2():
while True:
print("-------2-----")
time.sleep(1)
yield
def main():
t1 = task1()
t2 = task2()
while True:
next(t1)
next(t2)
if __name__ == '__main__':
main()
greenlet : 对yield 进行了封装,可以更简便的达到上述效果:
from greenlet import greenlet
import time
def task1():
while True:
print("------1------")
gr2.switch()
time.sleep(1)
def task2():
while True:
print("-------2-----")
gr1.switch()
time.sleep(1)
if __name__ == '__main__':
gr1 = greenlet(task1)
gr2 = greenlet(task2)
gr1.switch()
greenlet 已经实现 了协程,但是还需要 代码说明切换。
gevent进行了更进一步的封装:,能够在一个 greenlet 遇到 IO操作时,自动切换到其他 greenlet 执行,当IO完成,再切换回来执行。
需要把延时或耗时的操作,换为gevent里提供的,例如 gevent.socket(),gevent.sleep()
如果之前的好多代码没有使用 gevent里提供的耗时模块,
可以 使用 from gevent import monkey monkey.patch_all(), nam , 会自动把 time.sleep()换为 gevent.sleep()
import gevent
def f(n):
for i in range(n):
print(gevent.getcurrent(), i)
gevent.sleep(0.5)
g1 = gevent.spawn(f,5)
g2 = gevent.spawn(f,4)
g3 = gevent.spawn(f,3)
g1.join()
g2.join()
g3.join()
# 上述六行代码还可以写为:
gevent.joinall([
gevent.spawn(f,5),
gevent.spawn(f,4),
gevent.spawn(f,3)
])
协程依赖于线程,也就是 在一个线程中,遇到 耗时的任务时(如网路下载等待时),转而执行同一个线程里的其他任务。
利用单线程,处理多个请求,而不阻塞
在单进程单线程里,把套接字设置为 非阻塞,可以 实现 服务器处理多个请求。
设置非阻塞的方式: 套接字.setblocking(false)
def main():
"""整体控制"""
# 1.创建套接字,返回一个 特定类型的套接字对象,还有待于确定具体的值。
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.绑定本机IP和端口,用于 客户端找到 特定的进程
tcp_server_socket.bind(("", 7891))
# 3.变为 监听套接字,等待 客户端发送请求 来激活阻塞,可以指定能够接受的最大连接数
tcp_server_socket.listen(128)
tcp_server_socket.setblocking(False)
client_socket_list = list()
while True:
# 实现 可以处理多个请求,用单进程单线程的方式
try:
new_socket, client_addr = tcp_server_socket.accept()
except Exception as ret:
print("---没有新客户到来,继续监听")
else:
print("如果没有上个异常,就是来了新的客户端的访问")
new_socket.setblocking(False) # 设置套接字为非阻塞的方式
client_socket_list.append(new_socket)
for client_socket in client_socket_list:
try:
recv_data = client_socket.recv(1024)
except Exception as ret:
print("客户的数据还没有发送过来")
else:
if recv_data:
print("客户端发来了数据")
# 可以调用service_high 来出来请求。函数在后一段代码中
else:
# 客户端调用close,发来了空的数据
client_socket_list.remove(client_socket)
client_socket.close()
# 关闭监听套接字
tcp_server_socket.close()
上面这个 虽然是 HTTP/1.1 但是 由于 手动调用了 close() ,所以效果像 短连接。
建立长连接的代码:
改动 发送回到 header ,添加 Content-Length:body的长度,
这样,浏览器就知道,此次的数据发送完啦。
改为长连接形式后的代码:
def main():
"""整体控制"""
# 1.创建套接字,返回一个 特定类型的套接字对象,还有待于确定具体的值。
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.绑定本机IP和端口,用于 客户端找到 特定的进程
tcp_server_socket.bind(("", 7891))
# 3.变为 监听套接字,等待 客户端发送请求 来激活阻塞,可以指定能够接受的最大连接数
tcp_server_socket.listen(128)
tcp_server_socket.setblocking(False)
client_socket_list = list()
while True:
time.sleep(2)
# 4.等待客户端的连接,若有客户端发来请求,会返回一个客户端的套接字用于 数据的回传
# new_socket, client_addr = tcp_server_socket.accept()
# 5.为这个客户端的请求做处理
# service_client(new_socket)
# service_high(new_socket)
# 实现 可以处理多个请求,用单进程单线程的方式
try:
new_socket, client_addr = tcp_server_socket.accept()
except Exception as ret:
print("---没有新客户到来,继续监听")
else:
print("如果没有上个异常,就是来了新的客户端的访问")
new_socket.setblocking(False) # 设置套接字为非阻塞的方式
client_socket_list.append(new_socket)
for client_socket in client_socket_list:
try:
recv_data = client_socket.recv(1024)
except Exception as ret:
print("客户的数据还没有发送过来")
else:
if recv_data:
print("客户端发来了数据")
# 进行数据的处理。。。可以调用service_client
service_high(client_socket,recv_data)
else:
# 客户端调用close,发来了空的数据
client_socket_list.remove(client_socket)
client_socket.close()
# 关闭监听套接字
tcp_server_socket.close()
# 修改为长连接的形式
def service_high(new_socket,request):
# 1. 接受浏览器发来的请求,并解析
# request = new_socket.recv(1024).decode("utf-8")
request_lines = request.decode("utf-8").splitlines()
print("=" * 30)
print(request_lines)
ret = re.match(r"[^/]+(/[^ ]*)", request_lines[0])
if ret:
file_name = ret.group(1)
if file_name == "/":
file_name = "/index.html"
# 返回 HTTP 格式的数据给 浏览器
try:
f = open("../frontEnd" + file_name, 'rb')
except:
response = "HTTP/1.1 404 NOT FOUND\r\n"
response += "Content-Length:%d\r\n" % len("---- file not find----")
response += "\r\n"
response += "---- file not find----"
new_socket.send(response.encode("utf-8"))
else:
response_body = f.read()
f.close()
response_header = "HTTP/1.1 200 OK\r\n"
response_header += "Content-Length:%d\r\n" % len(response_body)
response_header += "\r\n"
response = response_header.encode("utf-8") + response_body
new_socket.send(response)
# new_socket.close() 把这行去除了,这样就不是短连接的方式了
上面这个代码的运行效率是不高的,因为当 访问的客户端很多事,需要遍历的套接字也很多,每次遍历都要把用户态的套接字,复制到操作系统内核中,等待检测是否有文件到来,这样遍历一遍就很耗时。是轮询的方式处理套接字。
epoll
web静态服务器epoll 提供了 一个 应用程序和操作系统内核 共用的内存(内存映射技术mmp),避免了复制的耗时,并且以 事件通知的形式来处理每个套接字。
epoll在Python中的简单应用:
import select
# 创建一个epoll对象
epl = select.epoll()
# 向epoll中注册 套接字, 第一个参数 文件描述符,第二个参数 什么事件类型,是接受还是发送还是错误输出
epl.register(套接字对象.fileno(), select.EPOLLIN)
# 默认会阻塞,当数据到达套接字的对应的文件时,操作系统检测到,以事件通知的方式,告诉程序,并解阻塞
# 会返回元祖列表,(文件描述符,事件类型)
fd_event_list = epl.poll()
for fd,event in fd_event_list :
根据这些找到相应的套接字对象,再进行相应的处理。可以利用字典来预先标记对应的fd和套接字。