• 主页
  • 相册
  • 随笔
  • 目录
  • 存档
Total 244
Search AboutMe

  • 主页
  • 相册
  • 随笔
  • 目录
  • 存档

Flask浅析-1

2020-05-19

本文是理解Flask,尤其是要处理异步io(asyncio)的前置知识

1. 前置知识

现在一般python做web主要有三大部分,web server, web framework, async io.

  • web server 承担端口监听和接受请求的任务
  • web framework 主要承担路由,业务逻辑等任务。有了web server和web framework基本就能运行了
  • server读取解析请求,生成environ和start_response,然后调用middleware;
  • middleware完成自己的处理部分后,可以继续调用下一个middleware或application,形成一个完整的请求链;
  • application位于请求链的最后一级,其作用就是生成最终的响应

1.1. sync vs async(同步与异步)

synchronous I/O在執行I/O operation的時候會將process/thread給block住, 而asynchronous則不同, 其在user process觸發I/O操作後, 就直接回傳去做別的事了, 等到kernel處理完I/O後, 會發送一個信號給user process, 說I/O已經結束了, 在這段過程中, user process就沒有被block住(就是叫別人去幫你等東西或是做事喇).

再換個講法, 區別這兩者的關鍵就是到底是誰在進行真正的I/O, 如果是主執行緒, 那就是synchronous, 若是衍生出來的子執行緒, 待子執行緒完成I/O operation後回報給主執行緒, 這就是asynchronous.


  • Synchronous I/O: 包含了blocking I/O, nonblocking, I/O multiplexing(selector), 以及signal-driven I/O
  • Asynchronous I/O: 就是asynchronous I/O, 但它跟nonblocking還是差很多的

异步编程基本上是指事先不知道执行顺序的编程

1.2. block vs non-block(阻塞与非阻塞)

blocking I/O基本上會讓user process進入block的狀態, 直到操作完成才會繼續作業, 而nonblocking則是在kernel還在準備資料的情況下會立刻回傳

1.2.1. non-block

當user process呼叫recvfrom之後, 若kernel這邊的資料還沒有準備好, 就不要block user process了, 反之, 立刻回傳一個error(EWOULDBOLCK). 所以站在user process的視角來看, 呼叫recvfrom後並不用卡在那邊等待, 而是可以立刻得到一個結果. 當user process發現回傳的是error時, 就可以知道資料還沒準備好, 這時就可以再次發送recvfrom操作. 當kernel這邊準備好資料後, 且又再次收到來自user process的system call時, kernel就可以把資料copy到user process中(maybe application buffer), 然後回傳結果.

所以, nonblocking其實就是user process要不斷地去問kernel說資料好了沒. 這在application中的做法, 基本上就是用一個loop去一直call recvfrom, 這其實就是我們常說的polling. 儘管這種方式看來很浪費CPU時間, 但似乎還是滿常見的.

1.2.1.1. non-block vs async

nonblocking I/O跟asynchronous I/O的差異其實很明顯. 對nonblocking來說, 確實大部分時間都沒有被block, 但是user process還是要主動地去做check的動作, 然後在資料準備完後, 也要自己主動呼叫recvfrom去把資料copy至application buffer中; 而對asynchronous來說, 則是user process把I/O operation整個委託給kernel去完成, 然後kernel完成後會再發信號通知user process, 這樣一來, user process就不用自己去check還有copy資料了, 因為這些都會由kernel來代勞

1.3. Asynchronous I/O

异步I/O是计算机操作系统对输入输出的一种处理方式:发起I/O请求的线程不等I/O操作完成,就继续执行随后的代码,I/O结果用其他方式通知发起I/O请求的程序。与异步I/O相对的是更为常见的“同步(阻塞)I/O”:发起I/O请求的线程不从正在调用的I/O操作函数返回(即被阻塞),直至I/O操作完成。

1.4. coroutine

由于python的多线程基本就是个摆设,python web中一般用协程+异步IO的方式来实现并发。gevent就是一个协程+异步IO的库,其作用是将阻塞的应用变为非阻塞,来提高并发量。

协程是计算机程序的一类组件,推广了协作式多任务的子程序,允许执行被挂起与被恢复

协程可以通过yield(取其“让步”之义而非“出产”)来调用其它协程,接下来的每次协程被调用时,从协程上次yield返回的位置接着执行;通过yield方式转移执行权的协程之间不是调用者与被调用者的关系,而是彼此对称、平等的


