第112集:线程创建与启动

学习目标

  1. 掌握Python中创建线程的多种方法
  2. 学会如何启动和管理线程
  3. 掌握线程参数传递的方式
  4. 理解守护线程的概念和应用场景
  5. 学会设置和获取线程名称
  6. 了解线程的基本属性和状态

一、线程创建的基本方法

在Python中,threading模块提供了创建和管理线程的功能。我们可以通过以下几种方式创建线程:

1.1 直接创建Thread对象

最简单的方式是直接创建threading.Thread对象,并将需要执行的函数作为参数传递。

import threading
import time

# 定义线程要执行的函数
def worker():
    print(f"线程开始执行: {threading.current_thread().name}")
    time.sleep(2)  # 模拟耗时操作
    print(f"线程执行完成: {threading.current_thread().name}")

# 创建线程对象
t = threading.Thread(target=worker)

# 启动线程
t.start()

print(f"主线程: {threading.current_thread().name}")

解释:

  • target参数指定线程要执行的函数
  • start()方法启动线程,使其进入就绪状态
  • threading.current_thread().name获取当前线程的名称

1.2 继承Thread类

我们可以通过继承threading.Thread类并重写run()方法来创建线程。

import threading
import time

# 继承Thread类
class MyThread(threading.Thread):
    # 重写run方法
    def run(self):
        print(f"线程开始执行: {self.name}")
        time.sleep(2)
        print(f"线程执行完成: {self.name}")

# 创建自定义线程对象
t = MyThread()

# 启动线程
t.start()

print(f"主线程: {threading.current_thread().name}")

解释:

  • 自定义线程类需要继承threading.Thread
  • 必须重写run()方法,该方法定义了线程要执行的代码
  • 调用start()方法时,会自动执行run()方法

1.3 使用函数创建线程

我们可以将任何可调用对象作为线程的目标函数,包括普通函数、lambda表达式和类的实例方法。

import threading
import time

# 使用lambda表达式创建线程
t1 = threading.Thread(target=lambda: print("Hello from lambda thread"))
t1.start()

# 使用实例方法创建线程
class MyClass:
    def worker(self, name):
        print(f"Worker {name} started")
        time.sleep(1)
        print(f"Worker {name} finished")

my_obj = MyClass()
t2 = threading.Thread(target=my_obj.worker, args=("Alice",))
t2.start()

解释:

  • 任何可调用对象都可以作为target参数
  • 对于实例方法,需要提供对象实例和方法参数

二、线程参数传递

线程执行的函数通常需要参数,我们可以通过以下方式传递参数:

2.1 使用args传递位置参数

import threading
import time

def worker(name, delay):
    print(f"Worker {name} started with delay {delay}s")
    time.sleep(delay)
    print(f"Worker {name} finished")

# 使用args传递位置参数
t1 = threading.Thread(target=worker, args=("Alice", 1))
t2 = threading.Thread(target=worker, args=("Bob", 2))

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

解释:

  • args参数接受一个元组,包含传递给目标函数的位置参数
  • 参数的顺序必须与函数定义的顺序一致

2.2 使用kwargs传递关键字参数

import threading
import time

def worker(name, delay):
    print(f"Worker {name} started with delay {delay}s")
    time.sleep(delay)
    print(f"Worker {name} finished")

# 使用kwargs传递关键字参数
t1 = threading.Thread(target=worker, kwargs={"name": "Alice", "delay": 1})
t2 = threading.Thread(target=worker, kwargs={"name": "Bob", "delay": 2})

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

解释:

  • kwargs参数接受一个字典,包含传递给目标函数的关键字参数
  • 字典的键必须与函数参数名一致

2.3 同时使用args和kwargs

import threading
import time

def worker(name, delay, message=""):
    print(f"Worker {name} started with delay {delay}s{message}")
    time.sleep(delay)
    print(f"Worker {name} finished")

# 同时使用args和kwargs
t = threading.Thread(target=worker, args=("Alice", 1), kwargs={"message": " - special task"})
t.start()

解释:

  • 可以同时使用argskwargs传递参数
  • args中的参数会先传递,然后是kwargs中的参数

三、线程名称管理

每个线程都有一个名称,可以通过以下方式设置和获取:

3.1 设置线程名称

import threading

def worker():
    print(f"当前线程名称: {threading.current_thread().name}")

