利用Python编写并发性程序——多线程、多进程、协程总结

前言

Python是一门高效的语言,使用Python可以轻松的开发出可扩展高性能应用。

什么叫做可扩展呢?

  • 横向扩展:一个系统通过增加更多的计算机节点来扩展,比如创建一个负载均衡的服务器集群,叫做横向扩展。
  • 纵向扩展:增加一个系统在单个计算机节点中的资源,包括存储量,CPU占用率等,叫做纵向扩展。

如何度量一个系统的扩展性和性能呢?

  • 比如说系统在扩展前每分钟处理120个文件(吞吐量),生成一个文件所需要的的时间(延迟)在2s。

    通过纵向扩展,增加服务器上的RAM,使系统吞吐量提高到每分钟生成180个文件,延迟保持在2s不变。

    此时系统在吞吐量方面的扩展性如下:

    扩展性(吞吐量)= 180/120 = 1.5X

  • 如果不改变内存大小,使程序变为多线程,延迟从2s下降到1s,此时的性能:

    性能(延迟): X = 2/1 = 2X

一个具有高并发和低延迟的系统是性能较高的,并且能够更好地进行水平扩展和垂直扩展。

相反低并发和高延迟的程序性能是低的,扩展性也是差的,而要想提高可扩展性必须先解决延迟和并发性。

并发性

可以使用不同的技术实现并发性:

  1. 多线程:线程是可以由CPU执行的最简单的编程指令序列。一个程序可以由任意数量的线程组成。通过将任务分配给多个线程,一个应用程序可以同时执行更多的工作。所有的线程都在同一个进程中运行。
  2. 多进程:在消息传递和共享内存方面,多进程将设计更多的开销。然而,相比于多线程,多进程操作可以使执行大量CPU密集型计算的程序获益更多。
  3. 异步任务:在这种技术中,操作是异步执行的,没有特定的任务顺序。异步处理通常从任务队列中挑选任务,并安排它们在将来的时间执行,通常在回调函数或特殊的future对象中接受结果。异步任务通常发生在单个线程中。

并发和并行

  • 并发和并行都是指同时执行工作,而不是顺序执行工作。

  • 但是并发并不要求同步执行多个任务,只要同时开始执行就可以了。

  • 并行要求同时开始、同步执行多个任务。

  • 一个单核的CPU同一时间最多执行一个线程,所以在单核的CPU上多线程是通过CPU调度实现的,CPU调度器实现了线程进出的快速切换,这样它们看上去就好像是在并行。

    而在多核CPU中,多线程可以实现真正的并行。

  • 但是并行在资源占用方面是巨大的,应该使用并发处理技术来更好的利用资源,而不是一味的增加并行处理。

Python中的并发性——多线程机制

使用第三方库Threading可以实现多线程,同时该库也公开了以下的同步单元

  1. 锁(lock)对象对于同步受保护的共享资源的访问很有用,以及与其类似的RLock对象。
  2. 条件(condition)对象,它对线程在等待任意条件时进行同步很有用。
  3. 事件(event)对象,在线程之间提供了基本的信号机制
  4. 信号量(semaphore)对象,允许一组固定的线程相互等待,同步到一个特定的状态,接着继续往下执行。

生产者/消费者架构

使用Python多线程借助队列实现生产者、消费者架构。

该架构使用场景

  • 多线程的系统中,其中包含一组生产数据的线程和另一组消费或处理数据的线程,该模型是理想的选择。

使用该架构的系统特点

  1. 生产者是用来专门生产数据的工作者(线程)类,可以从一个特定的源接受数据或者自己生成数据。
  2. 生产者将数据添加到共享的同步队列中。在Python中,这个队列由适当命名的队列模块里的队列类提供。
  3. 消费者类在队列上等待(消费)数据。一旦获得了数据,就会处理并产生结果。
  4. 当生产者停止生成数据并且消费者缺乏数据时,程序就结束了。

生产者消费者案例

生产者、消费者模式1.0版本:

  • 生产者类不断生成样式添加至队列中,消费者类不断取出样式改造样式。
  • 主线程——设置一组生产者和消费者并运行它们:
import threading
import time
import random
import string
from queue import Queue


