一文讲透Python多线程,面试主管:我给你月薪 20K,来我公司上班

部署运行你感兴趣的模型镜像

本篇文章介绍了多线程的基本概念、Python多线程的使用方法、线程同步机制以及线程池的使用。

本章的思维导图如下:
在这里插入图片描述

1. 线程基本概念

线程是进程内的执行单元,共享进程的内存空间和资源,而进程是独立的资源分配单位。多线程的优势在于轻量级和高效的资源共享,适用于需要并发处理但资源隔离要求不高的场景。

Python多线程主要实现并发(交替执行任务),而非真正的并行(同时执行)。受限于全局解释器锁(GIL),CPU 密集型任务难以通过多线程加速,但 I/O 密集型任务(如网络请求、文件读写)能显著提升效率。

GIL 确保同一时刻仅有一个线程执行 Python 字节码,但线程在等待 I/O 操作时会释放 GIL,因此多线程在 I/O 密集型场景中仍能提升性能。

2. Python 多线程的使用方法

Python通过threading模块实现多线程编程,其核心类是threading.Thread,主要参数如下:

threading.Thread(
    target=None,   # 线程执行的函数
    args=(),       # 函数参数(元组形式)
    kwargs={},     # 函数关键字参数(字典形式)
    daemon=None    # 是否为守护线程(主线程退出时自动终止)
)

2.1 threading.Thread 创建线程

使用 threading.Thread 创建线程有两种方式,一种是直接使用 Thread 进行创建,另一种是通过继承 Thread 类自定义线程的创建。

2.1.1 直接使用 Thread 进行创建

import threading


def worker(name, num):
    print(f"Worker {num} is starting")
    ret = 0
    for i in range(num):
        ret += i
    print(f"worker {name} 完成从0到{num}的求和,结果为{ret}")
    
def main():
    threads = []
    for i in range(1, 5):
        t = threading.Thread(target=worker, args=(i, i*1000))
        t.start()
        threads.append(t)
    
    for t in threads:
        t.join()
        
if __name__ == "__main__":
    main()

输出结果:

worker 1000 is starting
worker 1 完成从01000的求和,结果为499500
Worker 2000 is starting
Worker 3000 is starting
worker 2 完成从02000的求和,结果为1999000
worker 3 完成从03000的求和,结果为4498500
Worker 4000 is starting
worker 4 完成从04000的求和,结果为7998000

2.1.2 通过继承 Thread 类自定义线程创建过程

有时候,我们需要自定义线程的创建过程。例如,让线程保存一下额外的信息,定制化线程类执行的代码。

下面写了一个多线程程序,用来计算从 1 到 100000 整数的和。当然,这个场景并不适合使用多线程计算。这里只是作为一个例子。

import threading
import time
import matplotlib.pyplot as plt

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

# 定义线程类
class SumThread(threading.Thread):
    def __init__(self, start, end):
        super().__init__()
        self.start_num = start
        self.end_num = end
        self.result = 0

    def run(self):
        # 使用等差数列求和公式优化计算
        n = self.end_num - self.start_num + 1
        self.result = n * (self.start_num + self.end_num) // 2

def calculate_sum(num_threads, total_numbers=1000000):
    total_sum = 0
    threads = []
    
    # 优化线程分配,确保每个线程处理大致相等的工作量
    chunk_size = (total_numbers + num_threads - 1) // num_threads  # 向上取整
    
    # 创建并启动线程
    for i in range(num_threads):
        start = i * chunk_size + 1
        end = min((i + 1) * chunk_size, total_numbers)
        if start > total_numbers:
            break
        thread = SumThread(start, end)
        threads.append(thread)
        thread.start()
    
    # 等待所有线程完成并汇总结果
    for thread in threads:
        thread.join()
        total_sum += thread.result
    
    return total_sum

