面试题答案
一键面试资源开销
- 线程:线程的创建、销毁和切换开销较大。每个线程都需要占用一定的内存空间来维护其栈空间,一般情况下,线程栈的大小在数MB级别。
- 协程:协程的创建和切换开销极小。协程是在用户态下进行调度,不需要像线程那样进行上下文切换,其栈空间也相对较小,通常只有几KB。
调度方式
- 线程:由操作系统内核进行调度。操作系统根据一定的调度算法(如时间片轮转等),在多个线程之间分配CPU时间。线程的调度是抢占式的,即当一个线程的时间片用完或者有更高优先级的线程出现时,当前线程会被暂停执行。
- 协程:由程序员在代码中通过yield、await等关键字手动控制调度。协程的调度是非抢占式的,只有当一个协程主动让出控制权时,其他协程才能获得执行机会。
适用场景
- 线程:适用于I/O密集型任务,但在CPU密集型任务中,由于线程的频繁切换会带来较大开销,性能反而会下降。例如,在进行网络请求、文件读写等I/O操作时,线程可以在等待I/O完成的过程中让出CPU,让其他线程有机会执行。
- 协程:特别适用于高并发、I/O密集型任务。由于其开销小、调度灵活的特点,在处理大量并发连接(如网络爬虫、Web服务器等)时,能够显著提高程序的性能和效率。
协程更合适的Python异步编程场景举例
- 网络爬虫:在爬取大量网页时,需要频繁进行网络请求,这是典型的I/O密集型任务。使用协程可以在等待网络响应时,快速切换到其他请求,大大提高爬取效率。例如使用
aiohttp
库进行异步网络请求:
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
urls = ['http://example.com', 'http://example.org', 'http://example.net']
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
results = await asyncio.gather(*tasks)
print(results)
if __name__ == '__main__':
asyncio.run(main())
- Web服务器:处理大量客户端连接时,每个连接的处理过程可能包含I/O操作(如读取请求数据、写入响应数据等)。使用协程可以轻松应对高并发连接,提高服务器的吞吐量。例如
Tornado
框架,它基于协程实现了高性能的Web服务器。
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
async def get(self):
await asyncio.sleep(1) # 模拟I/O操作
self.write("Hello, world")
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
在上述例子中,协程能够在I/O操作时让出执行权,使得其他任务可以继续执行,从而在高并发场景下提高程序的整体性能,而线程由于其调度方式和资源开销的限制,在这种场景下不如协程高效。