Python中的并发编程

58 篇文章 1 订阅

        导读:Python提供了多种机制来支持并发编程,这些机制包括线程、进程、协程以及异步编程模型,这篇文章让我们一一讨论。

目录

线程与进程

基本概念

进程(Process)

线程(Thread)

创建一个进程和线程

使用multiprocessing模块创建进程

使用threading模块创建线程

协程与异步编程

基本概念

协程

异步编程

实现异步操作

使用asyncio进行异步

异步I/O操作

并发难题

数据同步问题

解决策略

demo

死锁

解决策略

活锁和资源饥饿

解决策略

上下文切换的开销

解决策略

实际案例

网络服务器

案例描述

代码示例

数据处理

案例描述

代码示例

Web爬虫

案例描述

代码示例


线程与进程

基本概念

进程(Process)

  • 进程是操作系统分配资源和调度的基本单位。
  • 每个进程拥有自己的内存空间、数据栈以及其他用于跟踪执行的辅助数据。
  • 进程间通信(IPC)较为复杂,常见的方式包括管道、信号、套接字等。

线程(Thread)

  • 线程是进程内的执行单位,一个进程可以有多个线程,它们共享相同的内存空间。
  • 线程之间的通信和数据共享更为容易,但也更容易出现数据同步的问题。
  • Python中的线程受全局解释器锁(GIL)的影响,这意味着同一时刻只有一个线程可以执行Python字节码。尽管如此,线程仍然适用于I/O密集型任务。

创建一个进程和线程

使用multiprocessing模块创建进程

import multiprocessing
import os


def square_numbers():
    for i in range(5):
        print(f" {i} 的平方 {i * i}")
        print(f"进程 ID: {os.getpid()}")


if __name__ == '__main__':
    # 创建进程
    process = multiprocessing.Process(target=square_numbers)
    # 启动进程
    process.start()
    # 等待进程完成
    process.join()
    print("进程结束")

可以看到,整个任务期间,我们一直是使用创建的进程来执行任务。

使用threading模块创建线程

创建线程的方法类似:

import threading
import time


def print_numbers():
    for i in range(1, 6):
        time.sleep(1)
        print(f"======》 {i}")


# 创建线程
thread = threading.Thread(target=print_numbers)
# 启动线程
thread.start()
# 等待线程完成
thread.join()

print("=======结束")

至此为止,我们已经可以创建单独的进程和线程,并使用它来执行一些我们封装好的函数。但目前看来使用线程或者进程好像没啥用是吧,别急,你现在可以把他当做一个知识点,一个没什么用但是你掌握了的知识点,后面会有大用,在继续之前 我们来补充一点注意事项:

  1. 线程共享内存空间,而进程拥有独立的内存。这意味着线程之间的通信更简单,但在多线程访问共享资源时需要特别注意同步问题;
  2. 由于GIL的存在,Python的线程并不能实现真正的并行,但它们适用于I/O密集型任务;
  3. 一个线程崩溃可能会影响整个进程的稳定性。进程间相互独立,一个进程的崩溃不会直接影响其他进程

协程与异步编程

基本概念

协程

  • 协程是一种可以暂停和恢复执行的函数,是异步编程的核心。
  • 在Python中,协程是通过async def关键字定义的。
  • 使用await关键字暂停协程的执行,直到等待的操作完成。

异步编程

  • 异步编程是一种编程范式,用于非阻塞性操作,特别是I/O操作。
  • 它允许程序在等待某些操作完成时继续执行其他代码。
  • Python的asyncio库是实现异步编程的关键。

实现异步操作

话不多说,上代码:

使用asyncio进行异步

import asyncio

async def count():
    print("=====1======")
    await asyncio.sleep(1)
    print("======2=======")

async def main():
    await asyncio.gather(count(), count(), count())

asyncio.run(main())

输出结果:

分析:使用asyncio库来运行三个异步协程。在打印完第一次的结果后,睡了一秒,但因为同时有三个协程在工作,所以输出了三个,一秒后,又是三个2打印了出来。

异步I/O操作

异步爬虫必备:aiohttp

import aiohttp
import asyncio

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        html = await fetch(session, 'http://www.baidu.com')
        print(html)

