文章目录
PYTHON并发知识学习
课程内容:
- 多线程:t=threading.Thread(target=func_name,args=(xx,))、t.start()、t.join()、queue.Queue、Lock、ThreadPoolEexcutor()
- 多进程:CPU密集型,使用多线程的方式,反而会拖慢速度。因为线程运行时,资源获取与释放消耗时间。原理:多核CPU并行。
- 多协程:异步IO-asyncio,超级循环+IO复用。
第一节 python并发编程简介
并发编程,缩短运行时间。
哪些程序提速的方法?
单线程串行:CPU-IO-CPI-IO如此一步一步执行
多线程并发:CPU与IO可以并行,IO的读取不需要CPU参与,实现并发。原理上仍是单CPU处理。
多CPU并行:多核CPU的电脑,可实现多任务CPU-IO执行,是真正的并行执行进行加速。
多机器并行:在多CPU并行的基础上,多机器进行任务运行。
第二节 怎样选择多线程多进程多协程
第三节 python速度慢的罪魁祸首,全局解释器GIL
第四节 使用多线程,python爬虫被加速10倍
import requests
urls = [f"http://www.cnblogs.com/#p{page}"
for page in range(1, 50 + 1)]
def craw(url):
r = requests.get(url)
print(url, len(r.text))
craw(urls[0])
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import threading
import time
import note
def single_thread():
print("single_thread begin")
for url in note.urls:
note.craw(url)
print("single_thread end")
def muti_thread():
print("multi_thread begin")
threads = []
for url in note.urls:
threads.append(
threading.Thread(target=note.craw, args=(url,)) # url后添加逗号,说明是元组;不加逗号,说明是字符串。
)
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print("multi_thread end")
if __name__ == '__main__':
start = time.time()
single_thread()
end = time.time()
print("single_thread cost:", end - start, "seconds")
start = time.time()
muti_thread()
end = time.time()
print("multi_thread cost:", end - start, "seconds")
第五节 python实现生产者消费者爬虫
生产者生产的结果,会通过中间数据,传给消费者。
生产者将“输入数据”作为原料,消费者将“输出数据”进行输出。
这架构共有两个processor
processor①:获取待爬取的URL,进行网页的下载
下载好的内容放在网页队列中
processor②:消费者消费网页的数据,进行解析与数据存储。
queue.queue是阻塞函数,当队列已满,需要put填入数据。当队列没有元素,会等存在数据了再进行获取。
note.py文件
import requests
# 可以从html或xml文件中提取数据的python库
from bs4 import BeautifulSoup
urls = [f"http://www.cnblogs.com/#p{page}"
for page in range(1, 50 + 1)]
def craw(url):
r = requests.get(url)
return r.text
def parse(html):
# class="post-item-title"
soup = BeautifulSoup(html, "html.parser") # 获取html
links = soup.find_all("a", class_="post-item-title") # 文本解析
# 获取url和标题文本
return [(link["href"], link.get_text()) for link in links]
if __name__ == '__main__':
for result in parse(craw(urls[2])):
print(result)
02.producer_consumer_spider.py文件
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import queue
import note
# 为了查看中间过程,①解析完后,进行sleep;②添加日志--主要是打印线程的名字
import time
import random
import threading
# 开始爬行,此为生产者方法,会有输入数据过程。
def do_craw(url_queue: queue.Queue, html_queue: queue.Queue):
while True:
url = url_queue.get()
html = note.craw(url)
html_queue.put(html)
# 打印当前线程的名称、当前的url、队列的大小
print(threading.current_thread().name, f"craw{url}", "url_queue.size=", url_queue.qsize())
time.sleep(random.randint(1, 2))
def do_parse(html_queue: queue.Queue, fout):
while True:
html = html_queue.get()
results = note.parse(html)
for result in results:
fout.write(str(result) + "\n")
# 打印当前线程名称、总结果的大小、网站队列的大小。
print(threading.current_thread().name, f"results.size", len(results), "html_queue.size=", html_queue.qsize())
time.sleep(random.randint(1, 2))
if __name__ == '__main__':
"""
复杂的爬虫,可以分很多模块。
每个模块,都可以使用不同的线程组进行处理。
线程组之间,通过queue.Queue()进行交互。
通过queue.Queue(),主线程将数据扔进去。
url_queue数据传入生产者中后,产出html_queue。
消费者获得html_queue后,产出数据至fout文件。
"""
url_queue = queue.Queue()
html_queue = queue.Queue()
for url in note.urls:
url_queue.put(url)
for idx in range(3):
t = threading.Thread(target=do_craw, args=(url_queue, html_queue), name=f"craw{idx}")
t.start()
fout = open("02.data.txt", "w") # 打开数据文件,并设置为写入模式。
for idx in range(2):
t = threading.Thread(target=do_parse, args=(html_queue, fout), name=f"parse{idx}")
t.start()
第六节 python线程安全问题以及解决方案
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import threading
import time
lock=threading.Lock()
class Account:
def __init__(self,balance):
self.balance=balance
def draw(account,amount):
with lock:
if account.balance>=amount:
# 添加此语句,一定会使tb运行失败----在if执行完后,就出现线程的切换,使得tb余额为-600。
# 原因:sleep导致当前线程的阻塞,进行线程的切换。
time.sleep(0.1)
print(threading.current_thread().name,"取钱成功")
account.balance-=amount
print(threading.current_thread().name,"余额",account.balance)
else:
print(threading.current_thread().name,"取钱失败,余额不足")
if __name__ == '__main__':
account=Account(1000)
# 此处的name与threading.current_thread().name对应
ta=threading.Thread(name="ta",target=draw,args=(account,800))
tb=threading.Thread(name="tb",target=draw,args=(account,800))
ta.start()
tb.start()
第七节 python好用的线程池ThreadPoolExecutor
痛点:
新建线程系统需要分配资源,终止线程系统需要回收资源。如果可以重用线程,则可以减去新建/终止的开销。
解决办法:
线程池由2部分组成:线程池+队列。
- 线程池本身,里面是提前建好的线程。线程池中的资源会被重复地使用。
- 当新任务来了,不是直接加入线程池,而是加入任务队列。
流程:
- 线程池中已经创建完的线程,会挨个取出队列中的线程任务,进行依次的执行。
- 线程池中的任务执行完后,会接着取下一个任务进行执行。
- 当任务队列中没有新任务了,线程池不会销毁,等待下一个任务的到来。
总结:
通过任务队列与可重用的线程池,实现了线程池功能。
使用生产者线程组,实现map下的线程池写法。
使用消费者线程组,实现future下的两种线程池写法。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import concurrent.futures
import blog_spider
# craw
with concurrent.futures.ThreadPoolExecutor() as pool:
htmls = pool.map(blog_spider.craw, blog_spider.urls) # 注意,此处用的是urls,是一个列表
htmls = list(zip(blog_spider.urls, htmls)) # 列表中的每个元素都是元组,元组中有url和html
for url, html in htmls:
print(url, len(html))
print("craw over")
# parse
with concurrent.futures.ThreadPoolExecutor() as pool:
futures = {}
# 方法一:顺序输出
# 将future和url建立关系
for url, html in htmls:
future = pool.submit(blog_spider.parse, html)
futures[future] = url
# for future.url in futures.items():
# print(url,future.result())
# 方法二:不按顺序输出
for future in concurrent.futures.as_completed(futures):
url = futures[future]
print(url, future.result())
第八节 python使用线程池在web服务中实现加速
启动flask的方法:
flask默认写法:
app = flask.Flask(__name__)
def read_file():
time.sleep(0.1)
return "file result"
def read_db():
time.sleep(0.2)
return "db result"
def read_api():
time.sleep(0.3)
return "api result"
@app.route('/')
def index():
result_file = read_file()
result_db = read_db()
result_api = read_api()
return json.dumps({
"result_file": result_file,
"result_db": result_db,
"result_api": result_api,
})
if __name__ == '__main__':
app.run()
使用线程池加速后的结果:
对象获取:pool.submit(read_file)
结果获取:result_file.result()
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import json
import time
from concurrent.futures import ThreadPoolExecutor
import flask
pool = ThreadPoolExecutor()
app = flask.Flask(__name__)
def read_file():
time.sleep(0.1)
return "file result"
def read_db():
time.sleep(0.2)
return "db result"
def read_api():
time.sleep(0.3)
return "api result"
@app.route('/')
def index():
result_file = pool.submit(read_file)
result_db = pool.submit(read_db)
result_api = pool.submit(read_api)
return json.dumps({
"result_file": result_file.result(),
"result_db": result_db.result(),
"result_api": result_api.result(),
})
if __name__ == '__main__':
app.run()
几乎同时运行的话,就差不多300ms。总时间几乎为最长线程的时间。
第九节 使用多进程multiprocessing模块加速程序的运行
代码实现:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import math
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
PRIMES = [112272535095293] * 100
def is_prime(n):
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
sqrt_n = int(math.floor(math.sqrt(n)))
for i in range(3, sqrt_n + 1, 2):
if n % i == 0:
return False
return True
def single_thread():
for number in PRIMES:
is_prime(number)
def multi_thread():
with ThreadPoolExecutor() as pool:
pool.map(is_prime, PRIMES)
def multi_process():
with ProcessPoolExecutor() as pool:
pool.map(is_prime, PRIMES)
if __name__ == '__main__':
start = time.time()
single_thread()
end = time.time()
print("single_thread,cost:", end - start, "seconds")
start = time.time()
multi_thread()
end = time.time()
print("multi_thread,cost:", end - start, "seconds")
start = time.time()
multi_process()
end = time.time()
print("multi_process,cost:", end - start, "seconds")
输出结果:
single_thread,cost: 44.58890986442566 seconds
multi_thread,cost: 46.927555561065674 seconds
multi_process,cost: 8.359080076217651 seconds
第十节 python在Flask服务中使用多进程池加速程序运行
和第9节类似的运行情况,但进程池的执行中出现报错。原因:他们的环境之间都是相互完全隔离的。有一点限制,当定义这个pool时,它所依赖的这些函数必须都已经声明完了。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import flask
from concurrent.futures import ProcessPoolExecutor
import math
import json
process_pool = ProcessPoolExecutor()
app = flask.Flask(__name__)
def is_prime(n):
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
sqrt_n = int(math.floor(math.sqrt(n)))
for i in range(3, sqrt_n + 1, 2):
if n % i == 0:
return False
return True
@app.route("/is_prime/<numbers>")
# 传入逗号分隔的数字列表,进行批量调用
def api_is_prime(numbers):
number_list = [int(x) for x in numbers.split(",")]
results = process_pool.map(is_prime, number_list)
return json.dumps(dict(zip(number_list, results)))
if __name__ == '__main__':
app.run()
博主建议将进程池的声明放在app.run()前面。但是我执行后,没有太大的区别。
if __name__ == '__main__':
process_pool = ProcessPoolExecutor()
app.run()
第十一节 python异步IO实现并发爬虫
步骤:
1、asyncio:至尊循环;get_event_loop():while…True超级循环。
2、async def myfunc(url)中,async:说明这个函数是一个协程。
await:代表IO,IO执行完毕后,不进行阻塞,直接进入下一个程序的执行asycio.get_event_loop()
3、批量创建task任务:loop.create_task,参数是具体的协程函数。
4、对tasks任务,放到loop.run_until_complete进行事件的执行,并等待完成。
总结:
整体来说,这个异步爬行的框架也是单线程执行的。但是他会多个URL的并发爬虫同时进行。
注意一点,异步IO编程中,依赖的库要支持await,也就是说支持异步IO特性。否则单线程就不能并发执行了。
协程是异步IO执行的函数。协程与普通函数的区别:协程需要超级循环来调度。
汇总:
异步IO实际是超级循环+IO多路复用的结合。
IO多路复用:IO时,CPU可以干其他事情。即当前任务处于IO时,CPU处理下一任务计算。
超级循环:当所有的任务都按照IO多路复用后,再返回来处理第一个任务剩余CPU需计算的任务。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import asyncio
import aiohttp
import blog_spider
async def async_craw(url): # 定义一个异步爬虫的协程
print("craw url:",url)
async with aiohttp.ClientSession() as session: # 异步协程的对象
async with session.get(url) as resp:
result = await resp.text() # await不会一直等待,会切换到下一个url的爬取
print(f"craw url:{url},{len(result)}")
# 协程是异步IO中执行的函数。他与普通函数的区别:协程需要超级循环进行调度。
# 获取超级循环
loop=asyncio.get_event_loop()
# 定义一个tasks列表
tasks=[
loop.create_task(async_craw(url))
for url in blog_spider.urls]
import time
start=time.time()
loop.run_until_complete(asyncio.wait(tasks))
end=time.time()
print("use time seconds:",end-start)
# 单线程异步爬虫,大部分情况下,是快于多线程的。
# 所以多线程的爬取时的并发,需要进行多线程不同调度的切换。本身是耗费时间的。
# 但单线程异步爬虫,没有切换的开销。所以执行起来相对来说是更快的。
第十二节 在异步IO中使用信号量控制爬虫并发度
当线程完成一次对该semaphore对象的等待时,代表执行一次并发,所以计数值减一。
通过semephore控制并发度。当信号量满了后,会进入等待状态。防止爬虫将目标网站爬坏,超出他的处理能力。