线程的创建与启动:两种方式,一个目标

各位同学,今天我们来聊聊 Python 多线程编程的起点——threading 模块。说实话,我刚开始接触多线程时,总觉得这东西很神秘。后来踩过不少坑,才慢慢摸清门道。今天这章,咱们就从最基础的线程创建开始。

为什么需要多线程?

先问大家一个问题:你写过一个程序,运行起来后界面卡死过吗?

我遇到过。有一次写个文件下载工具,单线程下载大文件时,整个界面像死了一样。用户点取消都没反应。这就是典型的单线程阻塞问题。

多线程的核心价值,说白了就是「并发执行」。让程序同时干多件事,比如一边下载文件,一边响应用户操作。嗯,这就是我们学习 threading 的初衷。

创建线程的两种方式

Python 的 threading 模块提供了两种创建线程的方式。我个人习惯用第二种,但第一种在某些场景下也很方便。

方式一:直接使用 Thread 类

最直接的方式,就是把一个函数传给 Thread 对象。看代码:

import threading
import time

def download_file(file_name):
    print(f"开始下载:{file_name}")
    time.sleep(3)  # 模拟下载耗时
    print(f"下载完成:{file_name}")

# 创建线程
t = threading.Thread(target=download_file, args=("python_教程.pdf",))
# 启动线程
t.start()

print("主线程继续执行其他任务...")

运行这段代码,你会发现「主线程继续执行其他任务...」会先打印出来,然后才是下载完成的消息。这就是并发——主线程没等下载线程结束,就继续往下走了。

小提示:args 参数必须是一个元组。即使只有一个参数,也要写成 ("xxx",) 这种形式。我刚开始写的时候经常漏掉那个逗号,结果报错半天找不出原因。

方式二:继承 Thread 类

这种方式更面向对象一些。当你的线程需要维护状态,或者逻辑比较复杂时,我推荐用这个。

class DownloadThread(threading.Thread):
    def __init__(self, file_name):
        super().__init__()
        self.file_name = file_name
        self.progress = 0

    def run(self):
        print(f"开始下载:{self.file_name}")
        for i in range(10):
            time.sleep(0.3)
            self.progress = (i + 1) * 10
            print(f"{self.file_name} 下载进度:{self.progress}%")
        print(f"下载完成:{self.file_name}")

# 使用
t = DownloadThread("系统编程.pdf")
t.start()

注意看,关键是要重写 run() 方法。线程启动后,会自动调用 run() 里的代码。我在项目中经常用这种方式,因为可以方便地在线程对象上保存一些中间状态,比如这里的 progress。

线程的启动与等待

创建完线程,下一步就是启动它。启动很简单,调用 start() 方法就行。但有个细节要注意:不要直接调用 run()。

为什么?

直接调用 run() 相当于普通函数调用,不会创建新线程。代码会在当前线程里顺序执行。我曾经见过新手同事犯这个错,调试了半天才发现线程根本没跑起来。

那怎么等待线程结束呢?用 join() 方法:

t = threading.Thread(target=download_file, args=("data.zip",))
t.start()

print("等待下载线程结束...")
t.join()  # 主线程会在这里阻塞,直到 t 线程执行完毕
print("下载线程已结束,主线程继续")

join() 的作用就是让主线程「等一等」。这在需要汇总多个线程结果时特别有用。比如你要同时下载 10 个文件,等全部下载完再合并处理,这时候 join() 就派上用场了。

daemon 属性:守护线程的秘密

这是很多初学者容易忽略的点,但我觉得它非常重要。

先看个例子:

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

t = threading.Thread(target=background_task)
t.daemon = True  # 设置为守护线程
t.start()

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

运行这段代码,你会发现主线程结束后,程序就退出了。那个死循环的后台任务也跟着停了。

这就是 daemon 线程的特性:当所有非守护线程都结束时,守护线程会被强制终止。

核心概念:daemon 线程是为其他线程服务的。主线程结束后,守护线程就没有存在的意义了,Python 会直接把它「干掉」。

我曾经写过一个日志收集系统,后台有个线程专门收集日志并写入文件。这个线程就设成了 daemon。因为主程序退出后,日志收集也没必要继续了。但如果你的线程在做一些必须完成的操作(比如保存数据),千万别设成 daemon,否则数据可能丢失。

避坑指南:daemon 属性必须在 start() 之前设置。我曾经在 start() 之后才想起来设 daemon,结果抛了个 RuntimeError。记住:先设属性,再启动线程。

知识体系总览

下面这张图总结了本章的核心内容,帮你理清思路:

threading 模块入门知识体系 线程创建与管控 创建方式 方式一:Thread(target=func) 方式二:继承 Thread 重写 run() 启动与等待 start() 启动新线程 join() 等待线程结束 daemon 属性 守护线程随主线程退出 start() 前设置才有效 核心原则 先设 daemon → 再 start() → 需要结果时 join()

总结一下

这一章我们学了三个核心点:

  • 创建线程:两种方式,简单场景用函数式,复杂场景用继承式
  • 启动与等待:start() 启动新线程,join() 等待线程结束。千万别直接调 run()
  • daemon 属性:守护线程随主线程退出而终止,适合后台服务类任务

嗯,这些基础打牢了,后面讲线程同步、锁、条件变量时,你才不会觉得吃力。下一章我们聊聊线程之间怎么安全地共享数据——那才是多线程编程真正「烧脑」的地方。

我的建议:学完这章,立刻动手写几个小例子。比如创建一个下载线程,再创建一个进度显示线程,看看它们怎么配合。光看不练,永远学不会多线程。

可可精品资料库