热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Python多线程、多进程(三)之线程进程对比、多进程

Python多线程、多进程(一)之源码执行流程、GILPython多线程、多进程(二)之多线程、同步、通信Python多线程、多进程(三)之线程进程对比、多线程一、多线程与多

Python 多线程、多进程 (一)之 源码执行流程、GIL
Python 多线程、多进程 (二)之 多线程、同步、通信
Python 多线程、多进程 (三)之 线程进程对比、多线程

一、多线程与多进程的对比

在之前简单的提过,CPython中的GIL使得同一时刻只能有一个线程运行,即并发执行。并且即使是多核CPU,GIL使得同一个进程中的多个线程也无法映射到多个CPU上运行,这么做最初是为了安全着想,慢慢的也成为了限制CPython性能的问题。
一个线程想要执行,就必须得到GIL,否则就不能拿到CPU资源。但是也不是说一个线程在拿到CPU资源后就一劳永逸,在执行的过程中GIL可能会释放并被其他线程获取,所以说其它的线程会与本线程竞争CPU资源,线程是抢占式执行的。具体可在 understand GIL中看到,[传送门]。
多线程在python2中:当一个线程进行I/O的时候会释放锁,另外当ticks计数达到100(ticks可以看作是Python自身的一个计数器,也可对比着字节码指令理解,专门做用于GIL,每次释放后归零,这个计数可以通过 sys.setcheckinterval 来调整)。锁释放之后,就涉及到线程的调度,线程的锁进行,线程的切换。这是会消耗CPU资源,因此会造成程序性能问题和等待时延。另外由于线程共享内存的问题,没有进程安全性高。
但是对于多进程,GIL就无法限制,多个进程可以再多个CPU上运行,充分利用多核优势。事情往往是相对的,虽然可以充分利用多核优势,但是进程之的创建和调度却比线程的代价更高。
所以选择多线程还是多进程,主要还是看怎样权衡代价,什么样的情况。

1、CPU密集代码

下面来利用斐波那契数列模拟CPU密集运算。

def fib(n):
    # 求斐波那契数列的第n个值
    if n<=2:
        return 1
    return fib(n-1)+fib(n-2)

<1>、多进程

打印第25到35个斐波那契数,并计算程序运行时间

import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from concurrent.futures import ProcessPoolExecutor


def fib(n):
    if n<=2:
        return 1
    return fib(n-1)+fib(n-2)

if __name__ == "__main__":
    with ProcessPoolExecutor(3) as executor:  # 使用进程池控制  每次执行3个进程
        all_task = [executor.submit(fib, (num)) for num in range(25,35)]
        start_time = time.time()
        for future in as_completed(all_task):
            data = future.result()
            print("exe result: {}".format(data))

        print("last time is: {}".format(time.time()-start_time))

# 输出
exe result: 75025
exe result: 121393
exe result: 196418
exe result: 317811
exe result: 514229
exe result: 832040
exe result: 1346269
exe result: 2178309
exe result: 3524578
exe result: 5702887
last time is: 4.457437038421631

输出结果,每次打印三个exe result,总重打印十个结果,多进程运行时间为4.45秒

<2>、多线程

import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from concurrent.futures import ProcessPoolExecutor


def fib(n):
    if n<=2:
        return 1
    return fib(n-1)+fib(n-2)

if __name__ == "__main__":
    with ThreadPoolExecutor(3) as executor:  # 使用线程池控制  每次执行3个线程
        all_task = [executor.submit(fib, (num)) for num in range(25,35)]
        start_time = time.time()
        for future in as_completed(all_task):
            data = future.result()
            print("exe result: {}".format(data))

        print("last time is: {}".format(time.time()-start_time))

# 输出
exe result: 121393
exe result: 75025
exe result: 196418
exe result: 317811
exe result: 514229
exe result: 832040
exe result: 1346269
exe result: 2178309
exe result: 3524578
exe result: 5702887
last time is: 7.3467772006988525

最终程序运行时间为7.34秒

程序的执行之间与计算机的性能有关,每天计算机的执行时间都会有差异。从上述结果中看显然多线程比多进程要耗费时间。这就是因为对于密集代码(密集运算,循环语句等),tick计数很快达到100,GIL来回的释放竞争,线程之间频繁切换,所以对于密集代码的执行中,多线程性能不如对进程。

2、I/O密集代码

一个线程在I/O阻塞的时候,会释放GIL,挂起,然后其他的线程会竞争CPU资源,涉及到线程的切换,但是这种代价与较高时延的I/O来说是不足为道的。
下面用sleep函数模拟密集I/O

def random_sleep(n):
    time.sleep(n)
    return n

<1>、 多进程

def random_sleep(n):
    time.sleep(n)
    return n