def main():
    # 测试不同线程数的性能
    thread_counts = [1, 2, 4, 8, 16]
    execution_times = []
    results = []
    
    print("开始性能测试...")
    for num_threads in thread_counts:
        print(f"\n测试 {num_threads} 个线程...")
        start_time = time.perf_counter()  # 使用更高精度的时间计数器
        result = calculate_sum(num_threads)
        end_time = time.perf_counter()
        execution_time = end_time - start_time
        execution_times.append(execution_time)
        results.append(result)
        print(f"计算结果: {result}")
        print(f"计算耗时: {execution_time:.6f}秒")
    
    # 验证所有结果是否一致
    if len(set(results)) != 1:
        print("\n警告:不同线程数的计算结果不一致!")
    else:
        print("\n所有线程数的计算结果一致,验证通过!")
    
    # 绘制性能对比图
    plt.figure(figsize=(12, 8))
    plt.plot(thread_counts, execution_times, 'bo-', linewidth=2, markersize=8)
    plt.xlabel('线程数量', fontsize=14)
    plt.ylabel('执行时间(秒)', fontsize=14)
    plt.title('不同线程数下的计算性能对比\n(计算1到1000000的和)', fontsize=16)
    plt.grid(True, linestyle='--', alpha=0.7)
    plt.xticks(thread_counts)
    
    # 添加数据标签
    for i, (x, y) in enumerate(zip(thread_counts, execution_times)):
        plt.annotate(f'{y:.6f}s', (x, y), textcoords="offset points", 
                    xytext=(0, 10), ha='center')
    
    # 保存图像
    plt.savefig('thread_performance.png', dpi=300, bbox_inches='tight')
    plt.close()
    
    # 计算加速比
    base_time = execution_times[0]
    speedups = [base_time / max(t, 1e-6) for t in execution_times]  # 避免除零
    print("\n加速比(相对于单线程):")
    for threads, speedup in zip(thread_counts, speedups):
        print(f"{threads}线程: {speedup:.2f}x")

if __name__ == "__main__":
    main() 

绘制的结果:
在这里插入图片描述
可以看到,由于 GIL 的限制,多线程并不适合 CPU 密集型任务场景。

3. Python线程同步机制

这里介绍三种线程间同步机制:

  • 互斥锁
  • 信号量
  • 条件变量

3.1 互斥锁:threading.Lock()

当多个线程同时操作共享资源时,可能会造成数据不一致。例如,多个用户同时操作同一个银行账户的场景。如果一个用户在访问账户时,没有锁定账户,另一个用户访问账户时看到的数据可能会出现不一致。

  1. 创建锁:
    lock = threading.Lock()
  2. 检查锁是否满足
    if lock:

看一个例子:

# bank_account_thread.py
import threading
import time
import random

class BankAccount:
    def __init__(self, initial_balance=0):
        self.balance = initial_balance
        self.lock = threading.Lock()

    def deposit(self, amount):
        with self.lock:
            print(f"存款前余额: {self.balance}")
            self.balance += amount
            print(f"存款 {amount} 后余额: {self.balance}")

    def withdraw(self, amount):
        with self.lock:
            if self.balance >= amount:
                print(f"取款前余额: {self.balance}")
                self.balance -= amount
                print(f"取款 {amount} 后余额: {self.balance}")
            else:
                print(f"余额不足,无法取款 {amount}")

def customer_operations(account, operations):
    for operation in operations:
        if operation[0] == 'deposit':
            account.deposit(operation[1])
        else:
            account.withdraw(operation[1])
        time.sleep(random.uniform(0.1, 0.5))

def main():
    # 创建一个初始余额为1000的账户
    account = BankAccount(1000)
    
    # 定义两个客户的操作序列
    customer1_operations = [
        ('deposit', 200),
        ('withdraw', 300),
        ('deposit', 500)
    ]
    
    customer2_operations = [
        ('withdraw', 400),
        ('deposit', 100),
        ('withdraw', 200)
    ]
    
    # 创建两个线程
    thread1 = threading.Thread(target=customer_operations, args=(account, customer1_operations))
    thread2 = threading.Thread(target=customer_operations, args=(account, customer2_operations))
    
    # 启动线程
    thread1.start()
    thread2.start()
    
    # 等待所有线程完成
    thread1.join()
    thread2.join()
    
    print(f"\n最终账户余额: {account.balance}")

if __name__ == "__main__":
    main() 

对于 BankAccount 类内部维护了一个互斥锁,对账户进行存取之前必须先获得锁。

执行结果:

