Python 多线程开发基础

一、Python 多线程开发基础教程

1. 什么是多线程?为什么需要它?

  • 线程:一个程序的最小执行单位,多个线程可在同一进程中并发运行。
  • 多线程的好处
    • 提高程序响应速度(特别是 I/O 密集型任务)。
    • 并行处理多个任务。
    • 充分利用多核 CPU 资源。

2. Python 多线程的基础模块:threading

  • Python 的 threading 模块用于创建和管理线程。
基本代码示例:创建一个线程
import threading

def print_numbers():
    for i in range(5):
        print(f"Number: {i}")

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

print("主线程结束")

解释

  1. 创建线程时,target 指向需要在线程中执行的函数。
  2. start() 启动线程。
  3. join() 等待线程执行完毕后再继续主线程的操作。

3. 多线程常见问题与解决方法

问题 1:数据竞争

多个线程访问同一资源时可能会发生冲突。

示例:数据竞争问题
import threading
import time
counter = 0

def increase():
    global counter
    for _ in range(100000):
        current = counter;    # 使用这个数据, 假设是某数据库获取
        time.sleep(0.0000001) # 模拟有逻辑执行. 其他线程介入
        counter = current + 1 # 对原数据做操作
        

# 创建两个线程同时增加 counter
t1 = threading.Thread(target=increase)
t2 = threading.Thread(target=increase)

t1.start()
t2.start()
t1.join()
t2.join()

print(f"最终结果: {counter}")

输出
理论上结果应为 200000,但由于线程间竞争,结果可能出错。

