基于 python 协程的并发编程实践!简直不能在详细了!

前言

假设有一批小文件,每个文件都可以通过 mysql load 的方式导入数据库,请问如何操作可以取得较小的时间和资源消耗?

关于这个需求,我们自然会想到各种并发实现方式,比如多进程和多线程。由于众所周知的多进程切换的高昂代价以及在某些场合下需要考虑多进程之间的协调和通信,如果情非得已,恐怕很少会使用到多进程。然而在本文讨论的 python 世界中,多线程可能也不是一个好的选择。详见下文论述。

线程模型

我们知道操作系统的任务调度是基于内核调度实体(KSE,Kernel Scheduling Entity),所以线程的实现也是基于内核调度实体,也就是通过跟内核调度实体绑定实现自身的调度。根据线程与内核实体的对应关系上的区别,线程的实现模型大致可以分为两大类:内核级线程和用户级线程。

- 内核级线程模型

线程与内核线程 KSE 是一对一(1 : 1)的映射模型,也就是每一个用户线程绑定一个实际的内核线程,而线程的调度则完全交付给操作系统内核去做,应用程序对线程的创建、终止以及同步都基于内核提供的系统调用来完成,大部分编程语言的线程库 (比如 Java 的 java.lang.Thread、C++ 的 std::thread 等等) 都属于内核级线程模型。这种模型的优势和劣势同样明显:优势是实现简单,直接借助操作系统内核的线程以及调度器,所以 CPU 可以快速切换调度线程,于是多个线程可以同时运行,因此相较于用户级线程模型它真正做到了并行处理;但它的劣势是,由于直接借助了操作系统内核来创建、销毁和以及多个线程之间的上下文切换和调度,因此资源成本大幅上涨,且对性能影响很大。

- 用户级线程模型

线程与内核线程 KSE 是多对一(N : 1)的映射模型,多个用户线程的一般从属于单个进程并且多线程的调度是由用户自己的线程库来完成,线程的创建、销毁以及多线程之间的协调等操作都是由用户自己的线程库来负责而无须借助系统调用来实现。许多语言实现的协程库 基本上都属于这种方式(比如 python 的 gevent)。由于线程调度是在用户层面完成的,避免了系统调用和 CPU 在用户态和内核态之间切换的开销,因此对系统资源的消耗会小很多,然而该模型有个问题:假设在某个用户进程上的某个用户线程因为一个阻塞调用(比如 I/O 阻塞)而被 CPU 给中断(抢占式调度)了,整个进程将被挂起。因此,该模型并不能做到真正意义上的并发。

python 线程

我们广泛使用的 python 是基于 CPython 实现,然而由于 CPython 的内存管理不是线程安全的,于是引入了一个全局解释锁(Global Interpreter Lock)来保障 Python 的线程安全。正是因为 GIL 的存在,每个线程执行前都需要获取锁,从而导致多线程的并发性大大削弱,完全无法发挥多核的优势。同时 python 的线程切换是基于字节码指令的条数,因此对于 I/O 密集型计算密集型任务勉强还有用武之地,然而对于计算密集型任务,多线程切换的开销将使多线程成为鸡肋,执行效率反而不如单线程。以下是一个验证例子:

顺序执行的单线程 (single_thread.py)

#! /usr/bin/python
 
from threading import Thread
import time
 
def my_counter():
    i = 0
    for _ in range(100000000):
        i = i + 1
    return True
 
def main():
    thread_array = {}
    start_time = time.time()
    for tid in range(2):
        t = Thread(target=my_counter)
        t.start()
        t.join()
    end_time = time.time()
    print("Total time: {}".format(end_time - start_time))
 
if __name__ == '__main__':
    main()

同时执行的两个并发线程 (multi_thread.py)

#! /usr/bin/python
 
from threading import Thread
import time
 
def my_counter():
    i = 0
    for _ in range(100000000):
        i = i + 1
    return True
 
