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

python协程可以嵌套协程吗_Python爬虫进阶教程(二):线程、协程

简介线程线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。

简介

线程

线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。 线程没有自己的系统资源,只拥有在运行时必不可少的资源。但线程可以与同属与同一进程的其他线程共享进程所拥有的其他资源。

8be6fb04f493150ea480c065efc9a8bc.png

多线程类似同时执行多个不同程序,python的标准库提供了两个模块thread和threading,thread是低级模块,threading是高级模块,对thread进行了封装,大多情况下,我们只使用threading模块即可。

f14197aed342c48ccda0958d061faae5.png

threading模块

此模块在较低级别的thread模块之上构建的更高级别的线程接口,一般通过两种方式实现多线程,第一种方式是把一个函数传入并创建实例,然后调用start方法执行;第二种方式是直接从threading.Thread继承并创建线程类,然后重写init方法和run方法。

第一种方式代码示例:

import timeimport randomimport threading​def t_run(urls): """ 线程执行代码 """ # threading.current_thread()返回当前的Thread对象,对应于调用者控制的线程。 # 如果调用者控制的线程不是通过threading模块创建的,则返回一个只有有限功能的虚假线程对象。 print('Current %s is running...' % threading.current_thread().name) for url in urls: print(' threading %s -----> %s ' % (threading.current_thread().name, url)) time.sleep(random.random()) print('%s ended.' % threading.current_thread().name)​if __name__ == '__main__': # 创建两个线程实例 t1 = threading.Thread(target=t_run, name='Thread_1', args=(['url1', 'url2'],)) t2 = threading.Thread(target=t_run, name='Thread_2', args=(['url3', 'url4'],)) # 启动线程 t1.start() t2.start() # 等待线程结束 t1.join() t2.join() print('%s ended.' % threading.current_thread().name)

运行结果如下:

Current Thread_1 is running... threading Thread_1 -----> url1 Current Thread_2 is running... threading Thread_2 -----> url3 threading Thread_1 -----> url2 threading Thread_2 -----> url4 Thread_2 ended.Thread_1 ended.MainThread ended.

第二种方式用threading.Thread继承创建线程类

import timeimport randomimport threading​class MyThread(threading.Thread): """ 定义线程类 """​ def __init__(self, name, urls): """ 初始化,重写线程 """ threading.Thread.__init__(self, name=name) self.urls = urls​ def run(self): """ 执行函数 """ # 打印当前线程名 print('Current %s is running...' % threading.current_thread().name) for url in self.urls: print('Thread %s ------> %s' % (threading.current_thread().name, url)) time.sleep(random.random()) print('%s ended.' % threading.current_thread().name)​if __name__ == '__main__': print('%s is running...' % threading.current_thread().name) t1 = MyThread(name='Thread_1', urls=['url1', 'url2']) t2 = MyThread(name='Thread_2', urls=['url3', 'url4']) t1.start() t2.start() t1.join() t2.join() print('%s ended.' % threading.current_thread().name)

结果如下:

MainThread is running...Current Thread_1 is running...Thread Thread_1 ------> url1Current Thread_2 is running...Thread Thread_2 ------> url3Thread Thread_1 ------> url2Thread Thread_2 ------> url4Thread_1 ended.Thread_2 ended.MainThread ended.

线程同步

如果多个线程共同对某个数据进行修改,就有可能会造成不可预料的结果,为了防止这种情况发生,需要对线程进行同步,使用Lock和Rlock可以实现简单线程同步。

Lock 对象

一个可重入锁处于“locked”或者“unlocked”状态中的一种。它创建时处于unlocked状态。它有两个基本方法,acquire()和release()。当状态是unlocked时,acquire()改变该状态为locked并立即返回。当状态被锁定时,acquire()阻塞,直到在另一个线程中对release()的调用将其改为unlocked,然后acquire()执行,release()方法只应在锁定状态下调用;它将状态更改为已解锁并立即返回。如果尝试释放已解锁的锁,将会引发RuntimeError。

Rlock 对象

一个可重入锁必须由获得它的线程释放。一旦线程获得了可重入锁,同一线程可以再次获取它而不阻塞;在所有的release操作完成后,别的线程才能申请Rlock对象,见下面例子:

import threading# 创建Rlock实例lock = threading.RLock()# 定义变量num = 0​class MyThread(threading.Thread): """ 定义线程类 """​ def __init__(self, name): """ 重新定义name """ threading.Thread.__init__(self, name=name)​ def run(self): """ 执行函数 """ # 全局变量num global num while True: # 加锁 lock.acquire() print('%s locked, Number: %d' % (threading.current_thread().name, num)) if num >= 4: # 解锁 lock.release() print('%s released, Number: %d' % (threading.current_thread().name, num)) break num += 1 print('%s released, Number: %d' % (threading.current_thread().name, num)) lock.release()​if __name__ == '__main__': thread1 = MyThread('Thread_1') thread2 = MyThread('Thread_2') thread3 = MyThread('Thread_3') thread1.start() thread2.start() thread3.start()

运行结果如下:

Thread_1 locked, Number: 0Thread_1 released, Number: 1Thread_1 locked, Number: 1Thread_1 released, Number: 2Thread_1 locked, Number: 2Thread_1 released, Number: 3Thread_1 locked, Number: 3Thread_1 released, Number: 4Thread_1 locked, Number: 4Thread_1 released, Number: 4Thread_2 locked, Number: 4Thread_2 released, Number: 4Thread_3 locked, Number: 4Thread_3 released, Number: 4

可以看出Rlock锁只有线程1的num为4时,调用release方法,全部解锁后,线程2才可以调用,线程2开始时num就是4,所以也直接到if判断结束,调用release后,线程3开始执行。

全局解释器锁(GIL)

首先说的一点是GIL并不是Python的特性,它是Python解析器(CPython)引入的一个概念。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。

GIL全称Global Interpreter Lock,是一个互斥锁,它可以防止多个本地线程同时执行Python的某个值,毫无疑问全局锁的存在会对多线程的效率有不小影响。几乎等于Python是个单线程的程序。(这也是大家吐槽python多线程慢的槽点)

协程

协程又称微线程、纤程,就好比同时开启多个任务,但一次只顺序执行一个。等到所执行的任务遭遇阻塞,就切换到下一个任务继续执行,以期节省下阻塞所占用的时间。

协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其它协程共享全局数据和其它资源。对CPU来说协程就是单线程,不必考虑切换开销。

那么python如何实现协程呢?Python对协程的支持是通过generator实现的。在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。但是Python的yield不但可以返回一个值,它还可以接收调用者发出的参数。见下面简单生产者消费者示例:

def consumer(): r &#61; &#39;&#39; while True: # 这个地方注意&#xff0c;到达这个yield后&#xff0c;就会抛出n的值&#xff0c;暂停等待next或send继续 n &#61; yield r if not n: return print(&#39;[CONSUMER] Consuming %s ...&#39; % n) r &#61; &#39;200 OK&#39;​def produce(c): c.send(None) n &#61; 0 while n <5: n &#61; n &#43; 1 print(&#39;[PRODUCER] Porducing %s ...&#39; % n) r &#61; c.send(n) print(&#39;[PRODUCER] Consumer return: %s...&#39; % r) c.close()​c &#61; consumer()produce(c)

结果如下&#xff1a;

[PRODUCER] Porducing 1 ...[CONSUMER] Consuming 1 ...[PRODUCER] Consumer return: 200 OK...[PRODUCER] Porducing 2 ...[CONSUMER] Consuming 2 ...[PRODUCER] Consumer return: 200 OK...[PRODUCER] Porducing 3 ...[CONSUMER] Consuming 3 ...[PRODUCER] Consumer return: 200 OK...[PRODUCER] Porducing 4 ...[CONSUMER] Consuming 4 ...[PRODUCER] Consumer return: 200 OK...[PRODUCER] Porducing 5 ...[CONSUMER] Consuming 5 ...[PRODUCER] Consumer return: 200 OK...

注意到consumer函数是一个generator&#xff0c;把一个consumer传入produce后&#xff1a;

首先调用c.send(None)启动生成器&#xff1b;

然后&#xff0c;一旦生产了东西&#xff0c;通过c.send(n)切换到consumer执行&#xff1b;

consumer通过yield拿到消息&#xff0c;处理&#xff0c;又通过yield把结果传回&#xff1b;

produce拿到consumer处理的结果&#xff0c;继续生产下一条消息&#xff1b;

produce决定不生产了&#xff0c;通过c.close()关闭consumer&#xff0c;整个过程结束。

整个流程无锁&#xff0c;由一个线程执行&#xff0c;produce和consumer协作完成任务&#xff0c;所以称为“协程”&#xff0c;而非线程的抢占式多任务。

最后套用Donald Knuth的一句话总结协程的特点&#xff1a;

“子程序就是协程的一种特例。”

asyncio

asyncio是Python 3.4版本引入的标准库&#xff0c;直接内置了对异步IO的支持。用asyncio实现Hello world代码如下&#xff1a;

import asyncio# 把一个generator标记为协程类型&#64;asyncio.coroutinedef hello(): """ 定义一个生成器 """ print(&#39;Hello world!&#39;) # 通过yield from调用另一个标记为协程的生成器 r &#61; yield from asyncio.sleep(2) print(&#39;Hello again!&#39;)# 生成实例loop &#61; asyncio.get_event_loop()# 将标记为协程的生成器执行loop.run_until_complete(hello())loop.close()

执行结果&#xff1a;

Hello world!# 此处等待了2秒Hello again!

从结果可以看出hello()会首先打印出Hello world!&#xff0c;然后&#xff0c;yield from语法可以让我们方便地调用另一个generator。由于asyncio.sleep()也是一个coroutine&#xff0c;所以线程不会等待asyncio.sleep()&#xff0c;而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时&#xff0c;线程就可以从yield from拿到返回值(此处是None)&#xff0c;然后接着执行下一行语句。把asyncio.sleep(1)看成是一个耗时1秒的IO操作&#xff0c;在此期间&#xff0c;主线程并未等待&#xff0c;而是去执行EventLoop中其他可以执行的coroutine了&#xff0c;因此可以实现并发执行。

我们用Task封装两个coroutine试试&#xff1a;

import asyncioimport threading&#64;asyncio.coroutinedef hello(): print(&#39;Hello world! %s&#39; % threading.current_thread()) r &#61; yield from asyncio.sleep(5) print(&#39;Hello again! %s&#39; % threading.current_thread())loop &#61; asyncio.get_event_loop()tasks &#61; [hello(), hello()]loop.run_until_complete(asyncio.wait(tasks))loop.close()

输出结果如下&#xff1a;

Hello world! <_mainthread started>Hello world! <_mainthread started># 此处暂停5秒Hello again! <_mainthread started>Hello again! <_mainthread started>

由打印的当前线程名称可以看出&#xff0c;两个coroutine是由同一个线程并发执行的。

如果把asyncio.sleep()换成真正的IO操作&#xff0c;则多个coroutine就可以由一个线程并发执行。

我们用asyncio的异步网络连接来获取sina、sohu和163的网站首页&#xff1a;

import asyncio&#64;asyncio.coroutinedef wget(host): print(&#39;wget %s ...&#39; % host) connect &#61; asyncio.open_connection(host, 80) reader, writer &#61; yield from connect header &#61; &#39;GET / HTTP/1.0Host: %s&#39; % host writer.write(header.encode(&#39;utf-8&#39;)) yield from writer.drain() while True: line &#61; yield from reader.readline() if line &#61;&#61; b&#39;&#39;: break print(&#39;%s header > %s &#39; % (host, line.decode(&#39;utf-8&#39;).rstrip())) writer.close()loop &#61; asyncio.get_event_loop()tasks &#61; [wget(host) for host in [&#39;www.sina.com.cn&#39;, &#39;www.baidu.com&#39;, &#39;www.163.com&#39;]]loop.run_until_complete(asyncio.wait(tasks))loop.close()

输出结果如下&#xff1a;

wget www.163.com ...wget www.sina.com.cn ...wget www.baidu.com ...www.163.com header > HTTP/1.0 302 Moved Temporarily www.163.com header > Server: Cdn Cache Server V2.0 www.163.com header > Date: Sat, 06 Jan 2018 14:14:58 GMT www.163.com header > Content-Length: 0 www.163.com header > Location: http://www.163.com/special/0077jt/error_isp.html www.163.com header > Connection: close www.sina.com.cn header > HTTP/1.1 200 OK www.sina.com.cn header > Server: nginx www.sina.com.cn header > Date: Sat, 06 Jan 2018 14:12:15 GMT www.sina.com.cn header > Content-Type: text/html www.sina.com.cn header > Content-Length: 605048 www.sina.com.cn header > Connection: close www.sina.com.cn header > Last-Modified: Sat, 06 Jan 2018 14:09:06 GMT www.sina.com.cn header > Vary: Accept-Encoding www.sina.com.cn header > Expires: Sat, 06 Jan 2018 14:13:12 GMT www.sina.com.cn header > Cache-Control: max-age&#61;60 www.sina.com.cn header > X-Powered-By: shci_v1.03 www.sina.com.cn header > Age: 3 www.sina.com.cn header > Via: http/1.1 cnc.beixian.ha2ts4.205 (ApacheTrafficServer/6.2.1 [cMsSf ]), http/1.1 gwbn.beijing.ha2ts4.23 (ApacheTrafficServer/6.2.1 [cHs f ]) www.sina.com.cn header > X-Via-Edge: 15152479356296c6422730904eedb7d91845d www.sina.com.cn header > X-Cache: HIT.23 www.sina.com.cn header > X-Via-CDN: f&#61;edge,s&#61;gwbn.beijing.ha2ts4.21.nb.sinaedge.com,c&#61;115.34.100.108;f&#61;Edge,s&#61;gwbn.beijing.ha2ts4.23,c&#61;219.238.4.21 www.baidu.com header > HTTP/1.1 200 OK www.baidu.com header > Date: Sat, 06 Jan 2018 14:12:15 GMT www.baidu.com header > Content-Type: text/html www.baidu.com header > Content-Length: 14613 www.baidu.com header > Last-Modified: Fri, 29 Dec 2017 03:29:00 GMT www.baidu.com header > Connection: Close www.baidu.com header > Vary: Accept-Encoding www.baidu.com header > Set-COOKIE: BAIDUID&#61;BEA2CAC2706F8386AAC50DEEC6287BD9:FG&#61;1; expires&#61;Thu, 31-Dec-37 23:55:55 GMT; max-age&#61;2147483647; path&#61;/; domain&#61;.baidu.com www.baidu.com header > Set-COOKIE: BIDUPSID&#61;BEA2CAC2706F8386AAC50DEEC6287BD9; expires&#61;Thu, 31-Dec-37 23:55:55 GMT; max-age&#61;2147483647; path&#61;/; domain&#61;.baidu.com www.baidu.com header > Set-COOKIE: PSTM&#61;1515247935; expires&#61;Thu, 31-Dec-37 23:55:55 GMT; max-age&#61;2147483647; path&#61;/; domain&#61;.baidu.com www.baidu.com header > P3P: CP&#61;" OTI DSP COR IVA OUR IND COM " www.baidu.com header > Server: BWS/1.1 www.baidu.com header > X-UA-Compatible: IE&#61;Edge,chrome&#61;1 www.baidu.com header > Pragma: no-cache www.baidu.com header > Cache-control: no-cache www.baidu.com header > Accept-Ranges: bytes

async/await

asyncio提供的&#64;asyncio.coroutine可以把一个generator标记为coroutine类型&#xff0c;然后在coroutine内部用yield from调用另一个coroutine实现异步操作。