解决:使用锁(Lock
import threading
import time

lock = threading.Lock()
counter = 0

def increase_with_lock():
    global counter
    for _ in range(100000):
        with lock:                # 锁住代码块
            current = counter;    # 使用这个数据, 假设是某数据库获取
            time.sleep(0.0000001) # 模拟有逻辑执行. 其他线程介入
            counter = current + 1 # 对原数据做操作
t1 = threading.Thread(target=increase_with_lock)
t2 = threading.Thread(target=increase_with_lock)

t1.start()
t2.start()
t1.join()
t2.join()

print(f"最终结果: {counter}")

4. 守护线程(Daemon Thread)

  • 守护线程会在主线程结束时自动停止,常用于后台任务。
import threading
import time

def background_task():
    while True:
        print("后台任务运行中...")
        time.sleep(2)

t = threading.Thread(target=background_task, daemon=True)
t.start()

time.sleep(5)
print("主线程结束")

解释
即使后台任务没有完成,当主线程结束时,它也会终止。


5. 多线程中的常见经验

  1. 避免频繁切换线程:频繁切换可能降低效率,建议将任务合理分配。
  2. 选择合适的任务类型:对于 CPU 密集型任务,多线程不一定优于多进程(如使用 multiprocessing 模块)。
  3. 善用锁和条件变量:合理使用锁避免死锁,使用 Condition 等同步机制协调线程工作。

6. 实例:下载多个网页内容

import threading
import requests

urls = [
    "https://www.baidu.com",
    "https://www.sina.com.cn",
    "https://www.qq.com",
]

def download_content(url):
    response = requests.get(url)
    print(f"{url} 的内容长度为: {len(response.text)}")

threads = []
for url in urls:
    t = threading.Thread(target=download_content, args=(url,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print("所有网页下载完成")

解释

  • 创建多个线程并行下载网页,提升下载速度。

二、 Python 多线程中的线程池 (concurrent.futures)

1. 为什么使用线程池?

  • 线程池可以重用线程,避免创建和销毁线程的开销。
  • 适用于需要并发处理大量短任务的场景,如IO读写、网络请求。
  • 自动管理线程的数量,提升程序的性能和资源使用效率。

2. 基础示例:线程池执行多个任务

Python 提供了 concurrent.futures.ThreadPoolExecutor,用于方便地创建和管理线程池。

from concurrent.futures import ThreadPoolExecutor
import time

def task(n):
    print(f"任务 {n} 开始")
    time.sleep(2)  # 模拟耗时任务
    print(f"任务 {n} 完成")
    return n * 2

# 创建线程池,并提交多个任务
with ThreadPoolExecutor(max_workers=3) as executor:
    futures = [executor.submit(task, i) for i in range(5)]

# 获取每个任务的结果
for future in futures:
    print(f"结果: {future.result()}")

解释

  1. max_workers=3:线程池中最多同时运行 3 个线程。
  2. executor.submit(task, i):向线程池提交任务并执行。
  3. future.result():等待任务完成,并获取结果。

输出示例

任务 0 开始
任务 1 开始
任务 2 开始
任务 0 完成
任务 3 开始
任务 1 完成
任务 4 开始
任务 2 完成
任务 3 完成
任务 4 完成
结果: 0
结果: 2
结果: 4
结果: 6
结果: 8

3. 实战:使用线程池并行下载网页

from concurrent.futures import ThreadPoolExecutor
import requests

urls = [
    "https://www.baidu.com",
    "https://www.sina.com.cn",
    "https://www.qq.com",
]

def fetch_content(url):
    response = requests.get(url)
    return f"{url} 的内容长度为: {len(response.text)}"

# 创建线程池并行下载
with ThreadPoolExecutor(max_workers=5) as executor:
    results = executor.map(fetch_content, urls)

# 打印结果
for result in results:
    print(result)

解释

  1. executor.map():将任务分配给线程池中的线程并行执行。
  2. 网络请求任务非常适合多线程处理,因为 I/O 操作不会受到 GIL 的限制。

4. 使用线程池提高性能的编程经验

  1. 适合 I/O 密集型任务:如文件操作、网络请求等,多线程可以显著提高性能。
  2. 避免过多线程:过多的线程会导致上下文切换开销,建议线程数控制在 CPU 核心数的 1-2 倍以内。
  3. 捕获异常:可以在 future.result() 时捕获任务执行过程中的异常,确保程序稳定。
with ThreadPoolExecutor(max_workers=3) as executor:
    futures = [executor.submit(task, i) for i in range(5)]
    
    for future in futures:
        try:
            print(f"结果: {future.result()}")
        except Exception as e:
            print(f"任务执行出错: {e}")

三、Python GIL(Global Interpreter Lock)对多线程的影响

1. 什么是 GIL?
  • GIL(Global Interpreter Lock,全局解释器锁)是CPython(Python 最常用的实现)中的一个机制。
  • 它确保同一时间只有一个线程可以执行 Python 字节码,即便在多核 CPU 上运行多线程程序,线程间也必须轮流执行。
2. GIL 的设计初衷
  • GIL 的引入主要是为了简化内存管理。Python 的内存管理器(如垃圾回收)不是线程安全的,因此通过 GIL 限制并发访问,可以减少编程复杂度并避免线程间的数据竞争问题。

3. GIL 对多线程的影响

  1. 多线程在 CPU 密集型任务中的性能受限

    • 多线程程序即使运行在多核 CPU上,由于 GIL 的存在,同一时间只能有一个线程在执行
    • 这会导致 CPU 密集型任务(如大规模计算)的多线程性能远低于预期。
  2. I/O 密集型任务的优势

    • GIL 在执行 I/O 操作(如网络请求、文件读写)时,会暂时释放,让其他线程可以运行。因此,在I/O 密集型任务中,多线程仍然能提高性能。

4. 示例:GIL 对 CPU 密集型与 I/O 密集型任务的影响

4.1 CPU 密集型任务示例
import threading
import time

def cpu_task():
    total = 0
    for i in range(10**7):
        total += i

# 使用多线程执行 CPU 密集型任务
start = time.time()
# 创建 2 个线程做 “+” 操作
threads = [threading.Thread(target=cpu_task) for _ in range(2)]

for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

print(f"多线程耗时: {time.time() - start:.2f} 秒")

输出
即使有多个线程,CPU 密集型任务也不会快太多,因为 GIL 限制了并行性。


4.2 I/O 密集型任务示例
import threading
import time

def io_task():
    time.sleep(2)  # 模拟 I/O 操作

# 使用多线程执行 I/O 密集型任务
start = time.time()
# 快速创建5个线程, 模拟执行IO传输的调用
threads = [threading.Thread(target=io_task) for _ in range(5)]

for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

print(f"多线程耗时: {time.time() - start:.2f} 秒")

输出
由于 GIL 会在 time.sleep() 等 I/O 操作时释放,因此多个线程可以并发运行,程序的总耗时会缩短。


5. 如何规避 GIL 的限制?

多进程替代多线程:
使用 multiprocessing 模块创建多个进程,每个进程有独立的 GIL,可以充分利用多核 CPU。

from multiprocessing import Process
import time

def cpu_task():
    total = 0
    for i in range(10**7):
        total += i
    
# 多进程创建的代码 需要 if __name__ == '__main__':块来保护代码
if __name__ == "__main__":

   # 创建多个进程
    processes = [Process(target=cpu_task) for _ in range(2)]
    start = time.time()
    for p in processes:
        p.start()
    for p in processes:
        p.join()

    print(f"多线程耗时: {time.time() - start:.2f}") # 性能上优于上面的同样的线程CPU密集型代码效率

总结

每日学学又记记、明天单车变摩托 !

你可能感兴趣的:(python,1024程序员节,python)