python 模块开发,python高并发框架

  python 模块开发,python高并发框架

  本文主要介绍python的未来,并详细说明Python并发编程的未来模块前景。有需要的朋友可以借鉴一下,希望能有所帮助。祝大家进步很大,早日升职加薪。

  00-1010区分并发和并行并发编程期货到底什么是期货?为什么多线程一次只由一个线程执行?结论无论哪种语言,并发编程都是一项非常重要的技能。比如我们上一章使用的爬虫,广泛应用于工业的各个领域。我们每天在各种网站、app上获取的新闻信息,很大一部分是通过并发编程版本的爬虫获取的。

  正确使用并发编程无疑会给我们的程序带来巨大的性能提升。今天,我们将学习Python中的并发编程——Futures。

  

目录

  我们在学习并发编程的时候,经常会听到两个词:并发和并行。两者经常一起使用,导致很多人以为是一个意思,其实是错的。

  首先,要识别一个误区。在Python中,并发不仅仅是同时执行多个操作(线程或任务)。相反,它只允许一个操作在特定时刻发生,但线程或任务会相互切换,直到完成,如下图所示。

  在上图中,有两种不同的方式来切换顺序:任务和线程。它们对应于Python中两种形式的并发:——threading和asyncio。

  对于线程,操作系统知道每个线程的所有信息,所以会在适当的时候率先切换线程。这样做的好处是代码容易写,因为程序员不需要做任何切换操作;但是,切换线程的操作可能发生在语句的执行过程中(例如,X=1),这使得竞争条件更容易发生。

  至于asyncio,当主程序要切换任务时,必须通知它这个任务是可以切换的,这样就可以避免上面的竞态条件。

  至于并行,只是同时发生而已。这就是Python中多处理的含义,对应的是多进程。我们可以简单的理解,如果我们的电脑是8核CPU,我们可以强制Python打开8个进程同时执行,加快程序的运行速度。大概就是下图的思路吧。

  相比之下,并发通常用于I/O操作频繁的场景。比如我们要从一个网站下载多个文件,I/O操作的时间要比CPU操作的时间长很多,所以并发更适合。在CPU被大量使用的情况下,为了加快运算速度,我们将多使用几台机器,让多个处理器进行计算。

  还记得之前写博客,总结Python中的多线程是一种通过CPU切换上下文实现的“伪多线程”。在大量线程切换的过程中,会占用更多的CPU资源。但在进行IO操作时(无论是在网络上交换数据,还是从内存或硬盘中读写数据),都不需要CPU进行计算。所以多线程只适合IO密集型的环境,不适合计算密集型的操作。

  

区分并发和并行

  单线程和多线程的性能比较

  下面我们通过一个例子从代码的角度来理解一下并发编程中的Futures,并进一步比较它与单线程的性能差异。

  假设我们有一个任务,从网站下载一些内容并打印出来,如果是以单线程的方式完成的话。

  导入请求

  导入时间

  def下载_one(网址):

  resp=requests.get(url)

  打印(从{}中读取{})。格式(长度(对应内容),url))

  def下载_全部(网址):

  对于urls:中的url

  下载_一个(网址)

  定义主():

  站点=[

   https://en . Wikipedia . org/wiki/portal : arts ,

   https://en . Wikipedia . org/wiki/portal : history ,

   https://en . Wikipedia . org/wiki/portal : society ,

   https://en . Wikipedia . org/wiki/portal :传记,

  超文本传送协议(Hyper Text Transport Protocol的缩写)

  s://en.wikipedia.org/wiki/Portal:Mathematics,

   https://en.wikipedia.org/wiki/Portal:Technology,

   https://en.wikipedia.org/wiki/Portal:Geography,

   https://en.wikipedia.org/wiki/Portal:Science,

   https://en.wikipedia.org/wiki/Computer_science,

   https://en.wikipedia.org/wiki/Python_(programming_language),

   https://en.wikipedia.org/wiki/Java_(programming_language),

   https://en.wikipedia.org/wiki/PHP,

   https://en.wikipedia.org/wiki/Node.js,

   https://en.wikipedia.org/wiki/The_C_Programming_Language,

   https://en.wikipedia.org/wiki/Go_(programming_language)

   ]

   start_time = time.perf_counter()

   download_all(sites)

   end_time = time.perf_counter()

   print(Download {} sites in {} seconds.format(len(sites),end_time-start_time))

  if __name__ == __main__:

   main()

  

  这是种最简单暴力最直接的方式:

  先遍历存储网站的列表

  对当前的网站进行下载操作

  当前操作完成后,再对下一个网站进行同样的操作,一直到结束。

  可以试出来总耗时大概是2s多,单线程的方式简单明了,但是最大的问题是效率低下,程序最大的时间都消耗在I/O等待上(这还是用的print,如果是写在硬盘上的话时间会更多)。如果在实际生产环境中,我们需要访问的网站至少是以万为单位的,所以这个方案根本行不通。

  接着我们看看多线程版本的代码

  

