Python多线程和多进程

文章讲述了线程、进程、CPU密集型和IO密集型的区别,以及在Python中如何通过单线程、多线程和多进程处理这两种类型的任务。发现Python多线程在IO密集型场景中有性能提升,而在CPU密集型场景中受限于GIL,多进程则更为适用。
摘要由CSDN通过智能技术生成

一些基本概念

线程:利用cpu和io可以同时执行的原理,让cpu不会傻傻的等待io完成。
进程:利用多核cpu的能力,真正的并行执行任务。

cpu密集型:cpu密集型也叫计算密集型,io在很短的时间内就可以完成,cpu需要大量的计算和处理,特点就是cpu占用率相当高。
io密集型:io密集型指的是系统运作大部分状况都是cpu在等待io操作,cpu占用率很低。

串行:单进程中任务是按时间轴一个一个按照顺序执行的,其中可能会阻塞但是不会出现前一个任务还没结束,就开始执行下一个任务(任务的交叉执行)当cpu执行任务A时,在时间点 cpu调度执行任务B时,当 时刻任务A获取到cpu时间片后,仍然会从 时执行的代码开始往下执行,这样并不会造成资源的抢占,也就是并发问题。

这就像一群小朋友在排队等老师发糖果,排到最前面的小朋友先拿到糖果,老师回个微信停止发糖果或者老师暂时离开教室(此时教室的环境和小朋友的排队状态都没变)过了10分钟后回来继续发糖果,都是被允许的。

并行:多个进程同时执行任务,在多核cpu下,同时一时刻每一个核都在执行任务,且任务的总数等于cpu核心数,也就是说同一时刻任务在执行的时候没有发生cpu上下文切换的问题。这是一种理想状态。但在实际场景中,处于运行状态的任务是非常多的,很难做到真正意义上的并行。

还是那一群小朋友在排队领糖果,此时教室里又来了3个老师队伍被分成了4个,由4个老师在一个一个发糖果,发糖果结束的时间由队伍的长度(任务数)或者老师发的速度有关(cpu性能),老师在发糖果的过程中可以回微信(io操作)但是不能离开房间了(cpu上下文切换)。有的老师先发完糖果后也不能帮其他老师发糖果。这是理想的并发。

并发:指的是多个任务可以“同时"运行的现象,它是一种现象。是多任务"同时"运行的现象。对于单核心CPU来说,同一时刻只能运行一个任务。由于cpu上下文快速切换,使得看上去有多个任务同时都在运行,这种同时运行是一种假象。当任务数超过cpu核心数时,必然会导致cpu的上下文切换。此时需要考虑并发的问题。

并发的百度百科解释:
在这里插入图片描述

Python语言慢的主要原因

动态类型语言解释边执行
GIL 无法利用多核cpu并发执行

单/多线程在io密集型和cpu密集型的测试

lscpu 指令可以看到系统处理器架构为x86_64 32核服务器。

实验设计:

场景一 单/多线程大量请求数据库读取数据查看耗时,io密集型测试。
场景二 单/多线程/多进程计算cal_test函数查看耗时,cpu密集型测试。
加载网络请求读取数据属于io操作,不占用cpu执行时间;计算log求和属于cpu密集型操作占用cpu。

实验目的:

场景一验证Python多线程在io密集型操作的业务中可以提升性能。
场景二验证Pyhton多线程在cpu密集型业务没有性能提升,多进程可以提升性能。

场景一 单线程读取数据库数据

def query_db():
    with app.app_context():
        for i in range(10000):
            id = 1
            fraud_words = Fraud.query.get_or_404(id)
            label = fraud_words.fraud_type_word
            label_json = json.loads(label)


if __name__ == "__main__":
    t = time.time()
    th = threading.Thread(target=query_db)
    th.start()
    th.join()
    print(f"消耗时间:{time.time() - t} s")

    time.sleep(60)

在这里插入图片描述
在这里插入图片描述

程序启动可以在服务器上看到cpu累计执行时间远小于程序执行时间,可以简单说明程序在读取数据是不占用cpu时间片的,10000条数据的查询大概需要20s。

场景一 多线程读取数据库数据

def query_db():
    with app.app_context():
        for i in range(1000):
            id = 1
            fraud_words = Fraud.query.get_or_404(id)
            label = fraud_words.fraud_type_word
            label_json = json.loads(label)

        name = threading.current_thread().name
        print(f"线程:{name},消耗时间:{time.time() - t}")


