了解GIL机制

再了解多线程和多进程之前,我们有必要先了解python的GIL机制。

GIL(global interpreter lock),也叫做python的全局解释器锁。GIL导致了同一个时刻,只能有有一个线程在CPU上执行python任务。也就是是说,在单核的CPU上,无论你开启多少个python线程,CPU上始终只有一个python解释器在运作,将你的请求转化为CPU可执行的命令。这也是为什么有人说python的多线程是伪多线程。

造成这个问题的不是python语言本身,而是python的底层Cpython,而且python很多包也都是基于Cpython写的,因此这个问题,目前还没有看到有效的解决办法。

但这也并不意味着python多线程毫无意义,如果程序中很多功能是调用IO口,比如从数据库里面取数据,开启多个线程同时取数据就很有意义,因为这些程序计算量很小,CPU占用量极低,性能是被数据库本身所局限的,而非CPU。

但是如果程序的功能主要就是计算,那么开启多线程的意义就不大了,因为计算往往需要大幅度的占用CPU的资源,即使开启再多的线程,都只有一个python解释器为你工作。这时候就需要用到多进程,将python程序映射到多个CPU上进行计算,那么性能就会成倍的增加了!

所以,什么时候使用多线程要视情况而定,计算密集型使用多进程,IO密集型使用多线程。

多线程编程

1. 使用Manager().Queue() 作为队列管理的多进程

from multiprocessing import Manager, Pool

class multi_task: # 注释3
    def task_function(self, q):
        while not q.empty():  # 只要共享的队列参数非空,那么继续从队列中提取参数并计算
            try:
                print(q.qsize())  # 观察队列内的参数数量
                task_signal= q.get()  # 从队列中提取参数
                Calculator().run(task_signal)
            except Exception as e:  # 这一步非常重要,如果没有,当进程遇到报错时会提前结束进程
                print('error:', e)
                pass
        else:
            pass

    def daily_total_update(self):
        q = Manager().Queue()  # 生成跨进程的队列管理器
        for i in task_signal_list:  # 将任务参数加入到q中,注意参数必须是str格式
            q.put(str(i))
        n = multiprocessing.cpu_count() - 1  # 进程数量最大为cpu的核数
        pool = Pool(processes=n)  # 创建进程池
        for i in range(n):
            pool.apply_async(self.task_function, args=(q,))  # 向进程池发放任务以及他们共享的参数队列
        pool.close()
        pool.join()  # 主进程等待所有的子进程完成任务
        return 'success'

【注释】

  1. 要使用多进程pool = Pool(processes=n),必须使用q = Manager().Queue()。这是因为Manager().Queue()是跨进程的队列管理器,pool中的多个进程都会从Manager().Queue()提取任务信息。
  2. 在使用tornado或者flask时,必须使用类封装多进程,否则多进程会开启多个tornado或者flask的服务
  3. 子进程一定要对报错的信息进行处理,否则子进程中的任务一旦报错就会结束进程,导致后续的任务没有进程去完成。