存款前余额: 1000
存款 200 后余额: 1200
取款前余额: 1200
取款 400 后余额: 800
存款前余额: 800
存款 100 后余额: 900
取款前余额: 900
取款 300 后余额: 600
取款前余额: 600
取款 200 后余额: 400
存款前余额: 400
存款 500 后余额: 900

最终账户余额: 900

3.2 信号量:threading.Semaphore()

信号量(Semaphore)​​ 是一种同步原语,用于控制对共享资源的并发访问数量。它维护一个内部计数器,线程通过调用 acquire() 和 release() 来增减计数器,从而限制同时访问资源的线程数。

  • 计数器规则:

    • 当线程调用 acquire() 时,计数器减 1;如果计数器为 0,线程阻塞,直到其他线程释放资源。
    • 当线程调用 release() 时,计数器加 1,唤醒等待的线程。
  • 与互斥锁的区别

    • 互斥锁​​:同一时间只允许一个线程访问资源(计数器最大为 1)。
    • 信号量​​:允许指定数量的线程同时访问资源(计数器可设为 N)。

信号量的常用API:

  • 创建 n 个资源的信号量:sem = threading.Semaphore(n)
  • 获取信号量:sem.acquire()
  • 增加信号量:sem.release()

常见使用场景:

  • 资源池管理
  • 生产者-消费者模型
  1. 资源池管理(如数据库连接池)
import sqlite3
import threading
import time
import random
from contextlib import contextmanager

class SQLiteConnectionPool:
    def __init__(self, db_path, pool_size=3):
        self.db_path = db_path
        self.pool_size = pool_size
        self.semaphore = threading.Semaphore(pool_size)
        self.lock = threading.Lock()
        self.id_counter = 0  # 添加ID计数器
    
    @contextmanager
    def get_connection(self):
        # 获取信号量
        self.semaphore.acquire()
        try:
            # 每个线程创建自己的连接
            conn = sqlite3.connect(self.db_path)
            yield conn
        except sqlite3.Error as e:
            print(f"数据库连接错误: {e}")
        finally:
            # 确保连接存在时关闭连接并释放信号量
            if 'conn' in locals() and conn:
                conn.close()
            self.semaphore.release()

    def get_next_id(self):
        with self.lock:
            self.id_counter += 1
            return self.id_counter

