慕课⽹Flask⾼级编程实战-5.Flask中的多线程和线程隔离技术5.1 线程进程与多线程
1.进程
资源是稀缺的,进程是竞争计算机资源的基本单位。
⼀台计算机⾄少要有⼀个进程。
单核CPU⽤眼只能执⾏⼀个应⽤程序吗?No,他可以在不同的应⽤程序直接进⾏切换。由于切换的时间⾮常短,短到⼈类⽆法感知到,所以造成了⼀种假象,⼀个进程可以同时执⾏多个进程。
多核CPU可以真正意义上的同时运⾏多个进程。
切换进程是计算机的⼀个最重要的功能-进程调度。每个算法有⾃⼰不同的操作系统来决定每个应⽤程序何时挂起,并切换到另⼀个进程。
进程/线程直接的切换,对资源的开销⾮常⼤。所以这是设计⼀个应⽤程序的关键。
操作系统在将⼀个进程切换到另⼀个进程的时候,会保存当前进程的状态,这个状态叫上下⽂。
2.线程
对于计算机编程来说,线程的重要性要远⾼于进程,⼀个进程可以有多个线程。
线程产⽣的原因:现在CPU的运⾏速度⾮常快,⽤进程来管理资源粒度太⼤了,不能够有效充分的利⽤CPU的⾼性能,所以需要⼀个更⼩的单元来管理和使⽤CPU的资源。
线程⽐进程更加灵活⼩巧轻量,并且切换线程的开销要⽐进程⼩很多的,这能让我们在单位之间内处理更多的事情。
线程进程分⼯不同,进程负责分配资源,如内存资源;线程利⽤CPU执⾏代码(代码落在计算机最底部,是⼀条条的指令,由CPU来执⾏,指令是需要资源才能完成他的功能的)
线程不管理和持有资源,但是他可以访问进程的资源来完成功能。
3.多线程
每段代码执⾏起来,都会有⼀个进程,并且会分配⼀个默认的线程,成为主线程
可以通过python内置threading来操作线程
import threading
# 获取当前线程
t = print(threading.current_thread())
# 获取线程的名字
Name())
def worker():
t = print(threading.current_thread())
Name())
# 创建⼀个新的线程,target是线程要运⾏的函数,name是线程名
new_t = threading.Thread(target=worker,name='new_thread')
# 启动⼀个线程
newt.start()
复制代码
多线程编程的好处:更加充分的利⽤CPU的性能优势。可以将不同的线程运⾏在不同的核⾥。
多线程只是看起来是多个线程同时执⾏,实际上是交替执⾏,只是交替的实际⾮常短。
多线程编程是异步编程:1.主线程完全不知道其他线程的存在。2.主线程不依赖其他线程的执⾏结果。
5.2 Python和Flask的多线程
python不能充分利⽤多核CPU的优势:
因为python有⼀个全局解释器锁GIL(Global Interceptor Lock),这个全局解释器锁会不管有多少个核,我们⼀个进程有多少个线程,都限制了同⼀时间只能在⼀个核上运⾏⼀个线程。
为了解决线程安全问题引⽤了锁,python中有两种锁
细粒度锁:由程序员⼿动加锁
粗粒度锁:有python在解释器上加锁,同⼀时间只能运⾏⼀个核。
python有很多全局解释器,如cpython和jpython。GIL只是加在最常见的cpython上的,所以使⽤jpython可以去掉这个限制。
也可以采⽤多进程的⽅式来解决这个问题,但是这样会带来其他的问题,如多进程通信技术,进程切换带来的开销等
python的多线程到底是不是鸡肋
我们的程序从⼀个时间段内最多的操作类型,可以分类IO密集型和CPU密集型
IO密集型:⼀个时间段内,查询数据库,请求⽹络资源,读写⽂件等操作居多的,则属于IO密集型。
CPU密集型:⼀个时间段内,严重依赖CPU计算资源的操作(如圆周率的计算)⾮常多,则属于CPU密集型。
⽽我们⼤部分的编程都是IO密集型的,对于IO密集型的程序来源,Python的多线程是有意义的,例如在进⾏⼀个数据库查询的时候,⼤部分的时间都是在等待结果返回,等待的这段时间并不消耗CPU的资源,不如让给其他线程去执⾏。
flask的多线程以及问题
flask内置了webServer服务器,⼀般在⽣产环境会替换掉这个默认的服务器
flask内置的webServer服务器默认是使⽤单进程单线程的⽅式执⾏的,多个请求进来必须要⼀个⼀个的执⾏,不能同时执⾏,这是因为⾃⼰调试⼤部分情况下单进程单线程就⾜够了。
如果要开启多线程模式,只需要在运⾏Flask核⼼app对象的时候,设置关键字参数threaded=True即可。如果要开启多进程,将processes设置为True即可。
观察Flask多线程模式 的技巧 在新版pycharm上,启⽤debug模式(是⼯具的debug,不是Flask的debug),在视图函数上打⼀个断点,然后再浏览器上发多次请求,可以看到,处理每个请求的线程不⼀样
Flask在单线程的时候,request永远都指向的是为了处理当前请求⽽实例化的Requset对象,这没有问题;在多线程的时候,由于request只是⼀个变量,但是多个线程处理多个请求,就会实例化不同的Request对象,在某⼀时刻request变量指向哪⼀个Request 对象是不确定的,这时候,如果在代码中获取request并且视图修改的话就会遇到问题
5.3 Flask中的线程隔离
Flask内部,通过维护⼀个dict来实现线程隔离。伪代码如下 request={thread_key1:Request1,thread_ke
y2:Request2} 其中
thread_key是线程的唯⼀id号,Request就是每次请求的Request对象
Flask内部引⼊了⼀个werkzeug的库,这个库⾥有⼀个local模块,⾥⾯有⼀个Local对象,Flask内部线程隔离就是通过操作Local对象实现的。
1. Local对象
Local对象实际上就是对字典原理的⼀个封装
class Local(object):
__slots__ = ('__storage__', '__ident_func__')
def __init__(self):
# ⼀个私有变量__storage__字典
object.__setattr__(self, '__storage__', {})
object.__setattr__(self, '__ident_func__', get_ident)
def __iter__(self):
return iter(self.__storage__.items())
def __call__(self, proxy):
"""Create a proxy for a name."""
return LocalProxy(self, proxy)
def __release_local__(self):
self.__storage__.pop(self.__ident_func__(), None)慕课网资源在哪里
def __getattr__(self, name):
try:
return self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
def __setattr__(self, name, value):
# 取当前线程的线程ID号
ident = self.__ident_func__()
storage = self.__storage__
# 操作字典
try:
storage[ident][name] = value
except KeyError:
# 把线程id号作为key保存了起来
storage[ident] = {name: value}
def __delattr__(self, name):
try:
del self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
复制代码
使⽤线程隔离和不适⽤线程隔离的区别
定义⼀个对象,启动⼀个线程去修改这个对象,使⽤主线程打印这个对象
import threading
import time
class A:
b = 1
my_obj = A()
def worker():
my_obj.b = 2
new_thread = threading.Thread(target=worker, name="new_thread") new_thread.start()
time.sleep(1)
# 主线程
print(my_obj.b)
复制代码
# 打印结果为2
# 因为my_obj是主线程和新线程共享的对象
复制代码
将my_obj实例化改为使⽤Local线程隔离对象
import threading
import time
from werkzeug.local import Local
class A:
b = 1
my_obj = Local()
my_obj.b = 1
def worker():
my_obj.b = 2
print("in new thread b is: ", my_obj.b)
new_thread = threading.Thread(target=worker, name="new_thread") new_thread.start()
time.sleep(1)
print("in main thread b is:", my_obj.b)
复制代码
# 结果
in new thread b is: 2
in main thread b is: 1
复制代码
由于my_obj是⼀个线程隔离的对象,所以我们在新线程⾥修改my_obj是不会影响主线程⾥my_obj中的值的。他们保持了两个线程之间的数据的独⽴
Local的⾼明在于,他不需要我们去关⼼底层Local字典内部的细节,我们之间去操作Local对象的相关属性,这个操作本就是线程隔离的,给我们带来了很⼤的⽅便
2. 线程隔离的栈:LocalStack
接下来来继续讲解之前这张图右下⾓的部分。 通过Flask的源码,我们可以了解到_app_ctx_stack和_request_ctx_stack实际上是指向了LocalStack()对象,也就是⼀个线程隔离的栈,下⾯来看下源码
# context locals
# 是⼀个LocalStack对象
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))
复制代码
LocalStack源码,依旧在werkzeug库 的local模块下
class LocalStack(object):
"""This class works similar to a :class:`Local` but keeps a stack
of objects instead. This is best explained with an example::
>>> ls = LocalStack()
>>> ls.push(42)
>>> ls.top
42
>>> ls.push(23)
>>> ls.top
23
>>> ls.pop()
23
>>> ls.top
42
They can be force released by using a :class:`LocalManager` or with
the :func:`release_local` function but the correct way is to pop the
item from the stack after using. When the stack is empty it will
no longer be bound to the current context (and as such released).
By calling the stack without arguments it returns a proxy that resolves to
the topmost item on the stack.
.. versionadded:: 0.6.1
"""
def __init__(self):
# 内部维护了⼀个Local对象作为私有变量
self._local = Local()
def __release_local__(self):
self._local.__release_local__()
def _get__ident_func__(self):
return self._local.__ident_func__
def _set__ident_func__(self, value):
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论