python多线程详细讲解

发布于:2025-06-29 ⋅ 阅读:(14) ⋅ 点赞:(0)

1. 主线程与子线程的基本概念

  • 主线程:程序启动时自动创建的线程,是程序的入口点。
  • 子线程:由主线程通过threading.Thread创建的线程,用于并发执行任务。

多线程的实现

多线程模式,可以将批量的测试用例并发执行。
通过多线程的模式来并发处理测试用例时,最直接的方法就是,先将需要处理的内容保存在list中,基于list的元素来进行线程的添加,有多少元素,添加多少线程,最后通过start()来启动所有线程即可。

(1)简单示例

进程启动——>产生一个主线程——>多线程时,主线程会创建多个线程——>主线程代码执行完成,退出。

#!/usr/bin/env python
# encoding: utf-8

import threading
from time import sleep

def run(username):
    print('-----当前线程:' + threading.current_thread().name + '-----')
    print(username + '开始跑步')
    sleep(1)  # 休眠
    print(username + '跑累了')

# 一起跑步:通过多线程的形态实现多用户一起跑步的效果
# 建立线程池
rlist = []

# 分配线程
rlist.append(threading.Thread(target=run, args=['一片'], name='线程1'))
rlist.append(threading.Thread(target=run, args=('冰心',), name='线程2'))
rlist.append(threading.Thread(target=run, args=('玉壶',), name='线程3'))

print('..主线程继续执行..' + threading.current_thread().name)
for t in rlist:
    t.start()
print('..主线程代码执行完毕,但主线程并未真正退出..')

控制台输出:
输出结果

(输出结果并不一定,因为CPU调度的不确定性,即使休眠时间相同,线程唤醒后的执行顺序仍由操作系统调度决定,可能出现:)

冰心跑累了
一片跑累了
玉壶跑累了

(2)对比传统单线程

#!/usr/bin/env python
# encoding: utf-8

import threading
from time import sleep


def run(username):
    print('-----当前线程:' + threading.current_thread().name + '-----')
    print(username + '开始跑步')
    sleep(1)
    print(username + '跑累了')

# 传统单线程调用
for name in ['一片', '冰心', '玉壶']:
    run(name)

控制台:(注意到当前线程都是MainThread)

-----当前线程:MainThread-----
一片开始跑步
一片跑累了
-----当前线程:MainThread-----
冰心开始跑步
冰心跑累了
-----当前线程:MainThread-----
玉壶开始跑步
玉壶跑累了

2. 主线程与子线程的执行关系

  • 并发执行:主线程和子线程独立运行并发执行,默认互不等待(守护线程会随主线程强制终止)。
  • 结束顺序
    • 默认情况:主线程不会等待子线程结束,会直接退出(这里的退出指的是主线程代码执行结束,不是程序真正退出)。
    • 守护线程(Daemon Thread):通过t.daemon = True设置,主线程退出时会强制终止守护线程。
    • 非守护线程:主线程必须等待所有非守护线程结束才能真正退出(这里的退出是指程序终止退出),主线程的代码执行完毕后会继续等待非守护线程。

(注意:退出从两个层面区分,“主线程代码执行完毕” ≠ “程序退出”,程序退出的条件是所有非守护线程结束且主线程代码执行完毕。)

(PS:不懂守护进程可先跳转下方【4.设置守护进程】查看)

import threading
import time

def daemon_worker():
    print("守护线程开始")
    time.sleep(5)
    print("守护线程结束(可能不会执行到这一步)")

def non_daemon_worker():
    print("非守护线程开始\n")
    time.sleep(3)
    print("非守护线程结束")

# 创建守护线程
daemon_t = threading.Thread(target=daemon_worker)
daemon_t.daemon = True  # 设置为守护线程

# 创建非守护线程
non_daemon_t = threading.Thread(target=non_daemon_worker)

daemon_t.start()
non_daemon_t.start()

print("-------------主线程继续执行-------------")
time.sleep(1)
print("-----------主线程代码执行完毕,守护线程会被强制终止,非守护线程会继续执行---------------")

输出:
输出结果