# 使用name参数设置线程名称
t1 = threading.Thread(target=worker, name="MyThread-1")
t2 = threading.Thread(target=worker)  # 使用默认名称

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

解释:

  • 通过name参数可以设置线程的名称
  • 如果不指定名称,系统会自动生成类似"Thread-1"的名称

3.2 获取线程名称

import threading

def worker():
    # 获取当前线程对象
    current_thread = threading.current_thread()
    # 获取线程名称
    thread_name = current_thread.name
    print(f"当前线程名称: {thread_name}")

# 创建并启动线程
t = threading.Thread(target=worker, name="CustomThread")
t.start()

# 获取线程对象的名称
print(f"线程对象名称: {t.name}")

解释:

  • threading.current_thread()返回当前正在执行的线程对象
  • 通过线程对象的name属性可以获取线程名称

四、守护线程

4.1 守护线程的概念

守护线程(Daemon Thread)是一种特殊的线程,当主线程结束时,所有守护线程会自动终止,无论它们是否执行完成。

守护线程通常用于执行一些后台任务,如日志记录、监控等,这些任务不需要等待完成。

4.2 设置守护线程

import threading
import time

def daemon_worker():
    print("守护线程开始执行")
    time.sleep(5)  # 模拟长时间运行
    print("守护线程执行完成")

def normal_worker():
    print("普通线程开始执行")
    time.sleep(2)  # 模拟短时间运行
    print("普通线程执行完成")

# 创建守护线程
# 方式1:通过构造参数设置
daemon_thread = threading.Thread(target=daemon_worker, daemon=True)

# 方式2:通过属性设置
# daemon_thread = threading.Thread(target=daemon_worker)
# daemon_thread.daemon = True

# 创建普通线程
normal_thread = threading.Thread(target=normal_worker)

# 启动线程
daemon_thread.start()
normal_thread.start()

print("主线程睡眠1秒")
time.sleep(1)
print("主线程结束")

执行结果:

守护线程开始执行
普通线程开始执行
主线程睡眠1秒
主线程结束
普通线程执行完成

解释:

  • 守护线程设置为daemon=True
  • 主线程结束后,守护线程被强制终止,没有执行到"守护线程执行完成"的打印
  • 普通线程会继续执行直到完成

4.3 守护线程的应用场景

守护线程适用于以下场景:

  1. 后台服务:如定时检查、监控任务
  2. 日志记录:后台记录程序运行日志
  3. 资源清理:自动清理临时文件或缓存
  4. 辅助任务:为主线程提供辅助功能的任务

注意事项:

  • 守护线程不能访问共享资源的临界区,因为它们可能在任何时候被终止
  • 守护线程中不应该执行需要确保完成的操作,如文件写入、数据库事务等

五、线程的基本属性和方法

5.1 线程的基本属性

属性/方法 描述
name 线程名称
daemon 是否为守护线程
ident 线程标识符
is_alive() 线程是否处于活动状态
join([timeout]) 等待线程终止,可选超时时间
start() 启动线程
run() 线程执行的目标函数,通常不需要直接调用

5.2 线程的状态

线程从创建到终止会经历以下状态:

  1. 新建(New):线程对象已创建,但尚未启动
  2. 就绪(Runnable):线程已启动,等待CPU调度
  3. 运行(Running):线程正在CPU上执行
  4. 阻塞(Blocked):线程等待某个事件完成(如I/O操作、锁获取)
  5. 死亡(Dead):线程执行完成或被终止

5.3 获取线程状态

import threading
import time

def worker():
    time.sleep(2)

# 创建线程
thread = threading.Thread(target=worker, name="MyWorker")

# 线程创建后(新建状态)
print(f"线程名称: {thread.name}")
print(f"是否为守护线程: {thread.daemon}")
print(f"线程标识符: {thread.ident}")  # None,因为线程尚未启动
print(f"线程是否活动: {thread.is_alive()}")  # False

# 启动线程
thread.start()
print(f"\n线程启动后:")
print(f"线程标识符: {thread.ident}")  # 有值
print(f"线程是否活动: {thread.is_alive()}")  # True

# 等待线程完成
thread.join()
print(f"\n线程结束后:")
print(f"线程是否活动: {thread.is_alive()}")  # False

六、线程创建与启动的实际应用