与子例程区别

  • 子例程的生命期遵循后进先出;协程的生命期由对它们的使用需要来决定
  • 子例程的起始处是惟一的入口点;协程可以有多个入口点,协程的起始处是第一个入口点,每个yield返回出口点都是再次被调用执行时的入口点
  • 子例程只在结束时一次性的返回全部结果值;协程可以在yield时不调用其他协程,而是每次返回一部分的结果值(生成器或迭代器)

例子:生产者-消费者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var q := 新建队列

coroutine 生产者
loop
while q 不满载
建立某些新产品
向 q 增加这些产品
yield 给消费者

coroutine 消费者
loop
while q 不空载
从 q 移除某些产品
使用这些产品
yield 给生产者

协程非常类似于线程。但是协程是协作式多任务的,而线程典型是抢占式多任务的。这意味着协程提供并发(concurrency)而非并行(parallelism)。

协程超过线程的好处是它们可以用于硬性实时的语境(在协程之间的切换不需要涉及任何系统调用或任何阻塞调用),这里不需要用来守卫关键区块的同步性原语(primitive)比如互斥锁、信号量等,并且不需要来自操作系统的支

1.4.1. 抢占式与协作式

协作式多工(Cooperative Multitasking),是一种多工方式,多工是使电脑能同时处理多个程序的技术,相对于抢占式多工(Preemptive multitasking),协作式多工要求每一个运行中的程序,定时放弃自己的执行权利,告知操作系统可让下一个程序执行,使用户错觉以为各个程序都同时被执行


抢占式多任务处理(Preemption)是计算机操作系统中,一种实现多任务处理(multi task)的方式,相对于协作式多任务处理而言。抢占式环境下,操作系统完全决定进程调度方案,操作系统可以剥夺耗时长的进程的时间片,提供给其它进程(一般地,每个任务赋予一个优先级)

协程允许一个执行过程A中断,然后转到执行过程B,在适当的时候再一次转回来,有点类似于多线程。但协程有以下2个优势:

  • 协程的数量理论上可以是无限个,而且没有线程之间的切换动作,执行效率比线程高
  • 协程不需要“锁”机制,即不需要lock和release过程,因为所有的协程都在一个线程中。
  • 相对于线程,协程更容易调试debug,因为所有的代码是顺序执行的。

1.4.2. python协程

对基于生成器的协程的支持已弃用并计划在 Python 3.10 中移除

1
2
3
4
5
6
@asyncio.coroutine
def old_style_coroutine():
yield from asyncio.sleep(1)

async def main():
await old_style_coroutine()

ps:
yield: 运行态 =》 就绪态(而非阻塞态)

1.4.2.1. 生成器与协程
  • 生成器yield:带暂停的return
    • send:使得人们不仅可以暂停生成器,还可以将值送回暂停的生成器中(意味着可以手工控制协程)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      def jumping_range(up_to):
      """Generator for the sequence of integers from 0 to up_to, exclusive.

      Sending a value into the generator will shift the sequence by that amount.
      """
      index = 0
      while index < up_to:
      jump = yield index
      if jump is None:
      jump = 1
      index += jump


      if __name__ == '__main__':
      iterator = jumping_range(5)
      print(next(iterator)) # 0
      print(iterator.send(2)) # 2
      print(next(iterator)) # 3
      print(iterator.send(-1)) # 2
      for x in iterator:
      print(x) # 3, 4
      • 这个例子可以理解yield的“_返回值_”
  • yield from

    For simple iterators, yield from iterable is essentially just a shortened form of for item in iterable: yield item

    1
    2
    3
    4
    5
    6
    >>> def g(x):
    ... yield from range(x, 0, -1)
    ... yield from range(x)
    ...
    >>> list(g(5))
    [5, 4, 3, 2, 1, 0, 1, 2, 3, 4]

    但是,与普通的循环不同,”yield from “允许子生成器直接从调用的作用域中接收发送和抛出的值,并返回一个最终值给外部生成器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    >>> def accumulate():
    ... tally = 0
    ... while 1:
    ... next = yield
    ... if next is None:
    ... return tally
    ... tally += next
    ...
    >>> def gather_tallies(tallies):
    ... while 1:
    ... tally = yield from accumulate()
    ... tallies.append(tally)
    ...
    >>> tallies = []
    >>> acc = gather_tallies(tallies)
    >>> next(acc) # Ensure the accumulator is ready to accept values
    >>> for i in range(4):
    ... acc.send(i)
    ...
    >>> acc.send(None) # Finish the first tally
    >>> for i in range(5):
    ... acc.send(i)
    ...
    >>> acc.send(None) # Finish the second tally
    >>> tallies
    [6, 10]


    驱动这一变化的主要理由是允许即使是设计成使用发送和抛出方法的生成器也可以像一个大函数可以被分割成多个子生成器一样容易

    • 这里还值得注意的一点是yield没有抛出的值
    • 本质上看好像还是将双层嵌套改为单层的语法糖

      由于重构变得更容易,因此 yield from 也可以让你把生成器链在一起,这样就可以在不需要做任何特别的代码的情况下,让值在调用堆栈中上下浮动

    • yield from后面必须跟iterable对象(可以是生成器,迭代器)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> def chain(*iterables):