asyncio.run(main())

咳咳,爬虫虽好,也不要给对方太大压力哦

好了 分析一下它的优势:

  1. 非阻塞性,异步编程允许程序在等待操作完成时继续执行,提高了程序的响应性和效率。

  2. 资源利用,对于I/O密集型任务,异步编程可以帮助更好地利用系统资源,避免因等待I/O操作而闲置。

  3. 简化复杂性,虽然异步编程在一开始可能看起来复杂,但它可以帮助简化某些类型的并发代码。

注意

  • 异步编程需要一种不同的思维方式,理解asyncawait关键字是关键。
  • 并不是所有的库都支持异步操作。在选择库时,需要注意是否存在异步支持。
  • 错误处理在异步编程中同样重要,需要考虑如何处理异步任务中的异常。

并发难题

数据同步问题

当多个线程或进程尝试同时访问和修改共享数据时,可能会导致数据不一致和不可预测。

解决策略

  • 使用互斥锁(Mutex)或其他同步机制(如信号量、临界区)来确保在任一时刻只有一个线程可以访问共享资源。
  • 在Python中,可以使用threading.Lock来实现线程安全的操作。

demo

import threading

class Counter:
    def __init__(self):
        self.value = 0
        self.lock = threading.Lock()

    def increment(self):
        with self.lock:
            self.value += 1

死锁

  • 死锁是指两个或多个线程相互等待对方释放资源,从而导致它们都停止执行的情况。
  • 死锁通常发生在多个线程需要相同的锁,但以不同的顺序获取它们时。

解决策略

  • 避免多个锁的不必要使用。
  • 保证所有线程以相同的顺序获取锁。
  • 使用超时机制强制释放锁,或检测并打破死锁。

活锁和资源饥饿

  • 活锁是指线程虽然没有被阻塞(仍在运行),但无法继续执行有用的工作。
  • 资源饥饿发生在某些线程无法获取必要的资源,而其他线程则持续运行。

解决策略

  • 重新设计算法和工作流,以确保所有线程都有机会执行。
  • 实现合理的资源分配策略和优先级制度。

上下文切换的开销

  • 在并发环境中,操作系统经常需要在不同的线程或进程之间进行上下文切换。
  • 这些切换可能导致显著的性能开销。

解决策略

  • 优化线程数量,避免创建过多的线程。
  • 使用线程池来管理线程,减少创建和销毁线程的次数。

实际案例

所有案例不提供业务逻辑部分,只是讨论并发操作。

网络服务器

案例描述

  • 网络服务器常常需要同时处理成千上万的客户端请求。
  • 使用多线程或异步编程可以有效地提高服务器的响应能力和吞吐量。

代码示例

import asyncio
from aiohttp import web

async def handle(request):
    return web.Response(text="Hello, world")

app = web.Application()
app.router.add_get('/', handle)

web.run_app(app)

数据处理

案例描述

  • 在数据分析和数据科学中,经常需要处理大量数据集。
  • 并发编程(特别是多进程处理)可以在这些任务中大幅提高处理速度。

代码示例

from multiprocessing import Pool

def process_data(data):
    # 数据处理逻辑
    return result

if __name__ == '__main__':
    pool = Pool(processes=4)
    data_to_process = [data1, data2, data3, data4]
    results = pool.map(process_data, data_to_process)

Web爬虫

案例描述

  • Web爬虫需要从多个网页上快速有效地收集数据。
  • 使用多线程或异步编程可以同时发送多个网络请求,显著提高数据收集的效率。

代码示例

(感谢 baidu 提供的友情支持  -狗头) 一般情况不建议大家高并发爬取一个网站,会给对方网站造成很大压力,不要让网站成为网战。

import asyncio
import aiohttp

async def fetch_page(url, session):
    async with session.get(url) as response:
        return await response.text()

async def main(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_page(url, session) for url in urls]
        return await asyncio.gather(*tasks)

urls = ["http://www.baidu.com/a", "http://www.baidu.com/b", "http://www.baidu.com/c"]
pages = asyncio.run(main(urls))

-------------

觉得不错 点个赞吧~

  • 29
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

昊昊该干饭了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值