3. join(timeout=None)设置线程阻塞

join()让调用者线程阻塞,即调用者会暂时停止执行直到其他线程全部完成后才会继续执行。简言之,join()方法可以用来等待其他线程(被调用线程)的完成。timeout参数指定调用者等待多久。没有设置时,调用者就一直等待直到被调用线程结束后,才会结束。

具体来说,join()方法有以下作用:

  • 阻塞等待其他线程。
  • 等待其他线程完成后才能执行后面的代码。

例1:未设置timeout

执行join()但未指定timeout参数,主线程等全部子线程执行完毕后,才结束

#!/usr/bin/env python
# encoding: utf-8

import threading
from time import sleep

def run(username):
    print('..当前线程:' + threading.current_thread().name+'..')
    print(username + '开始跑步')
    sleep(1)  # 休眠
    print(username + '跑累了')

# 一起跑步:通过多线程的形态实现多用户一起跑步的效果
# 建立线程池
rlist = []

# 分配线程
rlist.append(threading.Thread(target=run, args=['一片'], name='线程1'))
rlist.append(threading.Thread(target=run, args=('冰心',), name='线程2'))
rlist.append(threading.Thread(target=run, args=('玉壶',), name='线程3'))


print('-------主线程继续执行' + threading.current_thread().name + '-------------------')
for t in rlist:
    t.start()
    # 主线程调用 t.join(),主线程被阻塞,等待子线程结束,主线程解除阻塞,继续循环启动子线程
    t.join()  # 被调用线程:t(子线程),调用者线程:执行 t.join() 的线程(本例中是主线程)
print('----------------主线程代码执行完毕----------------------')

控制台输出:
输出结果

上面的例子join()在循环内(如当前代码),线程会顺序执行(失去并发意义),可优化如下:

print('-------主线程继续执行' + threading.current_thread().name + '-------------------')
for t in rlist:
    t.start()

for t in rlist:
    # 主线程调用 t.join(),主线程被阻塞,等待子线程结束,主线程解除阻塞,继续循环启动子线程
    t.join()  # 被调用线程:t(子线程),调用者线程:执行 t.join() 的线程(本例中是主线程)

print('----------------主线程代码执行完毕----------------------')

输出:(三个线程并发执行,跑累了的顺序可能随机)
输出结果

例2:设置timeout

我们创建了三个不同的子线程并启动它们。之后,在join()方法中设置了一个最大等待时间为t1:0.1秒,t2:0.2秒钟,如果超时还未等待到三个线程全部完成,那么主线程会继续执行后续代码,打印出”主线程代码执行结束”的消息。

import threading
from time import sleep


def run(username):
    print('..当前线程:' + threading.current_thread().name + '..')
    print(username + '开始跑步')
    sleep(1)
    print(username + '跑累了')


# 一起跑步:通过多线程的形态实现多用户一起跑步的效果
# 建立线程池
rlist = []

# 分配线程,
rlist.append(threading.Thread(target=run, args=['一片'], name='线程1'))
rlist.append(threading.Thread(target=run, args=('冰心',), name='线程2'))
rlist.append(threading.Thread(target=run, args=('玉壶',), name='线程3'))

# 例4:join()设置timeout
t1 = threading.Thread(target=run, args=['一片'], name='线程1')
t2 = threading.Thread(target=run, args=('冰心',), name='线程2')
t3 = threading.Thread(target=run, args=('玉壶',), name='线程3')
print('-------主线程继续执行' + threading.current_thread().name + '-------------------')
t1.start()
t2.start()
t3.start()
# t1.join() t1不用join,随缘
# 下面任一个要是timeout>1s,就看不到“主线程结束了”的提前打印,
# 因为只要主线程需要等比如t1两秒,那t2在这2s内也早结束了,就只能看到“主线程结束了”打印在最后
t2.join(timeout=0.1)
t3.join(timeout=0.2)
print('----------------主线程代码执行结束----------------------')

控制台输出:(结果之一,因为哪个线程先结束是不确定的)
输出结果