... for it in iterables:
... for i in it:
... yield i
>>> s = 'ABC'
>>> t = tuple(range(3))
>>> list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]


>>> def chain(*iterables):
... for i in iterables:
... yield from i
...
>>> list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]
1.4.2.2. 事件循环(Event Loop)

事件循环提供了这样的一个循环:”当A发生时,做B(when A happens then do B)“。基本上,事件循环会关注某事发生的时间,当事件循环关注的事情发生时,它就会调用任何关注发生的代码。在 Python 3.4 中,Python 在标准库中以 asyncio 的形式获得了一个事件循环。

事件循环是每个 asyncio 应用的核心。事件循环会运行异步任务和回调,执行网络 IO 操作,以及运行子进程

1.4.2.3. asyncio

asyncio 是用来编写 并发 代码的库,使用 async/await 语法。

asyncio 被用作多个提供高性能 Python 异步框架的基础,包括网络和网站服务,数据库连接库,分布式任务队列等等

  • asyncio 提供一组 高层级 API 用于:
    • 并发地 运行 Python 协程 并对其执行过程实现完全控制
  • 还有一些 低层级 API:
    • 创建和管理 事件循环,以提供异步 API 用于 网络化, 运行 子进程,处理 OS 信号 等等;
1
2
3
4
5
6
7
8
9
import asyncio

async def main():
print('Hello ...')
await asyncio.sleep(1)
print('... World!')

# Python 3.7+
asyncio.run(main())
1.4.2.4. 历史
  • Python 2.2 中引入了生成器让代码的执行被暂停的生成器
  • Python 2.5 中引入了将值送回暂停的生成器的能力
  • Python 3.3 中加入了yield from
  • Python 3.4 asyncio

1.5. multithreading

……这段代码是并发的,但不是并行的。它之所以还是比较快,是因为这是一个**IO绑定的任务(IO bound task)**。在下载这些图像的时候,处理器几乎是不费吹灰之力,大部分时间都是在等待网络上的时间。这就是为什么Python多线程可以提供很大的速度提升的原因。每当其中一个线程准备好要做一些工作时,处理器就可以在这些线程之间切换。在Python或任何其他解释语言中使用线程模块与GIL,实际上会导致性能下降。如果你的代码正在执行CPU绑定的任务,比如解压gzip文件,使用线程模块会导致执行时间变慢。对于CPU绑定的任务和真正的并行执行,我们可以使用多进程模块。


虽然事实上参考Python的实现–CPython–有一个GIL,但并不是所有的Python实现都是如此。例如,使用 .NET 框架的 Python 实现 IronPython 没有 GIL,基于 Java 的实现 Jython 也没有 GIL

1.6. multiprocessing

为了使用多个进程,我们创建一个多进程池。通过它提供的map方法,我们将把URL的列表传递给Pool……这就是真正的并行化,但它也是有代价的。脚本的整个内存会被复制到每个被催生的子进程中。在这个简单的例子中,这并不是什么大问题,但对于非琐碎的程序来说,这很容易成为严重的开销


multiprocessing 模块允许程序员充分利用机器上的多核。可运行于 Unix 和 Windows 。

multiprocessing 模块还引入了在 threading 模块中没有的API。一个主要的例子就是 Pool 对象,它提供了一种快捷的方法,赋予函数并行化处理一系列输入值的能力,可以将输入数据分配给不同进程处理(数据并行)

1
2
3
4
5
6
7
8
9
10
from multiprocessing import Pool

def f(x):
return x*x

if __name__ == '__main__':
with Pool(5) as p:
print(p.map(f, [1, 2, 3]))

[1, 4, 9]

1.6.1. Process 类

在 multiprocessing 中,通过创建一个 Process 对象然后调用它的 start() 方法来生成进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from multiprocessing import Process
import os

def info(title):
print(title)
print('module name:', __name__)
print('parent process:', os.getppid())
print('process id:', os.getpid())

def f(name):
info('function f')
print('hello', name)

if __name__ == '__main__':
info('main line')
p = Process(target=f, args=('bob',))
p.start()
p.join()