6.1 多线程下载示例

import threading
import time

# 模拟下载文件的函数
def download_file(filename, size, speed):
    print(f"开始下载: {filename} ({size}MB)")
    downloaded = 0
    while downloaded < size:
        # 模拟下载进度
        downloaded += speed
        if downloaded > size:
            downloaded = size
        print(f"{filename}: {downloaded}/{size} MB ({(downloaded/size)*100:.1f}%)")
        time.sleep(0.5)  # 模拟下载间隔
    print(f"下载完成: {filename}")

# 下载任务列表
download_tasks = [
    ("movie.mp4", 500, 50),  # (文件名, 大小(MB), 下载速度(MB/s))
    ("music.mp3", 100, 20),
    ("document.pdf", 50, 10)
]

# 创建并启动线程
threads = []
for task in download_tasks:
    filename, size, speed = task
    t = threading.Thread(target=download_file, args=(filename, size, speed))
    threads.append(t)
    t.start()

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

print("所有文件下载完成")

6.2 多线程数据处理示例

import threading
import time

# 数据处理函数
def process_data(data_chunk, processor_id):
    print(f"处理器 {processor_id} 开始处理数据块")
    # 模拟数据处理
    processed_data = [item * 2 for item in data_chunk]
    time.sleep(1)
    print(f"处理器 {processor_id} 处理完成,结果: {processed_data}")
    return processed_data

# 原始数据
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# 将数据分成块
chunk_size = 3
data_chunks = [data[i:i+chunk_size] for i in range(0, len(data), chunk_size)]

# 创建并启动线程处理数据
threads = []
results = []

for i, chunk in enumerate(data_chunks):
    # 使用lambda捕获参数
    t = threading.Thread(target=lambda c, idx: results.extend(process_data(c, idx)), args=(chunk, i+1))
    threads.append(t)
    t.start()

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

print(f"\n所有数据处理完成,总结果: {results}")

七、线程安全与同步

在多线程环境下,当多个线程同时访问共享资源时,可能会导致数据不一致的问题。在后续的课程中,我们将学习线程同步机制,如锁、信号量等,来解决这些问题。

import threading

# 共享变量
counter = 0

# 创建锁
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100000):
        with lock:  # 获取锁
            counter += 1  # 临界区操作
        # 自动释放锁

# 创建两个线程
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)

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

# 等待线程完成
t1.join()
t2.join()

print(f"最终计数器值: {counter}")  # 200000

解释:

  • threading.Lock()创建一个锁对象
  • with lock:语句确保在进入临界区前获取锁,退出时自动释放锁
  • 这样可以确保counter += 1操作的原子性

八、总结与练习

8.1 总结

  1. 线程创建方法

    • 直接创建Thread对象
    • 继承Thread类
    • 使用函数、lambda表达式或实例方法
  2. 参数传递

    • 使用args传递位置参数
    • 使用kwargs传递关键字参数
    • 同时使用argskwargs
  3. 线程管理

    • 设置和获取线程名称
    • 设置守护线程
    • 获取线程状态和属性
    • 使用join()等待线程完成
  4. 守护线程

    • 主线程结束时自动终止
    • 适用于后台任务
    • 不适合执行需要确保完成的操作

8.2 练习

  1. 基础练习

    • 编写一个程序,创建3个线程,分别打印1-10、11-20、21-30的数字
    • 每个线程执行完成后,打印线程名称和完成信息
  2. 进阶练习

    • 实现一个多线程下载器,模拟下载5个不同大小的文件
    • 使用守护线程实现一个简单的进度监控功能
    • 编写一个程序,演示主线程结束时守护线程的行为
  3. 思考问题

    • 守护线程和普通线程的区别是什么?
    • 线程参数传递有哪几种方式?各有什么优缺点?
    • 为什么需要设置线程名称?在实际应用中有什么作用?

九、扩展阅读

  1. Python官方文档:

  2. 推荐书籍:

    • 《Python Cookbook》(第3版)第12章:并发编程
    • 《Fluent Python》第17章:并发执行
  3. 在线资源:


下集预告:第113集将学习线程同步机制中的锁机制,包括如何使用锁来解决线程安全问题,以及不同类型的锁的应用场景。

« 上一篇 进程与线程概念 下一篇 » 线程同步:锁机制