为了简化并更好地标识异步IO&#xff0c;从Python 3.5开始引入了新的语法async和await&#xff0c;可以让coroutine的代码更简洁易读。

请注意&#xff0c;async和await是针对coroutine的新语法&#xff0c;要使用新的语法&#xff0c;只需要做两步简单的替换&#xff1a;

  • 把&#64;asyncio.coroutine替换为async&#xff1b;
  • 把yield from替换为await。

那么6部分的hello代码就可以这样替换了

# 把一个generator标记为协程类型&#64;asyncio.coroutinedef hello():"""定义一个生成器"""print(&#39;Hello world!&#39;)# 通过yield from调用另一个标记为协程的生成器r &#61; yield from asyncio.sleep(2)print(&#39;Hello again!&#39;)

替换为

async def hello():"""定义一个生成器"""print(&#39;Hello world!&#39;)# 通过await调用另一个标记为协程的生成器r &#61; await asyncio.sleep(2)print(&#39;Hello again!&#39;)

其它位置的代码均不变化。



推荐阅读
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
  • 本文介绍了PE文件结构中的导出表的解析方法,包括获取区段头表、遍历查找所在的区段等步骤。通过该方法可以准确地解析PE文件中的导出表信息。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • 本文介绍了NetCore WebAPI开发的探索过程,包括新建项目、运行接口获取数据、跨平台部署等。同时还提供了客户端访问代码示例,包括Post函数、服务器post地址、api参数等。详细讲解了部署模式选择、框架依赖和独立部署的区别,以及在Windows和Linux平台上的部署方法。 ... [详细]
  • Commit1ced2a7433ea8937a1b260ea65d708f32ca7c95eintroduceda+Clonetraitboundtom ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文介绍了C++中省略号类型和参数个数不确定函数参数的使用方法,并提供了一个范例。通过宏定义的方式,可以方便地处理不定参数的情况。文章中给出了具体的代码实现,并对代码进行了解释和说明。这对于需要处理不定参数的情况的程序员来说,是一个很有用的参考资料。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文介绍了在Python张量流中使用make_merged_spec()方法合并设备规格对象的方法和语法,以及参数和返回值的说明,并提供了一个示例代码。 ... [详细]
  • 使用圣杯布局模式实现网站首页的内容布局
    本文介绍了使用圣杯布局模式实现网站首页的内容布局的方法,包括HTML部分代码和实例。同时还提供了公司新闻、最新产品、关于我们、联系我们等页面的布局示例。商品展示区包括了车里子和农家生态土鸡蛋等产品的价格信息。 ... [详细]
  • 超级简单加解密工具的方案和功能
    本文介绍了一个超级简单的加解密工具的方案和功能。该工具可以读取文件头,并根据特定长度进行加密,加密后将加密部分写入源文件。同时,该工具也支持解密操作。加密和解密过程是可逆的。本文还提到了一些相关的功能和使用方法,并给出了Python代码示例。 ... [详细]
  • C#多线程解决界面卡死问题的完美解决方案
    当界面需要在程序运行中不断更新数据时,使用多线程可以解决界面卡死的问题。一个主线程创建界面,使用一个子线程执行程序并更新主界面,可以避免卡死现象。本文分享了一个例子,供大家参考。 ... [详细]
  • 基于移动平台的会展导游系统APP设计与实现的技术介绍与需求分析
    本文介绍了基于移动平台的会展导游系统APP的设计与实现过程。首先,对会展经济和移动互联网的概念进行了简要介绍,并阐述了将会展引入移动互联网的意义。接着,对基础技术进行了介绍,包括百度云开发环境、安卓系统和近场通讯技术。然后,进行了用户需求分析和系统需求分析,并提出了系统界面运行流畅和第三方授权等需求。最后,对系统的概要设计进行了详细阐述,包括系统前端设计和交互与原型设计。本文对基于移动平台的会展导游系统APP的设计与实现提供了技术支持和需求分析。 ... [详细]
author-avatar
小轩之音_438
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有