class Style_Generator(threading.Thread):
    """生产者类: 不断生成样式放入队列"""

    def __init__(self, queue, sleep_time=1):
        self.sleep_time = sleep_time
        self.queue = queue
        # 停止生产的标志
        self.flag = True
        self._sizes = ('240', '320', '360', '480')
        self._color = ('red', 'yellow', 'blue', 'green')
        threading.Thread.__init__(self, name='producer')

    def __str__(self):
        return 'Producer'

    def get_size(self):
        return ''.join(random.choice(self._sizes))

    def get_color(self):
        return ''.join(random.choice(self._color))

    def run(self):
        """主线程方法"""
        while self.flag:
           style = f'颜色:{self.get_color()}, 大小:{self.get_size()}' 
           # 添加队列
           print(self, 'Put', style)
           self.queue.put(style)
           time.sleep(self.sleep_time)

    def stop(self):
        """停止生产者类"""
        self.flag = False

class Style_Consumer(threading.Thread):
    """消费者类:不断从队列中取出样式加工样式"""

    def __init__(self, queue):
        self.queue = queue
        self.flag = True
        self._images = ('山水图', '草木图', '花鸟图')
        threading.Thread.__init__(self, name='consumer')

    def __str__(self):
        return 'Consumer'

    def get_image(self):
        image = random.choice(self._images)
        return f'背景图片:{image}'
    
    def deal_style(self, style):
        """处理样式, 添加背景图"""
        new_style = f'{style},{self.get_image()}'
        print(self, 'deal_style:', new_style)

    def run(self):
        """消费者主线程"""
        while self.flag:
            style = self.queue.get()
            print(self, 'Got', style)
            self.deal_style(style)

    def stop(self):
        """停止消费"""
        self.flag = False

    
def main():
    """主线程:设置一组生产者和消费者并运行它们"""
    q = Queue(maxsize=200)
    producer, consumer = [], []

    for _ in range(2):
        t = Style_Generator(q)
        producer.append(t)
        t.start()

    for _ in range(2):
        t = Style_Consumer(q)
        consumer.append(t)
        t.start()

if __name__ == "__main__":
    main()

使用锁的资源约束

生产者、消费者模式2.0版本:

  • 上边的生产者、消费者架构代码有一个问题:会无休止地运行,消耗系统的资源。

  • 接下来使用一个锁来修改程序,一个实现计数器的同步单元将限制加工的样式数量,并且通过这种方式来结束程序。

  • 为了每次运行时加工固定数量的样式,应当支持添加计数器变量。但是,由于多个线程会检查并增加这个计数器,所以它需要通过一个锁对象来同步。

    class StyleDealer:
        """ 带有计数器处理固定数量样式的资源计数器类"""
    
        def __init__(self, limit=10):
            self.lock = threading.Lock()
            self.counter = {}
    
        def get_image(self):
            image = random.choice(self._images)
            return f'背景图片:{image}'
    
        def deal_style(self, style):
            """处理样式, 添加背景图"""
            new_style = f'{style},{self.get_image}'
            print(self, 'deal_style:', new_style)
            self.counter[style] = 1
            return True
    
        def deal(self, style):
            """计数器超出指定数量返回False"""
            with self.lock:
                if len(self.counter) >= self.limit:
                    return False
                self.deal_style(style)
                print('Count:', len(self.counter))
                return True
    

    同时对应的消费者类也需要变更:

    class Style_Consumer(threading.Thread):
        """消费者类:不断从队列中取出样式加工样式"""
    
        def __init__(self, queue, dealer):
            self.queue = queue
            self.flag = True
            self.dealer = dealer
            self._id = uuid.uuid4()/hex
            threading.Thread.__init__(self, name='Consumer-'+ self._id)
    
        def __str__(self):
            return 'Consumer' + self._id
    
        def run(self):
            """消费者主线程"""
            while self.flag:
                style = self.queue.get()
                print(self, 'Got', style)
                if not self.dealer.deal(style)
                    # 达到指定数量,退出循环
                    print(self, 'Set limit reached, quitting')
                    break
    
        def stop(self):
            """停止消费"""
            self.flag = False
    

    主线程代码也需要变更:

    def main():
        """ 
        主线程, 构造生产者类和消费者类,等待消费者达到数量停止,
        消费者停止后,停止生产者。
        """
        q = Queue(maxsize=2000)
        dealer = StyleDealer(limit=10)
        producers, consumers = [], []
        for i in range(3):
            t = Style_Generator(q)
            producers.append(t)
            t.start()
    
        for i in range(5):
            t = Style_Consumer(q, dealer)
            consumers.append(t)
            t.start()
    
        for t in consumers:
            t.join()
            print('Joined', t, flush=True)
    
        # 保证生产者不会阻塞队列
        while not q.empty():
            item = q.get()
    
        for t in producers:
            t.stop()
            print('Stopped', t, flush=True)
    

    总的代码如下:

    import threading
    import time
    import random
    import string
    import uuid
    from queue import Queue
    
    
    class StyleDealer:
        """ 带有计数器处理固定数量样式的资源计数器类"""
    
        def __init__(self, limit=10):
            self.lock = threading.Lock()
            self.counter = {}
            self.limit = limit
            self._images = ('山水图', '草木图', '花鸟图')
    
        def __str__(self):
            return 'StyleDealer'
    
        def get_image(self):
            image = random.choice(self._images)
            return f'背景图片:{image}'
    
        def deal_style(self, style):
            """处理样式, 添加背景图"""
            new_style = f'{style},{self.get_image()}'
            print(self, 'deal_style:', new_style)
            self.counter[style] = 1
            return True
    
        def deal(self, style):
            """计数器超出指定数量返回False"""
            with self.lock:
                if len(self.counter) >= self.limit:
                    return False
                self.deal_style(style)
                print('Count:', len(self.counter))
                return True
    
    
    class Style_Generator(threading.Thread):
        """生产者类: 不断生成样式放入队列"""
    
        def __init__(self, queue, sleep_time=1):
            self.sleep_time = sleep_time
            self.queue = queue
            # 停止生产的标志
            self.flag = True
            self._sizes = ('240', '320', '360', '480')
            self._color = ('red', 'yellow', 'blue', 'green')
            threading.Thread.__init__(self, name='producer')
    
        def __str__(self):
            return 'Producer'
    
        def get_size(self):
            return ''.join(random.choice(self._sizes))
    
        def get_color(self):
            return ''.join(random.choice(self._color))
    
        def run(self):
            """主线程方法"""
            while self.flag:
               style = f'颜色:{self.get_color()}, 大小:{self.get_size()}' 
               # 添加队列
               print(self, 'Put', style)
               self.queue.put(style)
               time.sleep(self.sleep_time)
    
        def stop(self):
            """停止生产者类"""
            self.flag = False
    
    
    class Style_Consumer(threading.Thread):
        """消费者类:不断从队列中取出样式加工样式"""
    
        def __init__(self, queue, dealer):
            self.queue = queue
            self.flag = True
            self.dealer = dealer
            self._id = uuid.uuid4().hex
            threading.Thread.__init__(self, name='Consumer-'+ self._id)
    
        def __str__(self):
            return 'Consumer' + self._id
    
        def run(self):
            """消费者主线程"""
            while self.flag:
                style = self.queue.get()
                print(self, 'Got', style)
                if not self.dealer.deal(style):
                    # 达到指定数量,退出循环
                    print(self, 'Set limit reached, quitting')
                    break
    
        def stop(self):
            """停止消费"""
            self.flag = False
    
    
    def main():
        """ 
        主线程, 构造生产者类和消费者类,等待消费者达到数量停止,
        消费者停止后,停止生产者。
        """
        q = Queue(maxsize=2000)
        dealer = StyleDealer(limit=10)
        producers, consumers = [], []
        for i in range(3):
            t = Style_Generator(q)
            producers.append(t)
            t.start()
    
        for i in range(5):
            t = Style_Consumer(q, dealer)
            consumers.append(t)
            t.start()
    
        for t in consumers:
            t.join()
            print('Joined', t, flush=True)
    
        # 保证生产者不会阻塞队列
        while not q.empty():
            item = q.get()
    
        for t in producers:
            t.stop()
            print('Stopped', t, flush=True)
    
    
    
    
    if __name__ == "__main__":
        main()
    

使用信号量的资源约束

信号量也可以实现多线程中对资源的约束。

信号量用大于0的值初始化:

  1. 当一个线程调用获得一个具有正内部值得信号量时,该值会减1,并且线程会继续前进。
  2. 当另一个线程调用释放这个信号量时,值会增加1.
  3. 当值到达0时,任何线程调用获得线程都被阻塞,直到它被另一个调用释放的线程唤醒。

生产者、消费者模式3.0版本:

import threading
import time
import random
import string
import uuid
from queue import Queue


class StyleDealer:
    """ 带有计数器处理固定数量样式的资源计数器类"""

    def __init__(self, limit=10):
        self.limit = limit
        self._images = ('山水图', '草木图', '花鸟图')
        self.counter = threading.BoundedSemaphore(value=limit)
        self.count = 0

    def __str__(self):
        return 'StyleDealer'

    def acquire(self):
        return self.counter.acquire(blocking=False)

    def release(self):
        return self.counter.release()
    

    def get_image(self):
        image = random.choice(self._images)
        return f'背景图片:{image}'

    def deal_style(self, style):
        """处理样式, 添加背景图"""
        new_style = f'{style},{self.get_image()}'
        print(self, 'deal_style:', new_style)
        self.count += 1
        return True

    def deal(self, style):
        """处理样式"""
        if self.acquire():
            self.deal_style(style)
        else:
            print('Semaphore limit reached, returning False')
            return False


class Style_Generator(threading.Thread):
    """生产者类: 不断生成样式放入队列"""

    def __init__(self, queue, sleep_time=1):
        self.sleep_time = sleep_time
        self.queue = queue
        # 停止生产的标志
        self.flag = True
        self._sizes = ('240', '320', '360', '480')
        self._color = ('red', 'yellow', 'blue', 'green')
        threading.Thread.__init__(self, name='producer')

    def __str__(self):
        return 'Producer'

    def get_size(self):
        return ''.join(random.choice(self._sizes))

    def get_color(self):
        return ''.join(random.choice(self._color))

    def run(self):
        """主线程方法"""
        while self.flag:
           style = f'颜色:{self.get_color()}, 大小:{self.get_size()}' 
           # 添加队列
           print(self, 'Put', style)
           self.queue.put(style)
           time.sleep(self.sleep_time)

    def stop(self):
        """停止生产者类"""
        self.flag = False


class Style_Consumer(threading.Thread):
    """消费者类:不断从队列中取出样式加工样式"""

    def __init__(self, queue, dealer):
        self.queue = queue
        self.flag = True
        self.dealer = dealer
        self._id = uuid.uuid4().hex
        threading.Thread.__init__(self, name='Consumer-'+ self._id)

    def __str__(self):
        return 'Consumer' + self._id

    def run(self):
        """消费者主线程"""
        while self.flag:
            style = self.queue.get()
            print(self, 'Got', style)
            if not self.dealer.deal(style):
                # 达到指定数量,退出循环
                print(self, 'Set limit reached, quitting')
                break

    def stop(self):
        """停止消费"""
        self.flag = False


def main():
    """ 
    主线程, 构造生产者类和消费者类,等待消费者达到数量停止,
    消费者停止后,停止生产者。
    """
    q = Queue(maxsize=2000)
    dealer = StyleDealer(limit=10)
    producers, consumers = [], []
    for i in range(3):
        t = Style_Generator(q)
        producers.append(t)
        t.start()

    for i in range(5):
        t = Style_Consumer(q, dealer)
        consumers.append(t)
        t.start()

    for t in consumers:
        t.join()
        print('Joined', t, flush=True)

    # 保证生产者不会阻塞队列
    while not q.empty():
        item = q.get()

    for t in producers:
        t.stop()
        print('Stopped', t, flush=True)




if __name__ == "__main__":
    main()

信号量和锁比较

  1. 使用锁的版本保护了修改资源的所有代码,例如检查计算器,修改样式,并将计数器加1,从而确保数据的一致性。
  2. 信号量版本的实现就像一个房间,房间限定最多进入多少人,出事房间人数为0,进入一个人,数量加一,出去一个人房间数量减一,当达到上限后房间就关闭了。
  3. 信号量版本比相同逻辑的锁版本快几倍。

使用条件的速率控制器

上边的几个版本的生产者、消费者模式架构在实际场景中使用都会遇到几个重要的问题:

问题

  1. 生产者生产速度比消费者消费速度快,导致队列堆积,消耗系统资源。
  2. 消费者消费速度比生产者生产者速度快,只要生产者没有太大的滞后,这种状况不算问题,但是当速度差距过大,就会导致系统分割成两半,即消费者保持闲置,而生产者则试图跟上需求。

解决方法

  • 固定队列的大小:只要达到队列的大小限制,生产者将被迫等待数据被消费者消费。这将总是保持队列的完整性。
  • 为工作者类提供超时设定和其他职责:使用一个超时设定来等待队列,当超时时,可以让工作者类sleep或者作别的事,然后再回来在队列上排队等候。
  • 动态配置工作者类的数量:按照需求自动增加或减少工作者类池的大小。如果生产者类过多了,系统将启动相同数量的消费者类来保持平衡。反之亦然。
  • 调整数据生产速率:静态的或动态的调整生产者的数据生成速率。