分析:因为3个子线程中间都需要休眠1s,而t2,t3的timeout都只设置了0.1、0.2s,主线程只等这么点时间,时间到了主线程结束,这时3个子线程的休眠可能还没结束,所以打印如上图。

4. 设置守护进程

线程是程序执行的最小单位,Python在进程启动起来后,会自动创建一个主线程,之后使用多线程机制可以在此基础上进行分支,产生新的子线程。子线程启动起来后,主线程默认会等待所有线程执行完成之后再退出。但是我们可以将子线程设置为守护线程,此时主线程任务一旦完成,所有子线程将会和主线程一起结束(就算子线程没有执行完也会退出)。

守护线程可以在线程启动之前,通过setDaemon(True)的形式进行设置,或者在创建子线程对象时,以参数的形式指定:

thread01 = Thread(target=target01, args="", name="线程1", daemon=True)

但是需要注意,如果希望主程序不等待任何线程直接退出,只有所有的线程都被设置为守护线程才有用。

#!/usr/bin/env python
# encoding: utf-8

import threading
from time import sleep

def run(username):
    print('-----当前线程:' + threading.current_thread().name + '-----')
    print(username + '开始跑步')
    # 一片休眠1s,冰心休眠3s,玉壶休眠5s
    sleep(1) if username == '一片' else sleep(3) if username == '冰心' else sleep(5)
    print(username + '跑累了')
# 例4:设置守护进程
# 分配线程,全部设为守护进程
# 配合timeout=n,发挥主线程结束就把子线程也结束的效果,如果不设置timeout参数,就算daemon=True,也会等该子线程结束再结束主线程
t1 = threading.Thread(target=run, args=['一片'], name='线程1', daemon=True)
t2 = threading.Thread(target=run, args=('冰心',), name='线程2', daemon=True)
t3 = threading.Thread(target=run, args=('玉壶',), name='线程3', daemon=True)
print('-------主线程继续执行' + threading.current_thread().name + '-------------------')
t1.start()
t2.start()
t3.start()
t1.join()  # 无timeout参数,会等t1执行完,因为t1休眠1s,所以到t1结束的时间>1s
t2.join(2)  # t1所用的1s+此处t2的timeout=2s,至少有3s,t2休眠3s,所以t2也是可以在主线程结束前结束的
t3.join(0.5)  # 1+2+0.5=3.5<5(t3休眠5s),所以主线程先结束,而t3设置了守护进程daemon=True,所以会随着主线程的结束,也结束了该子线程
print('----------------主线程代码执行结束----------------------')

控制台输出:
输出结果

分析:

  • t1:未打印“玉壶跑累了”,因为t1无timeout参数,会等t1执行完,t1休眠1s,所以到t1结束的时间>1s;
  • t2:休眠时间3s,timeout=2s,再加上执行完t1的>1s,总共>3s,够t2执行完。
  • t3:1+2+0.5=3.5<5(t3休眠5s),所以主线程在执行完t2后再等待0.5s,此时t3休眠未结束,主线程不等了先结束,而t3设置了守护进程daemon=True,所以会随着主线程的结束,也结束了该子线程

5. 线程锁Lock()

(1)线程同步与数据共享

  • 全局变量共享:主线程和子线程可以访问相同的全局变量,但需注意线程安全。
  • 线程安全问题:多个线程同时修改共享数据可能导致数据不一致。
  • 同步机制:
    • 锁(Lock):threading.Lock用于互斥访问共享资源。
    • 信号量(Semaphore):控制同时访问资源的线程数量。
    • 条件变量(Condition):线程间的等待 / 通知机制。

(2)线程锁的基本用法(threading.Lock)

线程锁提供两种状态:锁定(locked)和未锁定(unlocked),过acquire()release()方法控制:

例1
import threading

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

def increment():
    global counter
    for _ in range(100000):
        lock.acquire()  # 获取锁(阻塞直到锁可用)
        try:
            counter += 1  # 临界区代码,确保原子性
        finally:
            lock.release()  # 释放锁,必须在finally中确保释放

# 创建两个子线程
threads = [threading.Thread(target=increment) for _ in range(2)]
for t in threads:
    t.start()