if __name__ == "__main__":
    with ProcessPoolExecutor(5) as executor:
        all_task = [executor.submit(random_sleep, (num)) for num in [2]*30]
        start_time = time.time()
        for future in as_completed(all_task):
            data = future.result()
            print("exe result: {}".format(data))

        print("last time is: {}".format(time.time()-start_time))
#  输出
exe result: 2
exe result: 2
......(30个)
exe result: 2
exe result: 2
last time is: 12.412866353988647

每次打印5个结果,总共二十个打印结果,多进程运行时间为12.41秒

<2>、多线程

def random_sleep(n):
    time.sleep(n)
    return n

if __name__ == "__main__":
    with ThreadPoolExecutor(5) as executor:
        all_task = [executor.submit(random_sleep, (num)) for num in [2]*30]
        start_time = time.time()
        for future in as_completed(all_task):
            data = future.result()
            print("exe result: {}".format(data))

        print("last time is: {}".format(time.time()-start_time))

#  输出
exe result: 2
exe result: 2
......(30个)
exe result: 2
exe result: 2
last time is: 12.004231214523315

I/O密集多线程情况下,程序的性能较多进程有了略微的提高。IO密集型代码(文件处理、网络爬虫等),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO密集型代码比较友好

3、线程进程对比

  • CPU密集型代码(各种循环处理、计数等等),多线程性能不如多进程。
  • I/O密集型代码(文件处理、网络爬虫等),多进程不如多线程。

二、多进程

在python 进程、线程 (一)已经有简单的进程介绍。
不过与多线程编程相比,最需要注意的是这里多进程由并发执行变成了真正意义上的并行执行。

1、fork()调用

Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程,但是还是要有Unix/Linux系统支持,windows没有系统调用fork(),可以在本地虚拟机或者云服务器尝试,默认liunx发行版中是有python2.X的。
情况一

import os

print("Lanyu")  # 只打印一次
pid = os.fork()

if pid == 0:
  print('子进程 {} ,父进程是: {}.' .format(os.getpid(), os.getppid()))
