第八章:多线程与多任务——让程序“分身有术”

多线程是爬虫性能优化的核心手段之一,尤其适合处理高延迟网络请求大规模数据抓取。运用好多线程可以让我们很多程序组成和运行中事半功倍。

目标:学会用多线程让程序“一心多用”,从“单线程搬砖”到“多线程开挂”!
学习目标:掌握线程原理、共享变量与互斥锁,从此告别“程序卡死”的尴尬!


8.1 线程是什么?为什么程序员需要“分身术”?

核心概念:

  • 线程:程序执行的最小单位,可以理解为“轻量级进程”(比如同时边听歌边写代码)
  • 多线程优势
    • CPU利用率高:避免等待I/O时“空转”(如下载文件时同时处理其他任务)
    • 响应速度快:主线程卡顿时,子线程仍能运行(比如游戏主循环与音效分离)

举个栗子🌰:

 

python

import threading

def dance():
    print("线程1:正在跳广场舞!")

def sing():
    print("线程2:正在唱《孤勇者》!")

# 创建两个线程并启动
t1 = threading.Thread(target=dance)
t2 = threading.Thread(target=sing)
t1.start()
t2.start()

多线程在爬虫中的的核心作用

  1. 并发请求处理

    • 同时发起多个HTTP请求,减少等待时间(如同时爬取100个页面,单线程需100秒,10线程仅需10秒)。
    • 示例:处理分页数据时,同时下载多个页面。
  2. 资源利用率优化

    • 充分利用CPU多核特性(但Python因GIL限制,多线程更适合I/O密集型任务)。
    • 避免因单线程阻塞(如等待响应)导致资源闲置。
  3. 提升响应速度

    • 异步处理耗时操作(如文件写入、数据解析),避免主线程被阻塞。

8.2 共享全局变量:线程间的“爱情公寓”

问题场景:

 

python

import threading

# 全局变量(公共财产)
counter = 0

def add_counter():
    global counter
    for _ in range(100000):
        counter += 1

# 启动两个线程同时修改counter
t1 = threading.Thread(target=add_counter)
t2 = threading.Thread(target=add_counter)
t1.start()
t2.start()
t1.join()
t2.join()

print("最终结果:", counter)  # 可能输出小于200000的值!

问题原因:

  • 竞态条件:多个线程同时读写同一变量,导致数据不一致(就像两个人同时往存钱罐里塞硬币,结果数错了)

8.3 互斥锁:线程间的“排队叫号机”

原理与用法:

 

python

import threading

counter = 0
lock = threading.Lock()  # 创建锁对象

def safe_add():
    global counter
    for _ in range(100000):
        lock.acquire()  # 抢锁(如果锁被占用则阻塞)
        counter += 1
        lock.release()  # 释放锁

t1 = threading.Thread(target=safe_add)
t2 = threading.Thread(target=safe_add)
t1.start()
t2.start()
t1.join()
t2.join()

print("安全结果:", counter)  # 输出200000

锁的三种状态:

状态说明比喻
未锁定锁可以被任何线程获取公共厕所空闲中
锁定锁已被某个线程持有有人正在使用厕所
超时等待锁超时(需设置timeout参数)等了10分钟厕所没人出来

8.4 实战项目:多线程爬虫(带锁保护)

需求:

  1. 多线程抓取网页标题
  2. 用锁保护共享的存储列表
  3. 统计抓取进度

代码实现:

 

python

import threading
import requests
from bs4 import BeautifulSoup

urls = [
    "https://example.com/page1",
    "https://example.com/page2",
    # ... 更多URL
]
titles = []
lock = threading.Lock()
progress = 0

def fetch_url(url):
    global progress
    try:
        response = requests.get(url)
        soup = BeautifulSoup(response.text, 'html.parser')
        title = soup.title.string
        
        # 用锁保护共享列表
        with lock:
            titles.append((url, title))
        
        # 更新进度(加锁防止打印错乱)
        with lock:
            progress += 1
            print(f"进度:{progress}/{len(urls)}")
    except Exception as e:
        print(f"抓取失败:{url} → {str(e)}")

# 创建并启动线程
threads = []
for url in urls:
    t = threading.Thread(target=fetch_url, args=(url,))
    t.start()
    threads.append(t)

# 等待所有线程完成
for t in threads:
    t.join()

print("抓取完成!共获取", len(titles), "条数据")

8.5 GIL(全局解释器锁):Python多线程的“紧箍咒”

关键点:

  • GIL的作用:同一时刻仅允许一个线程执行Python字节码(导致CPU密集型任务无法真正并行)
  • 适用场景
    • I/O密集型:网络请求、文件读写(多线程有效)
    • CPU密集型:数学计算(建议用多进程multiprocessing模块)

代码验证:

 

python

import threading

def count_up():
    global count
    for _ in range(10000000):
        count += 1

count = 0

# 多线程执行(结果可能小于20000000)
t1 = threading.Thread(target=count_up)
t2 = threading.Thread(target=count_up)
t1.start()
t2.start()
t1.join()
t2.join()
print("多线程结果:", count)  # 可能输出19999999

# 多进程执行(结果准确)
from multiprocessing import Process
count = 0

def count_up_mp():
    global count
    for _ in range(10000000):
        count += 1

p1 = Process(target=count_up_mp)
p2 = Process(target=count_up_mp)
p1.start()
p2.start()
p1.join()
p2.join()
print("多进程结果:", count)  # 输出20000000

8.6 避坑指南:线程的“常见翻车现场”

  1. 死锁:多个线程互相等待对方释放锁(比如线程A拿锁1等锁2,线程B拿锁2等锁1)
    • 解决:按固定顺序获取锁,或设置超时时间(lock.acquire(timeout=5)
  2. 资源竞争:过度使用锁导致性能下降(比如每个变量都加锁)
    • 优化:缩小锁的范围(只在必要时加锁)
  3. 守护线程:主线程退出时子线程可能被强制终止
    • 解决:设置daemon=False(默认值),或手动join()等待

8.7 学习资源推荐:成为线程大师的捷径

  1. 在线工具
    • Python Threading Debugger(可视化调试多线程程序)
  2. 书籍推荐
    • 《Python并发编程实战》(详解GIL与多进程/多线程区别)
  3. 避坑秘籍
    • 优先用concurrent.futures模块简化线程池管理
    • 避免在锁内执行耗时操作(如网络请求)

8.8 本章小结

  • 核心技能

    1. 理解线程原理与GIL限制
    2. 用锁保护共享资源
    3. 实现多线程爬虫与进度统计
  • 高阶技巧

    • ThreadPoolExecutor实现线程池(自动管理线程复用)
    • 结合Queue模块实现生产者-消费者模型

多线程的搞笑彩蛋
如果线程会说话,可能会吐槽:“人类总让我抢锁,却忘了给我发个‘最佳员工’奖杯!”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值