接下来使用最后一种解决方法重写生产者、消费者架构:

  • 先看下threading模块条件对象同步单元的用法:

    一个条件对象是一个复杂的同步单元,它带有一个隐含的内置锁。它能够等待任意一个条件,直到这一条件变为True为止。当线程调用条件上的wait方法时,内置锁被打开,但是线程本身被阻塞:

    cond = threading.Condition()
    # In thread #1
    with cond:
    	while not some_condition_is_satisfied():
    	# this thread is now blocked
    	cond.wait()
    

    现在,另一个线程可以通过将条件设置为True来唤醒前面的线程,然后在条件对象上调用notify或notify_all方法。此时,前面的阻塞线程被唤醒,并继续它的运行:

    # In thread #2
    with cond:
    	# Condition is satisfied
    	if some_condition_is_satisfied():
    	# Notify all threads waiting on the condition
    	cond.notify_all()
    
  • 需要实现一个控制类,使用一个条件对象实现样式生产速率控制:

    class CreateStyleController(threading.Thread):
        """
        使用条件对象限制生产者生产速率的控制类
        """
    
        def __init__(self, rate_limit=0, nthreads=0):
            # 获取限制速率
            self.rate_limit = rate_limit
            # 生产者线程数量
            self.nthreads = nthreads
            # 样式数量
            self.count = 0
            self.start_t = time.time()
            self.flag = True
            self.cond = threading.Condition()
            threading.Thread.__init__(self)
    
        def incrument(self):
            # 样式的增长数量
            self.count += 1
    
        def calc_rate(self):
            """
            计算当前生产速率
            """
            # 速度太快导致除零,这里sleep 0.1 秒
            time.sleep(0.1)
            rate = 60.0*self.count/(time.time() - self.start_t)
            return rate
    
        def run(self):
            """
            如果当前增长速率小于速率限制,唤醒所有生产线程
            """
            while self.flag:
                rate = self.calc_rate()
                print(f'当前速率:{rate},速率限制{self.rate_limit}')
                if rate <= self.rate_limit:
                    with self.cond:
                        self.cond.notify_all()
        
        def stop(self):
            self.flag = False
        
        def throttle(self, thread):
            """
            动态调整生产者线程的速率:
            1)计算出当前速率与限制速率的速率差的绝对值。
            2)计算出需要调整的sleep时间差。
            3)如果当前速率大于限制速率,给当前线程的sleep时间加上sleep时间差。
            4)如果当前速率小于限制速率,给当前线程的sleep时间减去sleep时间差
                如果减去时间差的值小于0,就将当前线程的等待时间设置为0.
            """
            rate = self.calc_rate()
            print('Current Rate', rate)
    
            diff = abs(rate - self.rate_limit)
    
            sleep_diff = diff/(self.nthreads*60.0)
    
            if rate > self.rate_limit:
                thread.sleep_time += sleep_diff
                with self.cond:
                    print('Controller, rate is high, sleep more by',rate, sleep_diff)
            elif rate < self.rate_limit:
                print('Controller, rate is low, sleep by', rate, sleep_diff)
                sleep_time = thread.sleep_time
                sleep_time -= sleep_diff
                thread.sleep_time = max(0, sleep_time)
    
  • 更改后的生产者类代码:

    class Style_Generator(threading.Thread):
        """
        生成样式并支持通过外部控制器进行节流的生产者worker类
        """
    
        def __init__(self, queue, controller=None, sleep_time=1):
            self.sleep_time = sleep_time
            self.queue = queue
            # 运行线程的标志
            self.flag = True
            self._sizes = ('240', '320', '360', '480')
            self._color = ('red', 'yellow', 'blue', 'green')
            # 速率控制器
            self.controller = controller
            # 内部id
            self._id = uuid.uuid4().hex
            threading.Thread.__init__(self, name='Producer-' + self._id)
    
        def __str__(self):
            return 'Producer-' + self._id
        
        def get_size(self):
            return ''.join(random.choice(self._sizes))
    
        def get_color(self):
            return ''.join(random.choice(self._color))
    
        def run(self):
            """
            带有控制器的主线程方法:
            通过控制器来计算生产的样式的数量,当数量增长至5个的时候,
            开始通过控制器对象调整生产速率。
            """
            while self.flag:
                style = f'颜色:{self.get_color()}, 大小:{self.get_size()}'
                time.sleep(2)
                print(self, 'Put', style)
                self.queue.put(style)
                self.controller.incrument()
                if self.controller.count > 5:
                    self.controller.throttle(self)
                time.sleep(self.sleep_time)
        
        def stop(self):
            self.flag = False
    
  • 主线程的调用代码也需要更改:

    def main():
        q = Queue(maxsize=2000)
      # 需要使用确切的生产者数量配置控制器
        controller = CreateStyleController(rate_limit=300, nthreads=3)
        dealer = StyleDealer(limit=200)
    
        controller.start()
    
        producers, consumers = [], []
        for i in range(3):
            t = Style_Generator(q, controller)
            producers.append(t)
            t.start()
    
        for i in range(5):
            t = Style_Consumer(q, dealer)
            consumers.append(t)
            t.start()
    
        for i in consumers:
            t.join()
            print('Joind', t, flush=True)
    
        while not q.empty():
            item = q.get()
            controller.stop()
    
        for t in producers:
            t.stop()
            print('Stopped', t, flush=True)
    
    	controller.stop()
        print('#############end############')
    
    
    if __name__ == "__main__":
        main()
    

