面试题答案
一键面试多线程适用场景
- I/O 密集型任务:如网络请求、文件读写等,线程在等待 I/O 操作完成时会释放 GIL(全局解释器锁),其他线程可以利用这段时间执行,提高 CPU 利用率。
- 需要并发执行多个相对独立任务,且任务间需要共享数据:多线程能方便地共享进程内的资源。
异步编程适用场景
- 高并发 I/O 密集型场景:特别是在有大量 I/O 操作且操作之间不需要太多同步的情况下,异步编程可以显著提高效率,比如处理大量网络请求。
- 对资源占用敏感:异步编程不需要创建大量线程,减少了内存等资源开销。
网络爬虫结合思路
- 使用多线程管理异步任务:主线程创建多个工作线程,每个工作线程负责管理一组异步的网络请求任务。这样可以利用多线程处理不同批次的网页抓取,而每个线程内使用异步编程提高单个线程内的请求效率。
- 异步请求网页:在每个工作线程内,使用异步库(如
aiohttp
)发送网络请求获取网页内容,避免阻塞线程。
关键代码片段
import asyncio
import aiohttp
import threading
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def fetch_all(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
results = await asyncio.gather(*tasks)
return results
def worker(urls):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
htmls = loop.run_until_complete(fetch_all(urls))
print(htmls)
finally:
loop.close()
if __name__ == '__main__':
all_urls = ["http://example.com", "http://example.org", "http://example.net"]
num_threads = 2
sub_lists = [all_urls[i::num_threads] for i in range(num_threads)]
threads = []
for sub_list in sub_lists:
t = threading.Thread(target=worker, args=(sub_list,))
threads.append(t)
t.start()
for t in threads:
t.join()
在上述代码中,fetch
函数是一个异步函数,负责单个网页的抓取。fetch_all
函数用于批量处理多个 URL 的抓取。worker
函数在每个线程内创建一个新的事件循环来运行异步任务。在 main
部分,将 URL 列表分割成多个子列表,为每个子列表创建一个线程来执行 worker
函数,从而结合多线程和异步编程实现网页抓取。