main line
('module name:', '__main__')
('parent process:', 877)
('process id:', 950)
function f
('module name:', '__main__')
('parent process:', 950)
('process id:', 951)
('hello', 'bob')

1.6.2. 启动方法

  • spawn
    • 父进程启动一个新的Python解释器进程。子进程只会继承那些运行进程对象的 run() 方法所需的资源
    • 相对于使用 fork 或者 forkserver,使用这个方法启动进程相当慢
  • fork
    • 父进程使用 os.fork() 来产生 Python 解释器分叉
    • 父进程的所有资源都由子进程继承
    • 只存在于Unix
  • forkserver
    • 每当需要一个新进程时,父进程就会连接到服务器并请求它分叉一个新进程。分叉服务器进程是单线程的,因此使用 os.fork() 是安全的。没有不必要的资源被继承
    • 存在于Unix

1.6.3. 在进程之间交换对象

  • Queue
  • Pipe

1.6.4. 进程间同步

对于所有在 threading 存在的同步原语,multiprocessing 中都有类似的等价物。例如,可以使用锁来确保一次只有一个进程打印到标准输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from multiprocessing import Process, Lock

def f(l, i):
l.acquire()
try:
print('hello world', i)
finally:
l.release()

if __name__ == '__main__':
lock = Lock()

for num in range(10):
Process(target=f, args=(lock, num)).start()

1.6.5. Process

class multiprocessing.Process(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

  • join([timeout])
    • 如果可选参数 timeout 是 None (默认值),则该方法将阻塞,直到调用join()方法的进程终止。如果 timeout 是一个正数,它最多会阻塞 timeout 秒。请注意,如果进程终止或方法超时,则该方法返回 None 。

1.6.6. Queue

class multiprocessing.Queue([maxsize])

  • 返回一个使用一个管道和少量锁和信号量实现的共享队列实例。当一个进程将一个对象放进队列中时,一个写入线程会启动并将对象从缓冲区写入管道中。
  • get([block[, timeout]])
    • 从队列中取出并返回对象。
    • 如果可选参数 block 是 True (默认值) 而且 timeout 是 None (默认值), 将会阻塞当前进程,直到队列中出现可用的对象
1
2
3
4
5
6
7
8
9
10
11
from multiprocessing import Process, Queue

def f(q):
q.put([42, None, 'hello'])

if __name__ == '__main__':
q = Queue()
p = Process(target=f, args=(q,))
p.start()
print(q.get()) # prints "[42, None, 'hello']"
p.join()

2. 参考

  • gunicorn和uwsgi是怎么在使用gevent的,gunicorn/uwsgi和gevent? - 知乎
  • Python Multithreading Tutorial: Concurrency and Parallelism | Toptal
  • multiprocessing — Process-based parallelism — Python 3.8.3 documentation
  • 协程与任务 — Python 3.8.3 文档
  • Python进阶:理解Python中的异步IO和协程(Coroutine),并应用在爬虫中 - 知乎
  • 淺談I/O Model - Carl - Medium
  • How the heck does async/await work in Python 3.5?
  • 理解Python协程:从yield/send到yield from再到async/await_python_程序老兵的博客-CSDN博客
  • Python Web开发之——构建基于Flask框架的web后端项目 - 简书
  • Flask
  • Program Language
  • Python
  • Web
Python反射
awk笔记
  1. 1. 1. 前置知识
    1. 1.1. 1.1. sync vs async(同步与异步)
    2. 1.2. 1.2. block vs non-block(阻塞与非阻塞)
      1. 1.2.1. 1.2.1. non-block
        1. 1.2.1.1. 1.2.1.1. non-block vs async
    3. 1.3. 1.3. Asynchronous I/O
    4. 1.4. 1.4. coroutine
      1. 1.4.1. 1.4.1. 抢占式与协作式
      2. 1.4.2. 1.4.2. python协程
        1. 1.4.2.1. 1.4.2.1. 生成器与协程
        2. 1.4.2.2. 1.4.2.2. 事件循环(Event Loop)
        3. 1.4.2.3. 1.4.2.3. asyncio
        4. 1.4.2.4. 1.4.2.4. 历史
    5. 1.5. 1.5. multithreading
    6. 1.6. 1.6. multiprocessing
      1. 1.6.1. 1.6.1. Process 类
      2. 1.6.2. 1.6.2. 启动方法
      3. 1.6.3. 1.6.3. 在进程之间交换对象
      4. 1.6.4. 1.6.4. 进程间同步
      5. 1.6.5. 1.6.5. Process
      6. 1.6.6. 1.6.6. Queue
  2. 2. 2. 参考
© 2024 何决云 载入天数...