多进程机制

使用Process类实现多进程,与多线程相似,也提供了许多同步单元,这些单元几乎与线程模块中的完全相同。

多进程可以充分利用CPU,但同时又会占用大量的内存,在实现并行时,到底是选择多线程还是多进程要看具体的情况。

多进程案例:排序合并磁盘文件

现有100万个文件,每个文件包含范围为1~10000的100个整数,总共一亿个整数。现在使用多进程和计数器来读取这100万个文件写入到一个文件中,并且写入文件是排好序的。

import sys
import time
import collections
from multiprocessing import Pool

# 每个进程最多处理10万个数据
MAXINT = 100000

def sorter(filenames):
    """
    使用计数器排序文件内容的排序程序
    """
    counter = collections.defaultdict(int)

    for filename in filenames:
        for i
        in open(filename):
            counter[i] += 1
    
    return counter

def batch_files(pool_size, limit):
    """
    使用进程池生成批量处理文件
    """
    batch_size = limit // pool_size

    filenames = []

    for i in range(pool_size):
        batch = []
        for j in range(i*batch_size, (i+1)*batch_size):
            filename = 'numbers/number_%d.txt' % j
            batch.append(filename)
        filenames.append(batch)
    return filenames

def sort_files(pool_size, filenames):
    """
    使用进程池排序批量处理文件
    """
    with Pool(pool_size) as pool:
        counters = pool.map(sorter, filenames)
    with open('sorted_nums.txt', 'w') as fp:
        for i in range(1, MAXINT+1):
            count = sum([x.get(str(i) + '\n', 0) for x in counters])
        if count > 0:
            fp.write((str(i) + '\n') * count)
    print('Sorted')

if __name__ == "__main__":
    limit = int(sys.argv[1])
    pool_size = 4
    filenames = batch_files(pool_size, limit)
    sort_files(pool_size, filenames)

适合多线程的情况:

  • 程序需要维护许多共享状态,尤其是可变的状态。Python中的许多标准数据结构,例如列表、字典和其他都是线程安全的,所以使用线程而不是进程维护一个易变得共享状态代价相对较小。
  • 程序需要保存低的内存占用。
  • 程序花费大量的时间执行I/O。由于GIL是由线程释放的,所以它不会影响线程执行I/O的时间。
  • 程序没有太多需要通过多进程处理来扩展进行并行操作的数据。

适合多进程的情况:

  • 程序执行许多CPU密集的计算,例如字节码操作、数据处理和类似的大输入量的处理。
  • 程序的输入可以并行地分块,并且它的结果之后可以合并在一起。换句话说,程序的输入通过数据并行计算可以生成很好的结果。
  • 程序在内存使用方面没有任何限制,并且程序运行在一台具有多核CPU,内存足够的RAM的现代计算机上时。
  • 在需要同步的进程之间没有太多共享的可变状态时,因为这可能会减慢系统的速度并抵消多个进程所获得的性能提升。
  • 应用程序并不强依赖与I/O——文件或磁盘或套接字的I/O。

Python中的异步执行

Python第三方库asyncio模块实现了多任务的协程处理,关于协程不是很了解的可以看我以前写的这篇文章Python中的协程

先使用yield关键字自己实现一个简单的多任务的协程:

import random
import time
import collections
import threading
import sys

def number_generator(n):
    """
    生成范围为1..n的数字的协程任务
    """
    for i in range(1, n+1):
        yield i

def square_mapper(numbers):
    """
    一种将数字转换成平方的协程任务
    """
    for n in numbers:
        yield n*n
    
def prime_filter(numbers):
    """
    返回质数的协程任务
    """
    primes = []
    for n in numbers:
        if n % 2 == 0: continue
        flag = True
        for i in range(3, int(n**0.5+1), 2):
            if n % i == 0:
                flag = False
                break
        if flag:
            yield n

def scheduler(tasks, runs=10000):
    """
    多协程的任务调度器
    """
    results = collections.defaultdict(list)

    for i in range(runs):
        for t in tasks:
            print('Switching to task', t.__name__)
            try:
                result = t.__next__()
                print('Result=>',result)
                results[t.__name__].append(result)
            except StopIteration:
                break
    return results

