python-线程与进程

1.任务需求

在参加2022年3月华为软件精英挑战赛过程中,因为第一版算法比较暴力,耗时过久,当时希望借助多线程、多进程来加快算法的执行速度,在真实使用过程中有了很多基础但重要的理解和体会,总结整理如下,希望以后能够进一步合理使用多线程和多进程。本文的总结参考多篇文章,文中也会给出不少来自网络的代码,以便加深实操印象和理解。

2.基本概念

2.1 并发

指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)

2.2 并行

指的是任务数小于等于cpu核数,即任务真的是一起执行的。

2.3 线程

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

2.4 进程

进程是系统进行资源分配和调度的一个独立单位。

image-20220328110439591

3.基本实现

目前python3的内置模块中有threading 、multiprocessing可以实现多线程和多进程操作。

经过调研发现,如果涉及线程池和进程池,除这两种工作之外,还有一个模块可供使用(这个模块是在threading 、multiprocessing基础上进一步实现的)

【from concurrent.futures import ProcessPoolExecutor ,from concurrent.futures import ThreadPoolExecutor

3.1.多线程

多线程在python中有内置模块threading

(1)基本实现

# coding=utf-8
import threading
import time


def fun(Tid):
    print(Tid)
    time.sleep(3)


if __name__ == "__main__":
    start_time = time.time()
    for i in range(5):
        t = threading.Thread(target=fun, args=(i,))
        t.start()  # 启动线程,即让线程开始执行
    print(time.time() - start_time)

输出:

0
1
2
3
4
0.0

点评:基本的实现就是上文,但是会存在一个比较大的问题,大家看结果就可以发现最后打印的时间不是你真正想统计的时间,也就是说线程和print几乎同步执行了,那么如何满足你等待所有线程执行结束的需求呢?

(2)基本实现的一点完善

if __name__ == "__main__":
    start_time = time.time()
    t_pools = []
    for i in range(5):
        t = threading.Thread(target=fun, args=(i,))
        t_pools.append(t)
        t.start()  # 启动线程,即让线程开始执行
    for i in range(5):
        t_pools[i].join()
    print(time.time() - start_time)

输出

0
1
2
3
4
3.0104055404663086

点评:那就是需要在for循环让大家都启动后,再用一个for循环,利用t.join()指令通知他们等待线程结束。注意这里一定要先等for循环start所有线程后再通知他们等待,如果只用一个for循环,那么你就会发现多线程并发变成了单线程,因为他每一次都在等待上一次结束,这就变成了串行。

3.2 多进程

多进程在python中有内置模块multiprocessing

(1)基本实现

# -*- coding:utf-8 -*-
from multiprocessing import Process
import os
import time

def run_proc():
    """子进程要执行的代码"""
    print('子进程运行中,pid=%d...' % os.getpid())  # os.getpid获取当前进程的进程号
    print('子进程将要结束...')

if __name__ == '__main__':
    print('父进程pid: %d' % os.getpid())  # os.getpid获取当前进程的进程号
    p = Process(target=run_proc)
    p.start()

点评:基本实现方式和逻辑与线程类似,这里不做重复

3.3 线程池

在需要大量线程和进程的时候,不太方便像前文一个一个写线程或者进程,那么可以通过线程池或者进程池的方式,来进行线程和进程的管理和使用,个人认为使用线程池和进程池,代码的逻辑性和可读性也会好一些。

网上有大量的直接的线程池的写法,但是进程池和线程池写法一致,本文反其道而行之,使用进程池作为案例记录。

3.4 进程池

实现方法1

利用内置的multiprocessing模块

import os
import time
from multiprocessing import Pool

def work(n):
    print('%s run' %os.getpid())
    time.sleep(3)
    return n**2
if __name__ == '__main__':
    start_time = time.time()
    p=Pool(3) #进程池中从无到有创建三个进程,以后一直是这三个进程在执行任务
    res_l=[]
    for i in range(10):
        res=p.apply_async(work,args=(i,)) # 异步运行,根据进程池中有的进程数,每次最多3个子进程在异步执行
                                          # 返回结果之后,将结果放入列表,归还进程,之后再执行新的任务
                                          # 需要注意的是,进程池中的三个进程不会同时开启或者同时结束
                                          # 而是执行完一个就释放一个进程,这个进程就去接收新的任务。
        res_l.append(res)
    # 异步apply_async用法:如果使用异步提交的任务,主进程需要使用jion,等待进程池内任务都处理完,然后可以用get收集结果
    # 否则,主进程结束,进程池可能还没来得及执行,也就跟着一起结束了
    p.close()
    p.join()
    for res in res_l:
        print(res.get()) #使用get来获取apply_aync的结果,如果是apply,则没有get方法,因为apply是同步执行,立刻获取结果,也根本无需get
    print(time.time()-start_time)