def main():
    thread_array = {}
    start_time = time.time()
    for tid in range(2):
        t = Thread(target=my_counter)
        t.start()
        thread_array[tid] = t
    for i in range(2):
        thread_array[i].join()
    end_time = time.time()
    print("Total time: {}".format(end_time - start_time))
 
if __name__ == '__main__':
    main()

在 mac os,4 核 8G 内存 1.8MHz python3.7 上测试执行,多线程比单线程慢 2 秒!

另外值得一提的是尽管存在 GIL,但 python 多线程仍然不是线程安全的,对于共享状态的场合仍然需要借助锁同步。既然 python 多线程如此之糟,有没有一种线程切换代价更小和占用资源更低的技术呢?下面该轮到协程闪亮登场了!

协程

协程(Coroutine)又称微线程,属于用户级线程。上文中提到的 gevent 就是一种协程实现方式,除了 gevent 还有 asyncio。下文详细介绍。上文中,我们介绍了用户级线程就是在一个内核调度实体上映射出来的多个用户线程,用户线程的创建、调度和销毁完全由用户程序控制, 对内核调度透明:内核一旦将 cpu 分配给了线程,该 cpu 的使用权就归该线程所有,线程可以再次按照比如时间片轮转等常规调度算法分配给每个微线程,从而实现更大的并发自由度,但所有的微线程只能在该 cpu 上运行,无法做到并行。为了便于理解,我们这里把协程看作这些映射出来的“微线程”。用户程序控制的协程需要解决线程的挂起和唤醒、现场保护等问题,然而区别于线程的是协程不需要处理锁和同步问题,因为多个协程是在一个用户级线程内进行的,但需要处理因为单个协程阻塞导致整个线程(进程)阻塞的问题。下图展示线程和协程的对照关系:

生成器 - 协程基础

理解协程的挂起和唤醒,不得不提到生成器。生成器也是函数,但跟普通的函数稍有区别,请看下面定义的生成器:

def countdown(n):
    while n> 0:
        yield n
        n -= 1

调用 countdown 并不会执行,如果 print 该函数,会发现返回的是 generator 实例对象。

只有通过 next()函数来执行生成器函数。yield 命令产生了一个值,然后挂起函数,直到下一个 next() 函数。当生成器函数遇到 return 或结束,停止迭代数据。除了 next,还可以使用 send 激活生成器,两者可以交替使用。比如下面生成斐波那契数列的生成器:

def myfibbo(num):
    a,b=0,1
    count=0
    while count<num:
        a,b=a+b,a
    #yield b 是将b 返回给外部调用程序。
    #ret = yield 可以接收外部程序通过send()发送的信息,并赋值给ret
        ret = yield b
        count+=1
        print("step into here,count={},ret={}".format(count,:ret))

第一次当生成器处于 started 状态时,只能 send(None),否则会报错,当生成器 while 条件不满足退出时,会抛出异常 StopIteration, 如果生成器有返回值,会保存在 exception 的 value 属性中。

生成器首先是个迭代器,因此生成器可以嵌套调用子生成器。

def reader():
    # 模拟从文件读取数据的生成器,for表达式可以简写为:yield from range(4)
    #for i in range(4):
    #    yield i
    yield from range(4)
    
def reader_wrapper():
    yield from reader()
    
wrap = reader_wrapper()
for i in wrap:
    print(i)

在这里 yield from 同时起到了一个提供了一个调用者和子生成器之间的透明的双向通道的作用: 从子生成器获取数据以及向子生成器传送数据。通过上述生成器的例子中,我们已经大体感知到协程的影子了,但还是不够直观,而且不是正在意义上的协程,只是实现的代码执行过程中的挂起,唤醒操作。我们再介绍一个真正的协程实现库 greelet, 知名的网络并发框架如 eventlet,gevent 都是基于它实现的。

greenlet

from greenlet import greenlet

def test1():
    print(12)
    gr2.switch()
    print(34)

def test2():
    print(56)
    gr1.switch()
    print(78)

gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()

上例中创建了两个 green

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值