if __name__ == "__main__":
    tasks = []
    # 计算cpu时间或者real时间
    start = time.perf_counter()
    limit = int(sys.argv[1])
    # 将求平方的协程任务加入任务列表
    tasks.append(square_mapper(number_generator(limit)))
    # 将求质数的协程任务加入任务列表
    tasks.append(prime_filter(number_generator(limit)))
    # 调用任务调度器,并接收返回结果
    results = scheduler(tasks, runs=limit)
    # 打印最后一个质数
    print('Last prime=>', results['prime_filter'][-1])
    end = time.perf_counter()
    print('Time taken=>', end-start)

运行结果:

PS C:\Users\ZZY\Desktop> python .\a.py 20
Switching to task square_mapper
Result=> 1
Switching to task prime_filter 
Result=> 1
Switching to task square_mapper
Result=> 4
Switching to task prime_filter 
Result=> 3
Switching to task square_mapper
Result=> 9
Switching to task prime_filter 
Result=> 5
Switching to task square_mapper
Result=> 16
Switching to task prime_filter 
Result=> 7
Switching to task square_mapper
Result=> 25
Switching to task prime_filter 
Result=> 11
Switching to task square_mapper
Result=> 36
Switching to task prime_filter 
Result=> 13
Switching to task square_mapper
Result=> 49
Switching to task prime_filter 
Result=> 17
Switching to task square_mapper
Result=> 64
Switching to task prime_filter
Result=> 19
Switching to task square_mapper
Result=> 81
Switching to task prime_filter
Switching to task square_mapper
Result=> 100
Switching to task prime_filter
Switching to task square_mapper
Result=> 121
Switching to task prime_filter
Switching to task square_mapper
Result=> 144
Switching to task prime_filter
Switching to task square_mapper
Result=> 169
Switching to task prime_filter
Switching to task square_mapper
Result=> 196
Switching to task prime_filter
Switching to task square_mapper
Result=> 225
Switching to task prime_filter
Switching to task square_mapper
Result=> 256
Switching to task prime_filter
Switching to task square_mapper
Result=> 289
Switching to task prime_filter
Switching to task square_mapper
Result=> 324
Switching to task prime_filter
Switching to task square_mapper
Result=> 361
Switching to task prime_filter
Switching to task square_mapper
Result=> 400
Switching to task prime_filter
Last prime=> 19
Time taken=> 0.005162099999999999

Python中的asyncio模块

该模块有两种用法:

  • 使用async def 语句来定义函数

    该方法创建的协程通常使用await<future>表达式来等待将来的完成。

    使用一个event循环来调度执行协程,它将对象连接起来,并将它们作为任务进行调度。不同的操作系统提供不同的event循环。

  • 使用@asyncio.coroutine表达式进行装饰

    该方法时基于生成器的协程。

使用第一种方法实现代码,使用asyncio 关键字定义函数,使用await表达式达成协作:

import asyncio

def number_generator(m, n):
    """
    一个数字生成器的协程
    """
    yield from range(m, n+1)

async def prime_filter(m, n):
    """
    生成质数的协程
    """
    primes = []
    for i in number_generator(m, n):
        if i % 2 == 0: continue
        flag = True
        for j in range(3, int(i**0.5+1), 2):
            if i % j == 0:
                flag = False
                break
        if flag:
            print('Prime=>', i)
            primes.append(i)
        await asyncio.sleep(1.0)
    return tuple(primes)

async def square_mapper(m, n):
    """
    生成平方的协程
    """
    squares = []
    for i in number_generator(m, n):
        print('Square=>', i*i)
        squares.append(i*i)
        await asyncio.sleep(1.0)
    return squares

def print_result(future):
    print('Result=>', future.result())
    
if __name__ == "__main__":
	# 获得一个异步事件循环对象
    loop = asyncio.get_event_loop()
    # 建立future对象,聚合结果
    future = asyncio.gather(prime_filter(10, 20), square_mapper(10, 20))
    # future对象执行完毕后,打印聚合结果
    future.add_done_callback(print_result)
    # 定义循环机制
    loop.run_until_complete(future)
    # 结束循环
    loop.close()

运行结果可以看到是交叉运行的,这是因为我们使用了await来达成协作:

Prime=> 11
Square=> 100
Prime=> 13
Square=> 121
Square=> 144
Prime=> 17
Square=> 169
Prime=> 19
Square=> 196
Square=> 225
Square=> 256
Square=> 289
Square=> 324
Square=> 361
Square=> 400
Result=> [(11, 13, 17, 19), [100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400]]