def create_table(conn):
    cursor = conn.cursor()
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY,
            name TEXT,
            age INTEGER
        )
    ''')
    conn.commit()

def insert_user(conn, user_id, name, age):
    cursor = conn.cursor()
    cursor.execute('INSERT INTO users (id, name, age) VALUES (?, ?, ?)', 
                  (user_id, name, age))
    conn.commit()

def worker(pool, worker_id):
    print(f"线程 {worker_id} 正在等待获取数据库连接...")
    
    with pool.get_connection() as conn:
        print(f"线程 {worker_id} 获得了数据库连接,开始工作...")
        
        # 模拟数据库操作
        work_time = random.randint(1, 3)
        time.sleep(work_time)
        
        # 获取唯一ID并插入数据
        user_id = pool.get_next_id()
        name = f"User_{worker_id}"
        age = random.randint(20, 50)
        
        insert_user(conn, user_id, name, age)
        print(f"线程 {worker_id} 插入了一条数据: ID={user_id}, Name={name}, Age={age}")
        
        print(f"线程 {worker_id} 完成了工作,耗时 {work_time} 秒")

def main():
    # 创建数据库连接池
    pool = SQLiteConnectionPool('test.db')
    
    # 初始化数据库表
    with pool.get_connection() as conn:
        create_table(conn)
    
    # 创建多个工作线程
    threads = []
    for i in range(10):
        t = threading.Thread(target=worker, args=(pool, i))
        threads.append(t)
        t.start()
    
    # 等待所有线程完成
    for t in threads:
        t.join()
    
    print("所有数据库操作都已完成!")

if __name__ == "__main__":
    main() 
  1. 生产者-消费者模型(控制消费速度)
import queue
import threading

# 任务队列和信号量
task_queue = queue.Queue()
semaphore = threading.Semaphore(2)  # 最多2个消费者同时工作

def producer():
    for i in range(5):
        task_queue.put(f"任务-{i}")
        print(f"生产任务: 任务-{i}")

def consumer():
    while True:
        semaphore.acquire()  # 获取信号量
        task = task_queue.get()
        print(f"消费任务: {task} (剩余信号量: {semaphore._value})")
        task_queue.task_done()
        semaphore.release()  # 释放信号量

# 启动1个生产者,3个消费者
threading.Thread(target=producer, daemon=True).start()
for _ in range(3):
    threading.Thread(target=consumer, daemon=True).start()
task_queue.join()  # 等待所有任务完成

3.3 条件变量:threading.Condition()

条件变量是一种线程同步机制,用于实现线程间的等待-通知机制。它允许线程在某个条件不满足时挂起(等待),并在条件满足时被其他线程唤醒。条件变量通常与互斥锁(Mutex)​结合使用,以确保共享数据的安全访问。

核心要素:

  • 等待(Wait)​​:线程检查条件是否满足,若不满足则释放互斥锁并进入等待状态。
  • 通知(Signal/Broadcast):其他线程修改条件后,通过signal(唤醒一个线程)或broadcast(唤醒所有线程)通知等待的线程。
  • 条件谓词​​:描述条件的布尔表达式(如“队列不为空”),线程被唤醒后需重新检查该条件以避免虚假唤醒。
  • ​​互斥锁(Mutex):保护共享数据,确保条件检查和修改的原子性。

常用API:

  • 创建条件变量:cond = threading.Condition()
  • 通知等待条件变量的线程:cond.notify()
  • 等待条件变量:cond.wait()

使用条件变量实现生产者-消费者模式。


import threading
import time
import random

class ProducerConsumer:
    def __init__(self, max_size=5):
        self.buffer = []
        self.max_size = max_size
        self.condition = threading.Condition()
        
    def producer(self):
        while True:
            with self.condition:
                # 如果缓冲区已满,等待
                while len(self.buffer) >= self.max_size:
                    print("缓冲区已满,生产者等待...")
                    self.condition.wait()
                
                # 生产一个随机数
                item = random.randint(1, 100)
                self.buffer.append(item)
                print(f"生产者生产了: {item}, 当前缓冲区: {self.buffer}")
                
                # 通知消费者
                self.condition.notify()
            
            # 模拟生产时间
            time.sleep(random.uniform(0.1, 0.5))
    
    def consumer(self):
        while True:
            with self.condition:
                # 如果缓冲区为空,等待
                while len(self.buffer) == 0:
                    print("缓冲区为空,消费者等待...")
                    self.condition.wait()
                
                # 消费一个项目
                item = self.buffer.pop(0)
                print(f"消费者消费了: {item}, 当前缓冲区: {self.buffer}")
                
                # 通知生产者
                self.condition.notify()
            
            # 模拟消费时间
            time.sleep(random.uniform(0.1, 0.5))

def main():
    pc = ProducerConsumer()
    
    # 创建生产者和消费者线程
    producer_thread = threading.Thread(target=pc.producer)
    consumer_thread = threading.Thread(target=pc.consumer)
    
    # 启动线程
    producer_thread.start()
    consumer_thread.start()
    
    # 等待线程结束(实际上会一直运行)
    producer_thread.join()
    consumer_thread.join()

if __name__ == "__main__":
    main() 

4. Python线程池的使用方法

Python的线程池(ThreadPoolExecutor)是一种高效管理线程的机制,通过预先创建并复用线程来减少频繁创建/销毁线程的开销,适用于I/O密集型任务​(如网络请求、文件读写)。

4.1 创建线程池

(1)创建线程池

from concurrent.futures import ThreadPoolExecutor

# 创建最大并发数为3的线程池
with ThreadPoolExecutor(max_workers=3) as executor:
    # 提交任务(submit返回Future对象)
    future = executor.submit(task_function, arg1, arg2)
    print(future.result())  # 阻塞获取结果

(2)批量提交任务

  • map()方法:简化批量任务提交,按参数顺序返回结果。
results = executor.map(task_function, [1, 2, 3])  # 返回生成器
for result in results:
    print(result)
  • as_completed():按任务完成顺序获取结果。
futures = [executor.submit(task_function, i) for i in range(5)]
for future in as_completed(futures):
    print(future.result())

4.2 使用线程池实现一个简单的爬虫程序


import concurrent.futures
import requests
from bs4 import BeautifulSoup
import time
import random
from urllib.parse import urlparse

class WebCrawler:
    def __init__(self, max_workers=5):
        self.max_workers = max_workers
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        }
    
    def fetch_url(self, url):
        try:
            # 添加随机延迟,避免请求过于频繁
            time.sleep(random.uniform(0.5, 2.0))
            
            response = requests.get(url, headers=self.headers, timeout=10)
            response.raise_for_status()
            
            # 解析网页内容
            soup = BeautifulSoup(response.text, 'html.parser')
            
            # 获取网页标题
            title = soup.title.string if soup.title else "无标题"
            
            # 获取所有链接
            links = [a.get('href') for a in soup.find_all('a', href=True)]
            
            # 获取网页文本内容(示例:获取前100个字符)
            text_content = soup.get_text()[:100] + "..."
            
            return {
                'url': url,
                'title': title,
                'links': links,
                'content': text_content
            }
        except Exception as e:
            print(f"爬取 {url} 时出错: {str(e)}")
            return None

    def crawl_urls(self, urls):
        results = []
        with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            # 提交所有URL到线程池
            # 通过字典推导式将所有URL提交到线程池,生成future_to_url 字典:
            # - key:Future对象(代表异步任务)
            # - value:对应的URL
            future_to_url = {executor.submit(self.fetch_url, url): url for url in urls}
            
            # 获取结果。使用 as_completed 迭代已完成的任务(按完成顺序,而非提交顺序)
            for future in concurrent.futures.as_completed(future_to_url):
                url = future_to_url[future]
                try:
                    result = future.result()
                    if result:
                        results.append(result)
                        print(f"成功爬取: {url}")
                except Exception as e:
                    print(f"处理 {url} 时出错: {str(e)}")
        
        return results

def main():
    # 示例URL列表
    urls = [
        'https://www.python.org',
        'https://www.github.com',
        'https://www.baidu.com',
        'https://www.zhihu.com'
    ]
    
    # 创建爬虫实例
    crawler = WebCrawler(max_workers=3)
    
    # 开始爬取
    print("开始爬取...")
    start_time = time.time()
    
    results = crawler.crawl_urls(urls)
    
    end_time = time.time()
    print(f"\n爬取完成!耗时: {end_time - start_time:.2f} 秒")
    
    # 打印结果
    print("\n爬取结果:")
    for result in results:
        print(f"\nURL: {result['url']}")
        print(f"标题: {result['title']}")
        print(f"内容预览: {result['content']}")
        print(f"找到 {len(result['links'])} 个链接")

def test():
    with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
        future = executor.submit(pow, 2, 4)
        print(future.result())

if __name__ == "__main__":
    # main() 
    test()

执行结果:

开始爬取...
成功爬取: https://www.baidu.com
成功爬取: https://www.zhihu.com
爬取 https://www.github.com 时出错: HTTPSConnectionPool(host='github.com', 
port=443): Max retries exceeded with url: / (Caused by 
ConnectTimeoutError(<urllib3.connection.HTTPSConnection object 
at 0x000001B769F86710>, 'Connection to github.com timed out. 
(connect timeout=10)'))

爬取完成!耗时: 13.04 秒

爬取结果:

URL: https://www.baidu.com
标题: 百度一下,你就知道
内容预览: 百度一下,你就知道
<style data-for="result" type="text/css" >
html{font-size:100px}html body{font-size:.14rem;...
找到 59 个链接

URL: https://www.zhihu.com
标题: 知乎 - 有问题,就会有答案
内容预览:
知乎 - 有问题,就会有答案打开知乎App在「我的页」
右上角打开扫一扫其他扫码方式:微信下载 知乎App
开通机构号无障碍模式验证码登录密码登录获取短信
验证码获取语音验证码登录/注册其他 方式登录未注册手...
找到 24 个链接

今天的文章就到这里了,你已经掌握了 Python 多线程和线程池的基本使用,相信你可以拿到月薪 20 K的 Python 开发工作了。

关注我,通过大量可执行代码持续分享关于后端开发、人工智能等领域的硬核知识。

您可能感兴趣的与本文相关的镜像

Python3.9

Python3.9

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值