MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python threading 模块之 Thread 类详解

2021-02-201.9k 阅读

Python threading 模块简介

在Python中,threading模块是用于多线程编程的标准库。多线程编程允许在一个程序中同时运行多个线程,每个线程可以执行不同的任务,这在处理I/O密集型任务、提高程序响应性等方面具有显著优势。threading模块提供了丰富的类和函数,其中Thread类是实现多线程的核心类。

Thread 类的基本使用

Thread类用于创建和管理线程。要使用Thread类,首先需要导入threading模块。以下是一个简单的示例,展示了如何创建和启动一个线程:

import threading


def print_message():
    print("This is a thread.")


# 创建一个线程对象
thread = threading.Thread(target=print_message)
# 启动线程
thread.start()

在上述代码中:

  1. 定义了一个函数print_message,这是线程要执行的任务。
  2. 使用threading.Thread类创建了一个线程对象thread,并将print_message函数作为target参数传递给Thread类的构造函数。这意味着当线程启动时,它将执行print_message函数中的代码。
  3. 调用thread.start()方法启动线程。一旦调用start(),Python会安排线程在合适的时间开始执行target函数。

Thread 类的构造函数参数

Thread类的构造函数具有多个参数,提供了丰富的定制化选项:

class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
  1. group:通常为None,这个参数是为了将来实现线程组而保留的。目前在Python中,线程组尚未完全实现,因此一般不需要关注此参数。
  2. target:指定线程要执行的可调用对象(通常是函数)。如果不指定target,线程启动后将不执行任何操作。
  3. name:线程的名称。如果不指定,Python会自动为线程分配一个唯一的名称,格式为Thread-N,其中N是一个递增的数字。
  4. args:传递给target函数的位置参数,以元组的形式表示。例如,如果target函数接受两个参数ab,可以这样传递参数:args=(a, b)
  5. kwargs:传递给target函数的关键字参数,以字典的形式表示。例如,如果target函数接受关键字参数key1key2,可以这样传递参数:kwargs = {'key1': value1, 'key2': value2}
  6. daemon:设置线程是否为守护线程。守护线程是一种特殊的线程,当主线程退出时,守护线程会自动终止,而不管其是否完成任务。如果不设置此参数,线程的守护状态将继承自创建它的线程。如果主线程是守护线程,新创建的线程默认也是守护线程;否则,新线程默认不是守护线程。

以下是一个更复杂的示例,展示如何使用这些参数:

import threading


def greet(name, greeting='Hello'):
    print(f"{greeting}, {name}!")


# 创建线程并传递参数
thread = threading.Thread(target=greet, args=('Alice',), kwargs={'greeting': 'Hi'})
thread.start()

在这个例子中,greet函数接受两个参数namegreeting。通过args传递了位置参数'Alice',通过kwargs传递了关键字参数'greeting': 'Hi'

线程的生命周期

线程从创建到结束经历多个阶段,了解这些阶段有助于更好地管理和调试多线程程序。

  1. 新建(New):当使用Thread类创建一个线程对象时,线程处于新建状态。此时线程尚未开始执行。
  2. 就绪(Runnable):调用start()方法后,线程进入就绪状态。在这个状态下,线程等待CPU调度,一旦获得CPU时间片,就会开始执行。
  3. 运行(Running):线程获得CPU时间片,开始执行target函数中的代码,此时线程处于运行状态。
  4. 阻塞(Blocked):在运行过程中,线程可能会因为某些原因(如等待I/O操作完成、等待锁等)进入阻塞状态。在阻塞状态下,线程暂停执行,不会占用CPU时间片,直到阻塞条件解除,线程重新回到就绪状态。
  5. 死亡(Dead):当target函数执行完毕或者抛出未处理的异常时,线程进入死亡状态。此时线程的生命周期结束,不再执行任何代码。

线程的属性和方法

  1. name 属性:可以通过name属性获取或设置线程的名称。
import threading


def print_thread_name():
    print(f"Current thread name: {threading.current_thread().name}")


thread = threading.Thread(target=print_thread_name, name='CustomThread')
thread.start()

在上述代码中,通过name='CustomThread'设置了线程的名称,然后在print_thread_name函数中通过threading.current_thread().name获取当前线程的名称并打印。

  1. ident 属性ident属性返回线程的唯一标识符(如果线程尚未启动,返回None)。这个标识符是一个整数,在整个Python进程中是唯一的。