实现方法2

利用内置的from concurrent.futures import ProcessPoolExecutor模块

from concurrent.futures import ProcessPoolExecutor
import time,os

def task(n):
    (n1,n2) = n
    print('%s is running'% os.getpid())
    time.sleep(3)
    print(n2)
    return n1
def handle(res):
    res=res.result()
    print("handle res %s"%res)

if __name__ == '__main__':
    start_time = time.time()
    #同步调用
    # pool=ProcessPoolExecutor(8)
    #
    # for i in range(13):
    #     pool.submit(task, i).result() #变成同步调用,串行了,等待结果
    # # pool.shutdown(wait=True) #关门等待所有进程完成
    # pool.shutdown(wait=False)#默认wait就等于True
    # # pool.submit(task,3333) #shutdown后不能使用submit命令
    #
    # print('主')

    #异步调用
    pool=ProcessPoolExecutor(8)
    for i in range(13):
         obj=pool.submit(task,[i,1])
         obj.add_done_callback(handle) #这里用到了回调函数
    pool.shutdown(wait=True) #关门等待所有进程完成
    print(time.time()-start_time)
##注意,创建进程池必须在if __name__ == '__main__':中,否则会报错
##其他的用法和创建线程池的一样

理解的基础上两者的实现的调用差别不大,但是使用进程池实现优雅高效,值得掌握。

4.进程间通信

进程间通信方式比较多,但是因为相当于进行IO操作,进程间通信比较慢,我实现和熟悉的是下面三种,记录备忘

1.使用队列(不适合传递大数,可以传递一些简单的Flag)

http://c.biancheng.net/view/2635.html

关键:q.put() q.get()

2.使用管道(可以传递大数,但是速度比较慢)

import numpy as np
from multiprocessing import Process, Pipe
def fun1(conn):
    a = np.ones((1000,1000,30))*3000
    print('子进程发送消息:')
    conn.send(a)
    print('子进程接受消息:')
    print(conn.recv())
    # conn.close()

if __name__ == '__main__':
    conn1, conn2 = Pipe() #关键点,pipe实例化生成一个双向管
    p = Process(target=fun1, args=(conn2,)) #conn2传给子进程
    p.start()
    print('主进程接受消息:')
    print(conn1.recv())
    print('主进程发送消息:')
    conn1.send("你好子进程")
    p.join()
    print('结束测试')

3.使用进程自带的.get()

进程函数需要设置return

5.经验和反思

5.1 本文局限

1.上述实现对于线程没有考虑线程对于共享变量的安全性:技术栈有同步、互斥锁、银行家算法等;

2.守护线程参考文章

5.2 使用场合

更重要的一点感悟

以前对于线程和进程,我只知道可以加速,却没有有考虑锅使用场合的问题,在这次比赛中吃了大亏。

(1)对于计算密集型的子程序来说,采用多线程根本没有加速的效果,因为线程的本质就是抽空干活,而计算密集型的子程序使得cpu没有抽空干活的机会,那么它又如何给你加速的机会呢,谨记谨记!

(2)进程间进行数据的交互会产生额外的I/O开销,这个也是个大问题,在比赛中进程间通信严重堵塞程序,甚至不如直接写入写出文件进行交互。

两者分别在何种场景下应用?

  • 如果你的程序有大量与数据交互/网络交互,可以使用多线程,因为程序时间瓶颈不在于GIL而是在I/O,这时多线程的小开销就比多进程更实用。
  • 如果你的程序有图形界面GUI,使用多线程,GIL锁会帮助你让你的UI线程不会产生死锁等问题。
  • 如果你的程序的运行效率与CPU密切相关的,即瓶颈在于计算等情况,且有多核可用时,就可以考虑用多进程提高效率。
5.3 多进程假死问题

python多进程间用Queue通信时,如果子进程操作Queue满了或者内容比较大的情况下,该子进程会阻塞等待取走Queue内容(如果Queue数据量比较少,不会等待),如果调用join,主进程将处于等待,等待子进程结束,造成死锁

解决方式:在调用join前,及时把Queue的数据取出,而且Queue.get需要在join前 参考文章

5.4 传入进程池的目标函数不报错

多进程代码中调试中代码出错也不报错,简直要命,程序乱飞。

def apply_async(self, func, args=(), kwds={}, callback=None,
        error_callback=None):
    '''
    Asynchronous version of `apply()` method.
    '''
    if self._state != RUN:
        raise ValueError("Pool not running")
    result = ApplyResult(self._cache, callback, error_callback)
    self._taskqueue.put(([(result._job, 0, func, args, kwds)], None))
    return result

解决方法:利用 error_callback 查看报错信息 参考文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

儒雅的钓翁

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

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

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

打赏作者

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

抵扣说明:

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

余额充值