两种方法结合使用,异步访问url并解析响应结果:

import asyncio
import aiohttp
import async_timeout

@asyncio.coroutine
def fetch_page(session, url, timeout=5):
    """
    异步访问url
    """
    with async_timeout.timeout(timeout):
        response = session.get(url)
        return response

async def parse_response(futures):
    """
    解析响应数据
    """
    for future in futures:
        response = future.result()
        # 等待响应数据
        data = await response.text()
        print('Response for URL', response.url, '=>', response.status,
                len(data))
        response.close()



if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    urls = (
        'http://www.baidu.com',
        # 'http://www.google.com',
        'http://www.douban.com'
    )
    session = aiohttp.ClientSession(loop=loop)
    # 访问url任务
    tasks = map(lambda x: fetch_page(session, x), urls)
    # 等待任务执行完毕
    done, pending = loop.run_until_complete(asyncio.wait(
                                            tasks, timeout=10))
    loop.run_until_complete(parse_response(done))
    session.close()
    loop.close()

concurrent.future——高级并发处理

concurrent.future模块使用线程或进程提供高级别的并发处理,同时使用future对象异步返回数据。

它提供一个执行器接口,其提供两种方法:

  • submit: 提交一个可以异步执行的调用,返回一个表示可回调执行的future对象。
  • map: 将调用映射到一组迭代器,在future对象中异步地调度执行。但是该方法直接返回处理结果,而不是返回future列表。

执行器接口有两个具体的实现:

  • ThreadPoolExecutor: 在线程池中执行可调用的操作。
  • ProcessPoolExecutor: 在进程池中执行可调用的操作。

案例:异步计算一组整数的阶乘:

from concurrent.futures import ThreadPoolExecutor, as_completed
import functools
import operator

def factorial(n):
    return functools.reduce(operator.mul, [i for i in range(1, n+1)])

# 带有两个工作者的执行器
with ThreadPoolExecutor(max_workers=2) as executor:
    # future 作为键,数字作为值
    future_map = {executor.submit(factorial, n): n for n in range(10, 21)}

# 参数为已完成的future对象
for future in as_completed(future_map):
    num = future_map[future]
    print('Factorial of', num, 'is', future.result())

运行结果:

PS C:\Users\ZZY> & python c:/Users/ZZY/Desktop/a.py
Factorial of 16 is 20922789888000
Factorial of 20 is 2432902008176640000
Factorial of 13 is 6227020800
Factorial of 10 is 3628800
Factorial of 18 is 6402373705728000
Factorial of 15 is 1307674368000
Factorial of 12 is 479001600
Factorial of 11 is 39916800
Factorial of 17 is 355687428096000
Factorial of 19 is 121645100408832000
Factorial of 14 is 87178291200

案例:磁盘缩略图产生器:

import os
import sys
import mimetypes
from PIL import Image
from concurrent.futures import (
    ThreadPoolExecutor, ProcessPoolExecutor,
    as_completed
)

def thumbnail_image(filename, size=(64, 64), format='.png'):
    """
    处理图片
    """
    try:
        im = Image.open(filename)
        im.thumbnail(size, Image.ANTIALIAS)

        basename = os.path.basename(filename)
        print(basename)
        thumb_filename = basename.rsplit('.')[0] + '_thumb.png'
        im.save(thumb_filename)
        print('Saved', thumb_filename)
        return True
    except Exception as e:
        print(e)
        print('Error converting file', filename)
        return False

def directory_walker(start_dir):
    """
    遍历指定文件夹下的图片,返回图片名生产器
    """
    for root, dirs, files in os.walk(os.path.expanduser(start_dir)):
        for f in files:
            filename = os.path.join(root, f)
            # 判断是否为图像,返回图像文件名生产器
            file_type = mimetypes.guess_type(filename.lower())[0]
            if file_type != None and file_type.startswith('image/'):
                yield filename


def main():
    # root_dir = os.path.expanduser('C:\Users\ZZY\Pictures\Saved Pictures')
    root_dir = r'C:\Users\ZZY\Pictures\Saved Pictures'
    if '--process' in sys.argv:
        executor = ProcessPoolExecutor(max_workers=10)
    else:
        executor = ThreadPoolExecutor(max_workers=10)

    
    with executor:
        future_map = {executor.submit(thumbnail_image, filename):
                        filename for filename in directory_walker(root_dir)}
        for future in as_completed(future_map):
            num = future_map[future]
            status = future.result()
            if status:
                print('Thumbnail of', future_map[future], 'saved')

if __name__ == "__main__":
    main()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一切如来心秘密

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值