import threading
import time


def print_thread_ident():
    time.sleep(1)
    print(f"Thread ident: {threading.current_thread().ident}")


thread = threading.Thread(target=print_thread_ident)
thread.start()

在这个例子中,由于线程启动需要一定时间,所以在print_thread_ident函数中使用time.sleep(1)等待1秒,以确保线程已经启动,从而可以获取到有效的ident

  1. is_alive() 方法is_alive()方法用于判断线程是否还在运行。
import threading
import time


def long_running_task():
    time.sleep(2)


thread = threading.Thread(target=long_running_task)
thread.start()
print(f"Is thread alive: {thread.is_alive()}")
time.sleep(3)
print(f"Is thread alive: {thread.is_alive()}")

在上述代码中,启动线程后立即检查线程是否存活,然后等待3秒后再次检查。由于long_running_task函数睡眠2秒,所以第一次检查时线程是存活的,第二次检查时线程已经结束,不再存活。

  1. join() 方法join()方法用于等待线程结束。调用join()的线程会阻塞,直到被调用的线程执行完毕。
import threading
import time


def long_running_task():
    time.sleep(2)


start_time = time.time()
thread = threading.Thread(target=long_running_task)
thread.start()
thread.join()
end_time = time.time()
print(f"Total time: {end_time - start_time} seconds")

在这个例子中,主线程调用thread.join()后会阻塞,直到long_running_task函数执行完毕。这样可以确保在计算总时间时,包含了线程执行的时间。

线程共享数据

在多线程编程中,多个线程可能需要访问和修改共享数据。然而,这可能会导致数据竞争和不一致的问题。以下是一个简单的示例,展示了数据竞争的问题:

import threading


counter = 0


def increment():
    global counter
    for _ in range(1000000):
        counter += 1


threads = []
for _ in range(2):
    thread = threading.Thread(target=increment)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print(f"Final counter value: {counter}")

在上述代码中,定义了一个全局变量counter,两个线程都尝试对其进行100万次递增操作。理论上,最终counter的值应该是200万。但实际上,由于两个线程同时访问和修改counter,可能会导致数据竞争,最终的结果往往小于200万。

使用锁(Lock)解决数据竞争

为了解决数据竞争问题,可以使用锁(Lock)。锁是一种同步原语,它保证在同一时间只有一个线程可以访问共享资源。以下是使用锁修改后的代码:

import threading


counter = 0
lock = threading.Lock()


def increment():
    global counter
    with lock:
        for _ in range(1000000):
            counter += 1


threads = []
for _ in range(2):
    thread = threading.Thread(target=increment)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print(f"Final counter value: {counter}")

在这个例子中,创建了一个Lock对象lock。在increment函数中,使用with lock语句来获取锁。当一个线程进入with块时,它会自动获取锁,离开with块时,会自动释放锁。这样就确保了在同一时间只有一个线程可以执行counter += 1操作,从而避免了数据竞争。

线程同步原语 - 信号量(Semaphore)

除了锁之外,threading模块还提供了其他同步原语,如信号量(Semaphore)。信号量可以控制同时访问共享资源的线程数量。以下是一个使用信号量的示例:

import threading
import time


# 创建一个信号量,允许最多3个线程同时访问
semaphore = threading.Semaphore(3)


def access_resource(thread_num):
    with semaphore:
        print(f"Thread {thread_num} has access to the resource.")
        time.sleep(2)
        print(f"Thread {thread_num} is done with the resource.")


threads = []
for i in range(5):
    thread = threading.Thread(target=access_resource, args=(i,))
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

在上述代码中,创建了一个信号量semaphore,允许最多3个线程同时访问共享资源。每个线程在访问资源前通过with semaphore获取信号量,访问完成后自动释放信号量。这样,在任何时刻,最多只有3个线程可以同时访问资源。

线程同步原语 - 事件(Event)

事件(Event)是另一种同步原语,它允许一个线程通知其他线程发生了某个事件。Event对象有一个内部标志,线程可以等待这个标志被设置,也可以设置或清除这个标志。以下是一个使用事件的示例:

import threading
import time


event = threading.Event()


def wait_for_event():
    print("Thread waiting for event.")
    event.wait()
    print("Thread reacted to event.")