import concurrent.futures

  import requests

  import threading

  import time

  def download_one(url):

   resp = requests.get(url).content

   print(Read {} from {}.format(len(resp),url))

  def download_all(sites):

   with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:

   executor.map(download_one,sites)

  def main():

   sites = [

   https://en.wikipedia.org/wiki/Portal:Arts,

   https://en.wikipedia.org/wiki/Portal:History,

   https://en.wikipedia.org/wiki/Portal:Society,

   https://en.wikipedia.org/wiki/Portal:Biography,

   https://en.wikipedia.org/wiki/Portal:Mathematics,

   https://en.wikipedia.org/wiki/Portal:Technology,

   https://en.wikipedia.org/wiki/Portal:Geography,

   https://en.wikipedia.org/wiki/Portal:Science,

   https://en.wikipedia.org/wiki/Computer_science,

   https://en.wikipedia.org/wiki/Python_(programming_language),

   https://en.wikipedia.org/wiki/Java_(programming_language),

   https://en.wikipedia.org/wiki/PHP,

   https://en.wikipedia.org/wiki/Node.js,

   https://en.wikipedia.org/wiki/The_C_Programming_Language,

   https://en.wikipedia.org/wiki/Go_(programming_language)

   ]

   start_time = time.perf_counter()

   download_all(sites)

   # for i in sites:

   end_time = time.perf_counter()

   # print(Down {} sites in {} seconds.format(len(sites),end_time-start_time))

  if __name__ == __main__:

   main()

  

  这段代码的运行时长大概是0.2s,效率一下提升了10倍多,可以注意到这个版本和单线程的区别主要在下面:

  

def download_all(sites):

   with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:

   executor.map(download_one,sites)

  

  在上面的代码中我们创建了一个线程池,有5个线程可以分配使用。executer.map()与以前将的Python内置的map()函数,表示对sites中的每一个元素并发的调用函数download_one()函数。

  顺便提一下,在download_one()函数中,我们使用的requests.get()方法是线程安全的(thread-safe),因此在多线程的环境下,它也可以安全使用,并不会出现race condition(条件竞争)的情况。

  另外,虽然线程的数量可以自己定义,但是线程数并不是越多越好,以为线程的创建、维护和删除也需要一定的开销。所以如果设置的很大,反而会导致速度变慢,我们往往要根据实际的需求做一些测试,来寻找最优的线程数量。

  当然,我们也可以用并行的方式去提高运行效率,只需要在download_all()函数中做出下面的变化即可

  

def download_all(sites):

   with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:

   to_do = []

   for site in sites:

   future = executor.submit(download_one,site)

   to_do.append(site)

   for future in concurrent.futures.as_completed(to_do):

   future.result()

  

  在需要改的这部分代码中,函数ProcessPoolExecutor()表示创建进程池,使用多个进程并行的执行程序。不过,这里 通常省略参数workers,因为系统会自动返回CPU的数量作为可以调用的进程数。

  就像上面说的,并行方式一般用在CPU密集型的场景中,因为对于I/O密集型操作多数时间会用于等待,相比于多线程,使用多进程并不会提升效率,反而很多时候,因为CPU数量的限制,会导致执行效率不如多线程版本。

  

  

到底什么是Futures?

  Python中的Futures,位于concurrent.futures和asyncio中,他们都表示带有延迟的操作,Futures会将处于等待状态的操作包裹起来放到队列中,这些操作的状态可以随时查询。而他们的结果或是异常,也能在操作后被获取。

  通常,作为用户,我们不用考虑如何去创建Futures,这些Futures底层会帮我们处理好,我们要做的就是去schedule这些Futures的执行。比方说,Futures中的Executor类,当我们中的方法done(),表示相对应的操作是否完成——用True表示已完成,ongFalse表示未完成。不过,要注意的是done()是non-blocking的,会立刻返回结果,相对应的add_done_callback(fn),则表示Futures完成后,相对应的参数fn,会被通知并执行调用。

  Futures里还有一个非常重要的函数result(),用来表示future完成后,返回器对应的结果或异常。而as_completed(fs),则是针对给定的future迭代器fs,在其完成后,返回完成后的迭代器。

  所以也可以把上面的例子写成下面的形式:

  

def download_all(sites):

   with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:

   to_do = []

   for site in sites:

   future = executor.submit(download_one,site)

   to_do.append(site)

   for future in concurrent.futures.as_completed(to_do):

   future.result()

  

  这里,我们首先用executor.submit(),将下载每个网站的内容都放进future队列to_do里等待执行。然后是as_completed()函数,在future完成后输出结果

  不过这里有个事情要注意一下:future列表中每个future完成的顺序和他在列表中的顺序不一定一致,至于哪个先完成,取决于系统的调度和每个future的执行时间。

  

  

为什么多线程每次只有一个线程执行?

  前面我们讲过,在一个时刻下,Python主程序只允许有一个线程执行,所以Python的并发,是通过多线程的切换完成的,这是为什么呢?

  这就又和以前讲的知识串联到一起了——GIL(全局解释器锁),这里在复习下:

  事实上,Python的解释器并不是线程安全的,为了解决由此带来的race condition等问题,Python就引入了GIL,也就是在同一个时刻,只允许一个线程执行。当然,在进行I/O操作是,如果一个线程被block了,GIL就会被释放,从而让另一个线程能够继续执行。

  

  

总结

  这节课里我们先学习了Python中并发和并行的概念

  并发——通过线程(thread)和任务(task)之间相互切换的方式实现,但是同一时刻,只允许有一个线程或任务执行

  并行——多个进程同时进行。

  并发通常用于I/O频繁操作的场景,而并行则适用于CPU heavy的场景

  随后我们通过一个下载网站内容的例子,比较了单线程和运用FUtures的多线程版本的性能差异,显而易见,合理的运用多线程,能够极大的提高程序运行效率。

  我们还大致了解了Futures的方式,介绍了一些常用的函数,并辅以实例加以理解。

  要注意,Python中之所以同一时刻只允许一个线程运行,其实是由于GIL的存在。但是对于I/O操作而言,当其被block的时候,GIL会被释放,使其他线程继续执行。

  以上就是Python并发编程之未来模块Futures的详细内容,更多关于Python并发未来模块Futures的资料请关注盛行IT软件开发工作室其它相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: