• python通用规范-6


    文章目录

    6.1 线程
    6.1.1 多线程适用于阻塞式IO场景,不适用于并行计算场景
    6.1.2 建议使用Queue来协调各线程之间的工作
    6.2 协程
    6.3 并行
    6.1 线程

    6.1.1 多线程适用于阻塞式IO场景,不适用于并行计算场景

    Python的标准实现是CPython。
    CPython执行Python代码分为2个步骤:首先,将文本源码解释编译为字节码,然后再用一个解释器去解释运行字节码。字节码解释器是有状态的,需要维护该状态的一致性,因此使用了GIL(Global Interpreter Lock,全局解释器锁)。
    GIL的存在,使得CPython在执行多线程代码的时候,同一时刻只有一个线程在运行,无法利用多CPU提高运算效率。但是这个特点也带来了一个好处:CPython运行多线程的时候,内部对象缺省就是线程安全的。这个特性,被非常多的Python库开发者所依赖,直到CPython的开发者想要去除GIL的时候,发现已经有大量的代码库重度依赖
    这个GIL带来的内部对象缺省就是线程安全的特性,变成一个无法解决的问题了。
    虽然多线程在并行计算场景下无法带来好处,但是在阻塞式IO场景下,却仍然可以起到提高效率的作用。这是因为阻塞式IO场景下,线程在执行IO操作时并不需要占用CPU时间,此时阻塞IO的线程可以被挂起的同时继续执行IO操作,而让出CPU时间给其他线程执行非IO操作。这样一来,多线程并行IO操作就可以起到提高运行效率的作用了。
    综上,Python的标准实现CPython,由于GIL的存在,同一个时刻只能运行一个线程,无法充分利用多CPU提升运算效率,因此Python的多线程适用于阻塞式IO的场景,不适用于并行计算的场景。
    下面举一个对计算量有要求的求一个数的因数分解的代码实例,来说明Python多线程不适用于并行计算的场景:

    # -*- coding:utf-8 -*-
    from time import time
    from threading import Thread


    def factorize(number):
    for i in range(1, number + 1):
    if number % i == 0:
    yield i


    class FactorizeThread(Thread):
    def __init__(self, number):
    Thread.__init__(self)
    self.number = number

    def run(self):
    self.factors = list(factorize(self.number))


    def test(numbers):
    start = time()
    for number in numbers:
    list(factorize(number))
    end = time()
    print('Took %.3f seconds' % (end - start))


    def test_thread(numbers):
    start = time()
    threads = []
    for number in numbers:
    thread = FactorizeThread(number)
    thread.start()
    threads.append(thread)
    for t in threads:
    t.join()
    end = time()
    print('Mutilthread Took %.3f seconds' % (end - start))


    if __name__ == "__main__":
    numbers = [2139079, 1214759, 1516637, 1852285]
    test(numbers)
    test_thread(numbers)

    # 代码输出:
    # Took 0.319 seconds
    # Mutilthread Took 0.539 seconds
    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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    以上代码运行结果只是一个参考值,具体数据跟运行环境相关。但是可以看到单线程方式比多线程方式的计算速度要快。由于CPython运行多线程代码时因为GIL的原因导致每个时刻只有一个线程在运行,因此多线程并行计算并不能带来时间上的收益,反而因为调度线程而导致总时间花费更长。
    对于IO阻塞式场景,多线程的作用在于发生IO阻塞操作时可以调度其他线程执行非IO操作,因此在这个场景下,多线程是可以节省时间的。可以用以下的代码来验证:

    # -*- coding:utf-8 -*-
    from time import time
    from threading import Thread
    import os


    def slow_systemcall(n):
    for x in range(100):
    open("test_%s" % n, "a").write(os.urandom(10) * 100000)


    def test_io(N):
    start = time()
    for _ in range(N):
    slow_systemcall(_)
    end = time()
    print('Took %.3f seconds' % (end - start))


    def test_io_thread(N):
    start = time()
    threads = []
    for _ in range(N):
    thread = Thread(target=slow_systemcall, args=("t_%s" % _,))
    thread.start()
    threads.append(thread)
    for thread in threads:
    thread.join()
    end = time()
    print('Took %.3f seconds' % (end - start))


    if __name__ == "__main__":
    N = 5
    test_io(N)
    test_io_thread(N)
    # 代码输出:
    # Took 5.179 seconds
    # Multithread Took 1.451 seconds
    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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    可以看到单线程花费时间与多线程花费时间之比接近1:4,考虑线程调度的时间,这个跟一般语言的多线程起的作用比较相似。这是因为当Python执行IO操作时,实际上是执行了系统调用,此时线程会释放GIL,直到系统调用结束时,再申请获取GIL,也就是在IO操作期间,线程确实是并行执行的。
    Python的另外一个实现JPython就没有GIL,但是它并不是最常见的Python实现。

    6.1.2 建议使用Queue来协调各线程之间的工作

    如果Python程序同时要执行许多事务,那么开发者经常需要协调这些事务。而在各种协调方式中,较为高效的一种,则是采用函数管线。
    管线的工作原理,与制造业中的组装生产线(assembly line)相似。管线分为许多首尾相连的阶段,每个阶段都由一种具体的函数来负责。程序总是把待处理的新部件添加到管线的开端。每一种函数都可以在它所负责的那个阶段内,并发地处理位于该阶段的部件。等负责本阶段的那个函数把某个部件处理好之后,该部件就会传送到管线中的下一个阶段,以此类推,直到全部阶段都经历一遍。涉及阻塞式I/O操作或子进程的工作任务,尤其适合用此办法处理,这样的任务很容易分配到多个Python线程或进程中。
    例如,要构建一个照片处理系统,该系统从数码相机里面持续获取照片、调整其尺寸,并将其添加到网络相册中。这样的程序,可以采用三个阶段的管线来做。第一个阶段获取新图片,第二个阶段把下载好的图片传给缩放函数,第三个阶段把缩放后的图片交给上传函数。利用内置模块Queue中的Queue类来实现,可以变得容易且健壮。示例
    代码如下:

    from Queue import Queue
    from threading import Thread


    def download(item):
    print('download item')
    return item


    def resize(item):
    print('resize item')
    return item


    def upload(item):
    print('upload item')
    return item


    class ClosableQueue(Queue):
    SENTINEL = object()

    def close(self):
    self.put(self.SENTINEL)

    def __iter__(self):
    while True:
    item = self.get()
    try:
    if item is self.SENTINEL:
    return # Cause the thread to exit
    yield item
    finally:
    self.task_done()


    class StoppableWorker(Thread):
    def __init__(self, func, in_queue, out_queue):
    super(StoppableWorker, self).__init__()
    self.in_queue = in_queue
    self.out_queue = out_queue
    self.func = func

    def run(self):
    for item in self.in_queue:
    result = self.func(item)
    self.out_queue.put(result)


    if __name__ == '__main__':
    download_queue = ClosableQueue()
    resize_queue = ClosableQueue()
    upload_queue = ClosableQueue()
    done_queue = ClosableQueue()
    threads = [
    StoppableWorker(download, download_queue, resize_queue),
    StoppableWorker(resize, resize_queue, upload_queue),
    StoppableWorker(upload, upload_queue, done_queue),
    ]
    for thread in threads:
    thread.start()
    for _ in range(1000):
    download_queue.put(object())
    download_queue.close()
    download_queue.join()
    resize_queue.close()
    resize_queue.join()
    upload_queue.close()
    upload_queue.join()
    print('%s items finished' % done_queue.qsize())
    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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    要点:

    管线是一种优秀的任务处理方式,它可以把处理流程划分为若干阶段,并使用多条python线程来同事执行这些任务;
    构建并发式的管线时,要注意许多问题,其中包括:如何防止某个阶段陷入持续等待的状态之中、如何停止工作线程、以及如何防止内存膨胀等;
    Queue类所提供的机制可以彻底解决上述问题,它具备阻塞式的队列操作、能够指定缓冲区尺寸,而且还支持join方法,这使得开发者可以构建出健壮的管线;
    6.2 协程

    建议使用协程来处理并发场景
    Python程序员可以使用线程来运行多个函数,使这些函数看上去好像是在统一时间得到执行,然而,线程有其显著的缺点:

    多线程的运行协调起来比单线程过程式困难,需要依赖Lock来保证自己的多线程逻辑正确。
    每个在执行的线程,大约需要8MB内存,在线程处理逻辑较少而数量较多的工程模型中开销较大。
    线程启动、切换线程上下文的开销较大。
    Python的协程(coroutine)可以避免上述问题,它使得Python程序看上去好像是在同时运行多个函数,运行逻辑的调度由程序员自己决定。协程的实现方式,实际上是对生成器的一种扩展。启动生成器协程所需要的开销,与调用函数相仿。处于活跃状态的协程,在其耗尽之前,只会占用到不到1KB的内存。
    示例代码: 下面这段代码展示了Python利用yield表达式、生成器来实现的原生协程写法。 grep函数功能为筛选包含关键字的输入。 在定义生成器之后,需要使用next()启动生成器,此时生成器函数,会运行到yield表达式处等待输入。在这之后我们通过生成器的send方法向生成器传递输入,生成器就可以接着yield表达式处向下处理,处理完成后依靠while语句再次等待在yield表达式处。

    >>> def grep(pattern): # 生成器函数
    ... print("Searching for", pattern)
    ... while True:
    ... line = (yield) # yield表达式,生成器函数接收send的输入
    ... if pattern in line:
    ... print(line)
    ...
    >>> generator = grep("Python") # 定义生成器
    >>> next(generator) # 使用next启动生成器之后,会运行到yield表达式
    Searching for Python
    >>> generator.send("I love Python.") # 给生成器函数发送数据,继续yield处的运行
    I love Python.
    >>> generator.send("I love C++.")
    >>> generator.send("I love Java.")
    >>> generator.send("I love Python too.")
    I love Python too.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    如果在以上代码的基础上,连续推进多个独立的生成器,即可模拟出Python线程的并发行为,令程序看上去好像是在同时运行多个函数,同时其消耗相比多线程程序开销要小。

    6.3 并行

    建议使用concurrent.futures实现并行计算
    Python程序可以将独立的计算任务分配到多个CPU核上运行,提升并行计算的能力。Python的GIL使得无法使用线程实现真正的并行计算。Python程序的真正并行计算建议采用子进程的方式来实现,具体实现如下:

    对于并行计算的任务与主进程之间传递的数据比较少,且任务之间不需要共享状态和变量时,采用concurrent.furures的ProcessPoolExecutor类的简单实用方式即可实现并行计算;
    对于并行计算的场景不满足1)的状态时,可以采用multiprocessing模块提供的共享内存、进程锁、队列、代理等高级功能实现并行计算。因为使用复杂,所以如果不是特性场景,不建议使用这种方式;
    使用concurrent.furures的ProcessPoolExecutor类实现并行计算的示例代码如def calc_process():

    start = time.time()
    pool = ProcessPoolExecutor(max_workers=4)
    results = list(pool.map(gcd, numbers))
    end = time.time()
    print('process calc, Took %.3f seconds' % (end - start))
    print(results)
    ————————————————

    A thousand journey is started by taking the first step.
  • 相关阅读:
    模块
    Queue(队列)
    Stack(栈)
    Vector(容器)
    位图像素的颜色
    大数处理之三(除法)
    大数处理之二(幂运算)
    浮点数(double)的优势
    大数处理之一(加法和乘法)
    Depth-First Search
  • 原文地址:https://www.cnblogs.com/chengjian-physique/p/14995382.html
Copyright © 2020-2023  润新知