if __name__ == "__main__":
    t = time.time()
    thread_list = []
    for i in range(10):
        thread = threading.Thread(target=query_db, name=f"threading_{i}")
        thread_list.append(thread)

    for th in thread_list:
        th.start()

    for th in thread_list:
        th.join()

    print(f"消耗时间:{time.time() - t} s")

在这里插入图片描述

将10000次数据库读取的任务分给10个线程去做,从实验结果中可以看到在10s左右就可以完成10000次的数据读取,在大量的io操作场景中我们可以适当的增加线程来提升效率。

场景二:单线程计算cal_test函数

def cal_test():
    t = time.time()
    sum = 0
    for i in range(10000000):
        x = random.randint(1, 3)
        sum = sum + math.log(x - 0.5)

    # print(f"{multiprocessing.current_process().name}执行时间:{time.time() - t} s")
    # print(f"{threading.current_thread().name}执行时间:{time.time() - t} s")


if __name__ == "__main__":
    t = time.time()
    th = threading.Thread(target=cal_test)
    th.start()
    th.join()
    print(f"消耗时间:{time.time() - t} s")

在这里插入图片描述

**加粗样式**

1000万次的log计算并求和程序执行大概16s,任务进程cpu一直处于近100%的状态

场景二:多线程计算cal_test函数

def cal_test():
    t = time.time()
    sum = 0
    for i in range(10000000):
        x = random.randint(1, 3)
        sum = sum + math.log(x - 0.5)

    # print(f"{multiprocessing.current_process().name}执行时间:{time.time() - t} s")
    print(f"{threading.current_thread().name}执行时间:{time.time() - t} s")


if __name__ == "__main__":
    t = time.time()
    thread_list = []
    for i in range(10):
        thread = threading.Thread(target=cal_test, name=f"threading_{i}")
        thread_list.append(thread)

    for th in thread_list:
        th.start()

    for th in thread_list:
        th.join()

    print(f"消耗时间:{time.time() - t} s")

    time.sleep(60)

从结果上来看这可能和我们的预期不符合,10个线程在计算cal_test每个线程应该在16s左右,但是每个线程执行的时间大致上是单个线程执行cal_test函数时间的10倍左右。由于GIL的存在同一时刻cpu只能执行一个任务,其他任务处于阻塞状态,导致10个cal_test函数的计算时间是1个cal_test函数的10倍左右。通过上例可以看到在cpu密集型的任务中,多线程可能并不适用。(此例中相当于10个计算任务在排队获取cpu资源,某个时间点只能有一个任务在执行,并没有充分利用多核cpu的计算能力)

场景二:多进程计算cal_test函数
多线程并没有像我们想象的那样充分利用多核cpu的计算能力,python使用多进程可以解决这个问题。

def cal_test():
    t = time.time()
    sum = 0
    for i in range(10000000):
        x = random.randint(1, 3)
        sum = sum + math.log(x - 0.5)

    print(f"{multiprocessing.current_process().name}执行时间:{time.time() - t} s")
    # print(f"{threading.current_thread().name}执行时间:{time.time() - t} s")


if __name__ == "__main__":
    t = time.time()
    proc_list = []
    for i in range(10):
        proc = multiprocessing.Process(target=cal_test, name=f"processing_{i}")
        proc_list.append(proc)

    for proc in proc_list:
        proc.start()

    for proc in proc_list:
        proc.join()

    print(f"消耗时间:{time.time() - t} s")
    time.sleep(60)
  1. top 指令可以看出有10个正在处于运行的任务且进程cpu是跑满的,每个进程执行的时间都在单个计算函数cal_test的时间上下浮动,说明系统充分利用到了多核cpu的计算能力。
  2. 在16s的时间内我们可以执行10个类似cal_test函数的任务,适当增加进程数可以提高程序执行的效率。此外我们可以看到cpu累计使用时间和任务执行的时间基本保持一致。

总结

  • Python多线程适用于io密集型场景。
  • Python多进程适用于cpu密集型场景。
  • 串行:一次只能执行一个任务,该任务阻塞时其他任务只能等待。
  • 并行:通过多进程/多线程的方式取得多个任务,同时执行这些任务。
  • 并发:一种cpu在多任务之间来回切换执行的现象,看起来像是多个任务同时在执行。这些任务可能是并行执行的,也可能是串行执行的,和cpu核心数无关,是操作系统进程调度和cpu上下文切换达到的结果。
  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值