2. threading模块入门:创建线程的两种方式

说实话,Python的多线程入门并不难。但很多新手一上来就被各种概念搞晕了。我刚开始学的时候也是这样,总觉得线程是个很玄乎的东西。后来做项目多了才发现,其实核心就那么几个点。

今天咱们就聊聊threading模块最基础的东西——创建线程的两种方式。你想想看,不管多复杂的并发程序,归根结底都是从这两招开始的。

2.1 函数式创建:最直接的方式

我个人习惯先用函数式来演示,因为它够简单、够直观。说白了,就是把你要并发执行的代码,封装到一个普通函数里,然后交给Thread去跑。

import threading
import time

def worker(name, delay):
    """模拟一个耗时任务"""
    print(f"线程 {name} 开始工作")
    time.sleep(delay)
    print(f"线程 {name} 工作完成,耗时{delay}秒")

# 创建线程
t1 = threading.Thread(target=worker, args=("A", 2))
t2 = threading.Thread(target=worker, args=("B", 3))

# 启动线程
t1.start()
t2.start()

# 等待线程结束
t1.join()
t2.join()

print("所有线程执行完毕")

这段代码跑起来,你会发现两个线程几乎是同时启动的。我在项目中遇到过类似场景——批量下载文件时,用这种方式把每个下载任务丢到独立线程里,速度直接翻倍。

小技巧:args参数必须传元组,哪怕只有一个参数也要写成 (value,) 这种形式。我刚开始就老忘这个逗号,结果报错半天找不出原因。

2.2 类继承方式:更灵活的结构

当你的线程逻辑比较复杂,或者需要维护一些内部状态时,函数式就不太够用了。这时候我推荐用类继承的方式——继承Thread类,重写run方法。

import threading
import time

class DownloadThread(threading.Thread):
    def __init__(self, url, save_path):
        super().__init__()
        self.url = url
        self.save_path = save_path
        self.downloaded_size = 0  # 内部状态
    
    def run(self):
        """线程启动后自动执行的方法"""
        print(f"开始下载: {self.url}")
        # 模拟下载过程
        for i in range(5):
            time.sleep(1)
            self.downloaded_size += 20
            print(f"[{self.name}] 已下载: {self.downloaded_size}%")
        print(f"下载完成: {self.save_path}")

# 使用
t = DownloadThread("https://example.com/file.zip", "/tmp/file.zip")
t.start()
t.join()

你看,类继承的好处就出来了——可以保存每个线程自己的状态(比如下载进度),还能扩展更多方法。我记得有一次做爬虫项目,需要统计每个线程抓取了多少页面,用类继承就特别方便。

核心区别:
  • 函数式:简单任务首选,代码量少
  • 类继承:复杂逻辑、需要状态管理时用

2.3 线程的启动与等待

嗯,这里要注意两个关键方法:start() 和 join()。

start() 是告诉系统:「这个线程可以跑了」。但注意,它不会立即执行,而是进入就绪状态,等操作系统调度。我见过有人直接调用 run() 方法——那其实是在主线程里顺序执行,根本不是并发!

join() 是让主线程等一等。比如你下载文件,主线程必须等下载线程结束才能处理文件。不调用join的话,主线程可能先跑完了,程序就退出了。

import threading
import time

def slow_task(name, seconds):
    time.sleep(seconds)
    print(f"{name} 完成")

# 错误示范:不等待
t = threading.Thread(target=slow_task, args=("任务1", 3))
t.start()
print("主线程结束")  # 这行会先打印,因为没等t

# 正确做法:等待
t = threading.Thread(target=slow_task, args=("任务2", 3))
t.start()
t.join()  # 主线程停在这里,等t结束
print("主线程结束")  # 这行会在任务2完成后才打印
我曾经踩过的坑:在GUI程序里用join()会导致界面卡死。因为join()会阻塞主线程,而GUI的事件循环也在主线程里。解决方案是用回调或者队列来通知。

2.4 线程的命名与标识

多线程调试时最头疼的是什么?分不清哪个线程在干活。所以给线程起个好名字特别重要。

方法 说明 示例
name参数 创建时指定名字 Thread(name="下载线程1")
ident属性 系统分配的唯一ID thread.ident
native_id 操作系统层面的线程ID thread.native_id (Python 3.8+)
current_thread() 获取当前线程对象 threading.current_thread()
import threading
import time

def worker():
    current = threading.current_thread()
    print(f"当前线程: {current.name}, ID: {current.ident}")
    time.sleep(1)

# 创建时指定名字
threads = []
for i in range(3):
    t = threading.Thread(target=worker, name=f"Worker-{i}")
    threads.append(t)
    t.start()

for t in threads:
    t.join()

# 如果不指定名字,系统会默认叫 Thread-1, Thread-2 ...
t_default = threading.Thread(target=worker)
t_default.start()
t_default.join()

我个人习惯在创建线程时都加上有意义的名字,比如 "下载线程-图片"、"计算线程-数据分析"。这样日志一打出来,一眼就能看出问题出在哪个环节。

调试小技巧:在日志里加上线程名,配合logging模块的%(threadName)s格式化,排查问题效率翻倍。

2.5 本章知识体系

说了这么多,咱们用一张图来总结一下。这张图把线程的创建、启动、等待、命名串起来了,你一看就明白整个流程。

线程创建与生命周期 函数式创建 Thread(target=func, args=(...)) 类继承创建 class MyThread(Thread): def run(self): start() → 线程进入就绪状态 线程运行中(可设置name、获取ident) join() 等待线程结束 线程自然结束

这张图把整个流程串起来了。你从上面两种创建方式选一种,然后调用start()启动,线程就跑起来了。最后用join()等它结束,或者让它自己跑完。就这么简单。

好了,线程的创建入门就聊到这儿。记住两个核心点:函数式适合简单任务,类继承适合复杂逻辑。命名一定要有意义,调试时能省不少时间。下一节咱们聊聊线程同步的问题——那才是真正考验并发功底的地方。

可可精品资料库