else:
  print('我是父进程:{}.'.format(os.getpid())

# 输出
Lanyu
我是父进程:2993
子进程2994,父进程2993

fork()调用复制了一个进程,然后程序中就有两个进程,父进程的pid不为0,所以先打印子进程2994,父进程2993。然后子进程pid=0,打印我是父进程:2993。这里的Lanyu打印一次
情况二

import os


pid = os.fork()
print("Lanyu")  # 这里打印两次
if pid == 0:
  print('子进程 {} ,父进程是: {}.' .format(os.getpid(), os.getppid()))
else:
  print('我是父进程:{}.'.format(os.getpid())

# 输出
Lanyu
我是父进程:2993
Lanyu
子进程2994,父进程2993

这里的Lanyu打印两次是因为,由于fork()函数调用之后,程序立即成成一个子进程,主进程打印一次,子进程再打印一次。因此这里的Lanyu打印两次

情况三
还记得操作系统专业课的时候,老师讲的一道考研题

int main{
    fork();
    fork();
    fork():
    printf('process')
    return 0;
}

三次fork(),问此程序最终打印几个次process,关键在于fork()函数的用途,每一次都会复制一次进程,则最终,一个父进程被复制成8个进程,打印8次。

2、python多进程

虽然python中没有提供直接的进程调用函数,但是标准库中的模块提供能更多更方便的选择。 ProcessPoolExecutor进程池,与 multiprocessing标准的多进程模块。其实ProcessPoolExecutor也是对multiprocessing的封装调用,并且与ThreadPoolExecutor线程池提供的接口类似。而multiprocessing则更加底层。

<1>、进程编程

import time
import multiprocessing

def get_html(n):
    time.sleep(n)
    print("sub_progress success")
    return n

if __name__ == "__main__":
    progress = multiprocessing.Process(target=get_html, args=(2,))
    print(progress.pid)  # 打印结果为None,因为这个时候进程还未开启
    progress.start()  # 进程开启
    print(progress.pid)
    progress.join()
    print("main progress end")

# 输出
None
5056
sub_progress success
main progress end

<2>、使用进程池

import time
import multiprocessing

def get_html(n):
    time.sleep(n)
    print("sub_progress success")
    return n


if __name__ == "__main__":
    #使用进程池
    pool = multiprocessing.Pool(multiprocessing.cpu_count())  # 可以指明进程数,默认等于CPU数
    result = pool.apply_async(get_html, args=(3,))

    #等待所有任务完成
    pool.close()
    pool.join()

    print(result.get())

# 输出
sub_progress success
3

<3>、imap 接口

实例一

import time
import multiprocessing

def get_html(n):
    time.sleep(n)
    print("sub_progress success")
    return n


if __name__ == "__main__":

    # imap
    for result in pool.imap(get_html, [1,5,3]):
        print("{} sleep success".format(result))

# 输出
sub_progress success
1 sleep success
sub_progress success
sub_progress success
5 sleep success
3 sleep success

imap有点像python提供的内置函数map,讲[1,5,3]这个列表中的值一个一个传递给get_html函数对象,并按照传值的先后顺序,一一执行输出进程结果。

实例二:

import multiprocessing  

import time
def get_html(n):
    time.sleep(n)
    print("sub_progress success")
    return n


if __name__ == "__main__":

    pool = multiprocessing.Pool(multiprocessing.cpu_count())  # 可以进程数,不过最好是等于CPU数,这里也是进程数

    for result in pool.imap_unordered(get_html, [1,5,3]):
        print("{} sleep success".format(result))
# 输出
sub_progress success
1 sleep success
sub_progress success
3 sleep success
sub_progress success
5 sleep success

与imap方法不同的是imap_unordered方法,imap_unordered是按照进程的执行完成的先后顺序,打印进程执行结果,而不是依照列表中的先后顺序。可以依照需要调用。

划重点 多进程编程中,需要在__name__ == "main"下编写

更多API参考:[传送门]

3、进程通信

<1>、共享变量通信

类比线程之间的通信,首先想到的就是共享变量通信。但是在多进程中,一个进程都有自的隔离区,导致变量不能共享。
情况一

def producer(a):
    a += 100
    time.sleep(2)

def consumer(a):
    time.sleep(2)
    print(a)

if __name__ == "__main__":
    a = 1
    my_producer = Process(target=producer, args=(a,))
    my_cOnsumer= Process(target=consumer, args=(a,))
    my_producer.start()
    my_consumer.start()
    my_producer.join()
    my_consumer.join()

# 输出
1

结果进程没有共享变量。

但是Python的标准模块提供了Manager()在内存中划出一块单独的内存区,供所有的进程使用,共享变量。
情况二

from multiprocessing import Process, Manager

def add_data(p_dict, key, value):
    p_dict[key] = value

if __name__ == "__main__":
    progress_dict = Manager().dict()

    first_progress = Process(target=add_data, args=(progress_dict, "666", 666))  # 更新progress_dict
    second_progress = Process(target=add_data, args=(progress_dict, "999", 999))  # 更新progress_dict

    first_progress.start()
    second_progress.start()
    first_progress.join()
    second_progress.join()

    print(progress_dict)

# 打印结果
{'666': 666, '999': 999}  # 实现了变量的共享

在Manager中还可以有其它的数据结构,例如列表数组等可共享使用。

因此,在使用多进程编程的时候,如果像情况二共享全局变量,就仍旧需要加锁实现进程同步。

<2>、Queue队列通信

在multiprocessing模块中有Queue类安全的队列,也可以实现通信,不过在这种情况下无法联通线程池。

import time
from multiprocessing import Process, Queue, Pool, Manager

def producer(queue):
    queue.put("a")
    time.sleep(2)

def consumer(queue):
    time.sleep(2)
    data = queue.get()
    print(data)

if __name__ == "__main__":
    queue = Queue(10)  # 使用普通的Queue
    pool = Pool(2)

    pool.apply_async(producer, args=(queue,))
    pool.apply_async(consumer, args=(queue,))

    pool.close()
    pool.join()

# 无输出

想要使用进程池又实现消息队列通信就需要用到Manager管理者

import time
from multiprocessing import Process, Queue, Pool, Manager

def producer(queue):
    queue.put("a")
    time.sleep(2)

def consumer(queue):
    time.sleep(2)
    data = queue.get()
    print(data)

if __name__ == "__main__":
    queue = Manager().Queue(10)  # 在使用Manger的时候需要先将Manager实例化在调用Queue
    pool = Pool(2)

    pool.apply_async(producer, args=(queue,))
    pool.apply_async(consumer, args=(queue,))

    pool.close()
    pool.join()

# 输出
正常打印字符a

<3>、pipe管道通信

pipe也用于进程通信,从功能上说,提供的接口应该是queue的子集。但是queue为了更好的控制,所以内部加了很多的锁,而pipe在两个进程通信的时候性能会比queue更好一些。

def producer(pipe):
    pipe.send("Lanyu")

def consumer(pipe):
    print(pipe.recv())

if __name__ == "__main__":
    recevie_pipe, send_pipe = Pipe()
    #pipe只能适用于两个进程
    my_producer= Process(target=producer, args=(send_pipe, ))
    my_cOnsumer= Process(target=consumer, args=(recevie_pipe,))

    my_producer.start()
    my_consumer.start()
    my_producer.join()
    my_consumer.join()

# 输出
Lanyu

三、总结

最开始为了引出GIL,简单输了python源码的执行流程,也是先编译成字节码再执行。在CPython中,为了数据完整性和状态同步才有GIL,GIL同样使得多线程不能利用CPU多核优势,所以性能低部分是因为GIL。

线程需要加上GIL才能获取CPU资源,才能执行。线程通信的时候,可以用消息队列Queue和全局变量,但是对于全局变量这种通信方式,在执行字节码一定数量之后,会释放GIL,线程抢占式执行同样导致变量的混乱,所以我们加上了用户级别的互斥锁Lock,或者迭代锁Rlock保证了线程的状态同步。condition帮我们实现了线程的复杂通信,而semaphore信号量,使得我们在多个线程的情况下,控制并发线程的数量。线程池进一步的封装,提供了对线程的状态,异步控制等操作。

对于多进程,可以利用多核CPU优势,但是使用多线程和多进程还需要进一步根据密集I/O和密集运算型代码等具体情况。多进程标准模块中提供的接口与多线程类似,可相互参照。

陆陆续续总结关于这篇博文也有一个多星期了,但是还是感觉有说不清楚的地方逻辑不通,希望读者能在评论区指出。期间参阅了很多的文档,博客,教程。
印象最深刻的还是Understand GIL: [传送门]这篇关于GIL的解释,虽然是英文文档,但是作者总是能以最精炼的句子表达最清晰的观点。

上一篇:Python 多线程、多进程 (二)之 多线程、同步、通信


推荐阅读
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文介绍了机器学习手册中关于日期和时区操作的重要性以及其在实际应用中的作用。文章以一个故事为背景,描述了学童们面对老先生的教导时的反应,以及上官如在这个过程中的表现。同时,文章也提到了顾慎为对上官如的恨意以及他们之间的矛盾源于早年的结局。最后,文章强调了日期和时区操作在机器学习中的重要性,并指出了其在实际应用中的作用和意义。 ... [详细]
  • 本文由编程笔记#小编为大家整理,主要介绍了logistic回归(线性和非线性)相关的知识,包括线性logistic回归的代码和数据集的分布情况。希望对你有一定的参考价值。 ... [详细]
  • 超级简单加解密工具的方案和功能
    本文介绍了一个超级简单的加解密工具的方案和功能。该工具可以读取文件头,并根据特定长度进行加密,加密后将加密部分写入源文件。同时,该工具也支持解密操作。加密和解密过程是可逆的。本文还提到了一些相关的功能和使用方法,并给出了Python代码示例。 ... [详细]
  • 生成式对抗网络模型综述摘要生成式对抗网络模型(GAN)是基于深度学习的一种强大的生成模型,可以应用于计算机视觉、自然语言处理、半监督学习等重要领域。生成式对抗网络 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 本文介绍了Windows操作系统的版本及其特点,包括Windows 7系统的6个版本:Starter、Home Basic、Home Premium、Professional、Enterprise、Ultimate。Windows操作系统是微软公司研发的一套操作系统,具有人机操作性优异、支持的应用软件较多、对硬件支持良好等优点。Windows 7 Starter是功能最少的版本,缺乏Aero特效功能,没有64位支持,最初设计不能同时运行三个以上应用程序。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • Day2列表、字典、集合操作详解
    本文详细介绍了列表、字典、集合的操作方法,包括定义列表、访问列表元素、字符串操作、字典操作、集合操作、文件操作、字符编码与转码等内容。内容详实,适合初学者参考。 ... [详细]
  • MATLAB函数重名问题解决方法及数据导入导出操作详解
    本文介绍了解决MATLAB函数重名的方法,并详细讲解了数据导入和导出的操作。包括使用菜单导入数据、在工作区直接新建变量、粘贴数据到.m文件或.txt文件并用load命令调用、使用save命令导出数据等方法。同时还介绍了使用dlmread函数调用数据的方法。通过本文的内容,读者可以更好地处理MATLAB中的函数重名问题,并掌握数据导入导出的各种操作。 ... [详细]
  • IOS开发之短信发送与拨打电话的方法详解
    本文详细介绍了在IOS开发中实现短信发送和拨打电话的两种方式,一种是使用系统底层发送,虽然无法自定义短信内容和返回原应用,但是简单方便;另一种是使用第三方框架发送,需要导入MessageUI头文件,并遵守MFMessageComposeViewControllerDelegate协议,可以实现自定义短信内容和返回原应用的功能。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • 本文介绍了Python语言程序设计中文件和数据格式化的操作,包括使用np.savetext保存文本文件,对文本文件和二进制文件进行统一的操作步骤,以及使用Numpy模块进行数据可视化编程的指南。同时还提供了一些关于Python的测试题。 ... [详细]
author-avatar
书友68610983
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有