for t in threads:
    t.join()#join()的作用:让主线程等待子线程执行完毕,避免主线程提前退出。

print(f"Counter is {counter}")  # 正确输出200000
例2:更优雅的写法with lock

Lock对象支持上下文管理器,可自动管理锁的获取和释放

import threading

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

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

threads = [threading.Thread(target=increment) for _ in range(2)]
for t in threads:
    t.start()
for t in threads:
    t.join() #join()的作用:让主线程等待子线程执行完毕,避免主线程提前退出。注释后counter一般小于200000,主线程代码执行结束时,子线程还在执行

print(f"Counter is {counter}")  # 正确输出200000

输出:Counter is 200000

例3

当多个线程对同一份资源进行读写操作时,我们可以通过加锁来确保数据安全。可以通过threading.lock类来创建锁对象,一旦一个线程获得一个锁,会阻塞之后所有尝试获得该锁对象的线程,直到它被重新释放。这里举一个例子,通过加锁来确保两个线程在对同一个全局变量进行读写时的数据安全。

#!/usr/bin/env python
# encoding: utf-8

import threading
from time import sleep

book_num = 100  # 图书馆最开始有100本图书
bookLock = threading.Lock()


def books_return():
    global book_num
    while 97<book_num<103:
        bookLock.acquire()
        book_num += 1
        print("归还1本,现有图书{}本".format(book_num))
        bookLock.release()
        sleep(1)  # 模拟事件发生周期


def books_lease():
    global book_num
    while 97<book_num<103:
        with bookLock:
            book_num -= 1
            print("借走1本,现有图书{}本".format(book_num))
            sleep(1)  # 模拟事件发生周期


if __name__ == "__main__":
    thread_lease = threading.Thread(target=books_lease)
    thread_return = threading.Thread(target=books_return)
    thread_lease.start()
    thread_return.start()

控制台:(结果随机)
输出结果

6. 线程间通信(Queue)

  • 共享变量:通过全局变量或类属性传递数据(需配合锁)。
  • 队列(Queue):queue.Queue是线程安全的,用于线程间安全地交换数据。其阻塞机制自动处理了生产者 - 消费者的同步问题,无需手动加锁。

示例:

import threading
import queue
import time

q = queue.Queue()

def producer():
    for i in range(5):
        q.put(i)
        print(f"生产者线程: 放入数据 {i}")
        time.sleep(1)

def consumer():
    while True:
        item = q.get()
        if item is None:  # 退出信号
            break
        print(f"消费者线程: 取出数据 {item}")
        q.task_done()  # 通知队列任务已完成

producer_t = threading.Thread(target=producer)
consumer_t = threading.Thread(target=consumer)

producer_t.start()
consumer_t.start()

producer_t.join()  # 等待生产者线程结束
q.put(None)  # 发送退出信号给消费者
consumer_t.join()  # 等待消费者线程结束
print("所有线程已结束")

7. 异常处理

子线程异常:子线程中的未捕获异常不会影响主线程或其他线程,只会导致该子线程终止。

import threading
import time

def error_worker():
    print("子线程开始")
    time.sleep(1)
    raise Exception("子线程出错了!")

t = threading.Thread(target=error_worker)
t.start()

print("主线程继续执行,不受子线程异常影响")
time.sleep(3)
print("主线程结束")

控制台输出:
输出结果

8. 关于多线程的输出在控制台中“混在一起”

根本原因:标准输出(stdout)不是线程安全的

  1. 多个线程共享控制台输出当多个线程同时调用print()时,它们实际上是在竞争同一个控制台输出资源。
  2. 输出操作不是原子的print()内部包含多个步骤(格式化字符串、写入缓冲区、刷新到终端),这些步骤可能被其他线程打断。
  3. GIL不保证输出完整性虽然Python的全局解释器锁(GIL)确保同一时间只有一个线程执行字节码,但print()涉及系统调用,可能在执行期间释放GIL,允许其他线程插入。

部分参考:
Python多线程详解
join方法详解
join参考
python 多线程 join


网站公告

今日签到

点亮在社区的每一天
去签到