Python 多线程编程的十个关键概念

2024年 5月 21日 70.1k 0

今天我们要一起探索的是Python编程中的一个超炫酷领域——多线程!想象一下,你的程序能像超人一样同时处理多个任务,是不是很激动人心?让我们以轻松愉快的方式,一步步揭开它的神秘面纱。

Python 多线程编程的十个关键概念-1

1. 线程的初步认知

想象你是个厨房大厨,一边炒菜一边洗菜,这就是多线程的日常。在Python里,threading模块就是我们的厨房神器。

import threading

def cook():  # 炒菜线程
    print("炒菜中...")
    
def wash():  # 洗菜线程
    print("洗菜中...")

# 创建线程对象
thread1 = threading.Thread(target=cook)
thread2 = threading.Thread(target=wash)

# 启动线程
thread1.start()
thread2.start()

# 等待所有线程完成
thread1.join()
thread2.join()

print("饭做好啦!")

这段代码中,Thread类用来创建线程,target参数指定线程要执行的函数。start()让线程开始执行,而join()确保主线程等待这些小线程们完成它们的任务。

2. 线程同步:避免厨房混乱

在多线程世界,如果两个线程同时操作同一资源(比如共享食材),就可能出乱子。这时就需要“锁”来帮忙了,Python里的锁叫Lock。

import threading

shared_resource = 0
lock = threading.Lock()

def increase(): 
    global shared_resource
    lock.acquire()  # 上锁,防止同时访问
    shared_resource += 1
    lock.release()  # 解锁,释放控制权

threads = [threading.Thread(target=increase) for _ in range(100)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print("共享资源的最终值:", shared_resource)

每次访问共享资源前,先acquire()上锁,操作完后release()解锁,这样就避免了数据混乱。

3. 死锁:可怕的厨房僵局

但锁用不好也会出问题,就像两个厨师互相等待对方手中的锅,形成了死锁。要小心设计,避免循环等待。

4. 线程池:高效管理厨房帮手

想象一下,如果你每次炒菜都要新雇一个厨师,那得多浪费?线程池(ThreadPoolExecutor)就是解决这个问题的神器,它预先创建好一些线程,重复利用。

from concurrent.futures import ThreadPoolExecutor

def task(n):
    print(f"执行任务{n}")

with ThreadPoolExecutor(max_workers=5) as executor:
    executor.map(task, range(1, 6))

这里,ThreadPoolExecutor创建了一个最多有5个线程的池,map()函数并行执行任务列表中的每个任务。

5. 守护线程:厨房的清洁工

守护线程就像厨房的清洁工,在所有其他线程完成后默默清理。通过setDaemon(True)设置线程为守护线程。

def cleaner():
    while True:  # 假设这是一个无限循环,清理任务
        print("打扫厨房...")
        if not other_threads_running():  # 假定函数检查其他线程是否还在运行
            break

clean_thread = threading.Thread(target=cleaner)
clean_thread.setDaemon(True)
clean_thread.start()

# 其他线程的代码...

print("厨房关闭,清洁完成。")

6. 线程优先级:谁先炒谁先洗

虽然Python标准库没有直接提供线程优先级的功能,但可以通过队列等间接实现。不过,大多数情况下,Python的线程调度是公平的,不需要担心。

7. 全局解释器锁(GIL)

Python的GIL是一个让人又爱又恨的东西,它保证了任何时刻只有一个线程在执行Python字节码,这对多核CPU来说不是个好消息。但在I/O密集型任务中,GIL的影响没那么大。

8. 线程局部存储:我的调料我做主

不同线程需要不同的“调料”怎么办?threading.local()来帮忙,它提供了线程本地的存储空间。

import threading

local_data = threading.local()

def set_data():
    local_data.value = "这是我的调料"

def get_data():
    print(local_data.value)

t1 = threading.Thread(target=set_data)
t2 = threading.Thread(target=get_data)

t1.start()
t2.start()

t1.join()
t2.join()

在这里,每个线程都有自己的local_data,互不影响。

9. 线程异常处理:防患于未然

线程中的异常不会自动传递到主线程,需要用try-except捕获处理。

def risky_task():
    raise ValueError("出错了!")

try:
    t = threading.Thread(target=risky_task)
    t.start()
    t.join()
except ValueError as e:
    print(f"捕获到异常: {e}")

确保即使线程出错,程序也不会突然崩溃。

10. 实战演练:多线程下载

最后,来点实战吧,比如多线程下载图片,体验速度的提升。

import requests
import threading
from queue import Queue

def download_image(url):
    response = requests.get(url)
    with open(f"image_{url[-4:]}", 'wb') as f:
        f.write(response.content)
    print(f"下载完成:{url}")

image_urls = ["http://example.com/image1.jpg", "http://example.com/image2.jpg"]  # 假设的URL
queue = Queue()
threads = []

for url in image_urls:
    queue.put(url)

def worker():
    while not queue.empty():
        url = queue.get()
        download_image(url)
        queue.task_done()

for _ in range(3):  # 启动3个下载线程
    t = threading.Thread(target=worker)
    t.start()
    threads.append(t)

# 等待所有下载任务完成
for t in threads:
    t.join()

print("所有图片下载完成!")

通过队列分配任务给多个线程,实现了并行下载,大大提高了效率。

好啦,今天的探险就到这里!希望你已经对Python多线程有了更深入的理解。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论