Loading

Python并行计算专题

最近工作中经常涉及到Python并行计算的内容,所以今天做一期专题的知识整理。本文将涉及以下三块内容:1. 多核/多线程/多进程的概念区分,2. Python多线程,多进程的使用方式,3. Python进程池的管理方案

多核/多线程/多进程

一般而言,计算机可以并行执行的线程数量就是CPU的物理核数,因为CPU只能看到线程(线程是CPU调度分配的最小单位)。然而,由于超线程技术的存在,现在的CPU可以在每个内核上运行更多的线程,因此,现代计算机可以并行执行的线程数量往往是CPU物理核数的两倍或更多

进程是操作系统资源分配(内存,显卡,磁盘等)的最小单位,线程是执行调度(即CPU资源调度)的最小单位(CPU看到的都是线程而不是进程)。一个进程可以有一个或多个线程,线程之间共享进程的资源。如果计算机有多个内核,且计算机中的总的线程数量小于逻辑核数,那线程就可以并行运行在不同的内核中。如果是单核多线程,那多线程之间就不是并行的关系,它们只是通过分时的方式,轮流使用单个内核的计算资源,营造出一种“并发”执行的假象。由于线程切换需要耗费额外的资源,因此如果多线程协同处理的是同一个计算任务,那么该任务的完成速度是不如单线程从头算到尾的

针对计算密集型的任务,我们使用与逻辑内核数相同的线程数就可以最大化地利用计算资源(线程数少了会引起内核资源的闲置,线程数多了则会消耗不必要的资源在分时切换上)。如果是IO密集型任务,我们就需要创建更多的线程,因为当一个线程已经算好结果,在等待IO写入时,我们就可以让另一个线程去使用此时空闲的内核资源,在这个场景下线程间切换的代价是小于内核资源闲置的代价的

在上一段中,我们讨论的一直都是线程,那么什么时候我们应该使用更多的进程呢?回顾之前提到过的进程的特点:

进程是操作系统资源分配的最小单位

因此,是否使用多进程,这取决于你是否想要,并且是否能够发挥某一系统资源IO性能的最大值。举个例子:比如你想要尽可能快地往磁盘中写入数据,而此时一个进程并不能占满磁盘IO的带宽,那么我们就可以使用多进程,并发地往磁盘中写入内容,进而最大化地发挥磁盘的读写性能

一般而言,只要CPU 有多个逻辑内核,那么多个线程就能够在不同的内核上并发执行(即使此时只有一个进程)。但对 Python 来说,无论是单核还是多核,一个进程同时只能有一个线程在执行。为什么会出现这种情况呢?因为Python 在设计时采用了一种叫 GIL 的机制。GIL 的全称为 Global Interpreter Lock (全局解释器锁),它出于安全考虑,限制了每个Python进程下的线程,使其只有拿到GIL后,才能使用内核资源继续工作。因此,Python的多线程一般只用来实现分时的“并发”,要想充分发挥硬件性能,还是需要使用多个进程

Python多线程和多进程的使用方法

多线程

我们先来看Python多线程的一种最简单的使用方式:

import time
from threading import Thread

def foo(content, times):
    for i in range(times):
        timestamp = time.strftime('%H:%M:%S',time.localtime(time.time()))
        print(f"{content} {i+1}  {timestamp}")
        time.sleep(1)

thread_names = ('alpha', 'bata')
repeat_times = 3
for name in thread_names:
    th = Thread(target=foo, args=(name, repeat_times))
    th.start()
    time.sleep(0.1)

输出:

alpha 1  21:57:58
bata 1  21:57:58
alpha 2  21:57:59
bata 2  21:57:59
alpha 3  21:58:00
bata 3  21:58:00

Python通过标准库threading提供对线程的支持,其常用方法包括:

  • threading.currentThread()——返回当前的线程变量
  • threading.enumerate()——返回一个包含正在运行的线程的list

以及Thread对象的常用方法:

  • start():启动线程活动
  • join():阻塞当前的程序,直至等待时间结束或其Thread对象运行终止。该方法可用于计量多线程程序用时
  • isAlive():返回线程是否活动的
  • getName() 返回线程名,setName() 设置线程名

当然,Python中的Thread不仅仅可以通过函数的方式使用,还可以以面向对象的方式使用,参考文档说明:

The Thread class represents an activity that is run in a separate thread of control. There are two ways to specify the activity: by passing a callable object to the constructor, or by overriding the run() method in a subclass. No other methods (except for the constructor) should be overridden in a subclass. In other words, only override the __init__() and run() methods of this class.

当我们以面向对象的方式使用Thread时,我们只需要把需要并行的部分实现至类的run()方法中,然后在外部调用线程对象的start()方法即可:

import time
import threading

class MyThread(threading.Thread):
    
    def __init__(self, name, repeat_times):
        super(MyThread, self).__init__()
        self.content = name
        self.times = repeat_times

    def run(self):
        for i in range(self.times):
            timestamp = time.strftime('%H:%M:%S',time.localtime(time.time()))
            print(f"{self.content} {i+1}  {timestamp}")
            time.sleep(1)

thread_names = ('alpha', 'bata')
repeat_times = 3
for name in thread_names:
    th = MyThread(name, repeat_times)
    th.start()
    time.sleep(0.1)

该程序的输出与之前的多线程函数一致

多进程

Python多进程可以使用subprocess模块,参考:multiprocessing — Process-based parallelism - Python Docs

from multiprocessing import Process
import os

# 子进程要执行的代码
def run_proc(name):
    print('Run child process %s (%s)...' % (name, os.getpid()))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    p = Process(target=run_proc, args=('test',))
    print('Child process will start.')
    p.start()
    p.join()
    print('Child process end.')

进程池

Python进程池的使用可参考:Python使用进程池管理进程

from multiprocessing import Pool
import time
import os

def action(name='http://c.biancheng.net'):
    print(name,' --当前进程:',os.getpid())
    time.sleep(3)

if __name__ == '__main__':
    #创建包含 4 条进程的进程池
    pool = Pool(processes=4)
    # 将action分3次提交给进程池
    pool.apply_async(action)
    pool.apply_async(action, args=('http://c.biancheng.net/python/', ))
    pool.apply_async(action, args=('http://c.biancheng.net/java/', ))
    pool.apply_async(action, kwds={'name': 'http://c.biancheng.net/shell/'})
    pool.close()
    pool.join()


参考:

  1. 多线程,多进程,多核总结 - 知乎 (注意此文中部分描述存在事实性错误)
  2. What Is Hyper-Threading? - Intel
  3. python多线程基础
  4. python多线程之从Thread类继承
  5. Thread Objects - Python Docs
  6. Python多线程和多进程编程(并发编程)
  7. Python使用进程池管理进程
posted @ 2021-10-01 20:00  云野Winfield  阅读(343)  评论(0编辑  收藏  举报