def set_event():
    time.sleep(3)
    print("Setting event.")
    event.set()


wait_thread = threading.Thread(target=wait_for_event)
set_thread = threading.Thread(target=set_event)

wait_thread.start()
set_thread.start()

wait_thread.join()
set_thread.join()

在这个例子中,wait_for_event线程调用event.wait()等待事件发生。set_event线程在睡眠3秒后调用event.set()设置事件。当事件被设置后,wait_for_event线程会继续执行。

线程同步原语 - 条件变量(Condition)

条件变量(Condition)结合了锁和事件的功能,允许线程在满足特定条件时进行通信和同步。以下是一个使用条件变量的示例:

import threading


condition = threading.Condition()
queue = []


def producer():
    with condition:
        for i in range(5):
            queue.append(i)
            print(f"Produced: {i}")
            condition.notify()
            condition.wait()


def consumer():
    with condition:
        for _ in range(5):
            condition.wait()
            item = queue.pop(0)
            print(f"Consumed: {item}")
            condition.notify()


producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)

producer_thread.start()
consumer_thread.start()

producer_thread.join()
consumer_thread.join()

在上述代码中,producer线程生产数据并放入队列queue中,然后调用condition.notify()通知consumer线程有新数据。接着,producer线程调用condition.wait()等待consumer线程消费数据。consumer线程在启动后调用condition.wait()等待producer线程的通知,当收到通知后,从队列中取出数据并消费,然后调用condition.notify()通知producer线程可以继续生产。

线程池

在实际应用中,频繁地创建和销毁线程会带来一定的开销。线程池(ThreadPool)可以解决这个问题,它预先创建一定数量的线程,并将任务分配给这些线程执行。虽然Python标准库中没有直接提供线程池类,但可以使用concurrent.futures模块中的ThreadPoolExecutor来实现类似功能。以下是一个简单的示例:

import concurrent.futures


def square(x):
    return x * x


with concurrent.futures.ThreadPoolExecutor() as executor:
    numbers = [1, 2, 3, 4, 5]
    results = list(executor.map(square, numbers))
    print(results)

在上述代码中,使用ThreadPoolExecutor创建了一个线程池。executor.map方法将square函数应用到numbers列表的每个元素上,并返回结果。map方法会自动管理线程的分配和任务的执行,不需要手动创建和启动线程。

线程安全的设计原则

  1. 尽量减少共享数据:减少共享数据可以降低数据竞争的风险。如果可能,尽量让每个线程处理独立的数据。
  2. 使用同步原语:在访问共享数据时,使用锁、信号量、事件等同步原语来确保数据的一致性。
  3. 避免死锁:死锁是多线程编程中常见的问题,它发生在两个或多个线程相互等待对方释放资源的情况下。为了避免死锁,应确保线程以相同的顺序获取锁,并且尽量缩短持有锁的时间。
  4. 使用线程安全的数据结构:Python提供了一些线程安全的数据结构,如queue.Queue。使用这些数据结构可以避免手动同步带来的复杂性。

多线程与多核CPU

需要注意的是,Python的多线程在多核CPU上并不能充分利用多核优势。这是因为Python的全局解释器锁(GIL)机制。GIL确保在同一时间只有一个线程可以执行Python字节码,即使在多核CPU上也是如此。因此,对于CPU密集型任务,多线程可能并不能提高性能,反而可能因为线程切换带来额外开销。对于I/O密集型任务,由于线程在等待I/O操作时会释放GIL,多线程仍然可以提高程序的整体效率。如果需要充分利用多核CPU的性能,可以考虑使用multiprocessing模块进行多进程编程。

总结

threading模块的Thread类是Python多线程编程的核心。通过合理使用Thread类及其相关的同步原语,可以有效地编写多线程程序,提高程序的响应性和效率。在实际应用中,需要注意线程安全问题,避免数据竞争和死锁等常见问题。同时,要根据任务的性质(CPU密集型或I/O密集型)来选择合适的并发编程方式。希望通过本文的详细介绍,读者对Thread类有更深入的理解,并能在实际项目中灵活运用多线程技术。

以上就是关于Python threading模块之Thread类的详细讲解,涵盖了从基本使用到高级同步机制以及线程池等内容,希望能帮助你在Python多线程编程领域更上一层楼。