python 自定义多线程的传参方式是什么

举报
汪子熙 发表于 2024/12/04 17:16:45 2024/12/04
【摘要】 在 Python 中,threading 模块提供了多线程编程的支持,允许我们通过创建线程类的方式来实现并发操作。当我们自定义线程类时,可以通过修改类的构造方法和运行逻辑来传递和处理参数。以下面这个实际的代码片段为例来说明:import threadingclass MyThread(threading.Thread): def run(self): print(self...

在 Python 中,threading 模块提供了多线程编程的支持,允许我们通过创建线程类的方式来实现并发操作。当我们自定义线程类时,可以通过修改类的构造方法和运行逻辑来传递和处理参数。

以下面这个实际的代码片段为例来说明:

import threading

class MyThread(threading.Thread):

    def run(self):
        print(self._args)

t = MyThread(args=(100,))
t.start()

从这段代码来看,MyThread 继承自 threading.Thread,并且只重写了 run() 方法。在这里的 run() 方法中,它试图输出 self._args。根据一般的编程经验,_args 并不是一个标准的属性名,而是一个在代码中没有显式定义的属性。因此,我们需要理解,这里的 self._args 是如何出现的,以及传参的逻辑是如何工作的。

分析 Python 自定义多线程传参的机制

继承自 threading.Thread

threading.Thread 是 Python 标准库中的一个类,支持线程创建与管理。当你从 threading.Thread 派生子类时,系统提供了两种方式来为线程传递参数:

  1. 通过重写 __init__ 方法
  2. 通过 Thread 类的 argskwargs 参数

上面代码中,并没有重写 __init__ 方法,使用的是 threading.Thread 提供的默认构造器。threading.Thread 类的 __init__ 方法接受两个特别重要的参数:argskwargs,这两个参数分别用于传递位置参数和关键字参数。这是标准的 Python 多线程类的传参方式。

我们可以查看 threading.Thread 类的源码来验证这一点。threading.Thread__init__ 方法是这样定义的:

class Thread:
    def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, daemon=None):
        ...
        self._args = args
        self._kwargs = kwargs
        ...

从源码可以看出,__init__ 方法接收了 argskwargs 参数,并且将它们赋值给了 self._argsself._kwargs。也就是说,在创建线程对象时,我们可以通过 args 参数向线程传递位置参数,而这些参数会自动存储在 self._args 中。这解释了你在 run 方法中直接访问 self._args 的原因。

传参的具体实现

当你创建 MyThread 实例时,像这样:

t = MyThread(args=(100,))

你实际上调用了 threading.Thread 的默认构造函数,而 args=(100,) 被传递给了这个构造函数。在构造函数内部,args 被赋值给了 self._args,这就是为什么你可以在 run() 方法中访问 self._args 的原因。

运行这个线程时,run() 方法被调用,线程会输出 self._args 中的内容,也就是 (100,)

修改代码以更清晰地控制传参

虽然你没有重写 __init__ 方法,但如果你希望显式控制传参,并且想让代码更具可读性,可以通过重写 __init__ 方法来实现。这可以让你更清楚地看到参数传递的过程。你可以这样改写代码:

import threading

class MyThread(threading.Thread):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.my_args = args

    def run(self):
        print(self.my_args)

t = MyThread(100)
t.start()

在这个版本中,我们重写了 __init__ 方法,并通过 super().__init__(*args, **kwargs) 调用父类的构造函数。这允许我们继续使用 threading.Thread 的内置功能。同时,我们将参数保存在 self.my_args 中,以便在 run() 方法中访问。

通过这种方式,我们明确地控制了参数的传递,并避免了依赖 threading.Thread 类的隐式行为。这样代码的结构更加清晰,也让维护和扩展变得更简单。

Python 多线程的其他传参方式

除了通过 argskwargs 传递参数,还有其他常见的方式来为自定义线程传递参数。一个常见的方法是将参数封装在对象内部,或者通过队列等线程安全的数据结构来传递参数和结果。下面是几种典型的做法:

1. 直接通过构造函数传递参数

如上面提到的例子,重写 __init__ 方法,并通过构造函数传递参数。这样可以确保参数在对象创建时被正确传递并存储。

2. 使用队列进行线程间通信

queue.Queue 是一个线程安全的队列,可以用于在线程之间传递参数和结果。这种方式适用于复杂的多线程应用,尤其是在需要多个线程共享数据时。

import threading
import queue

def worker(q):
    while True:
        item = q.get()
        if item is None:
            break
        print(f'Processing {item}')
        q.task_done()

q = queue.Queue()
thread = threading.Thread(target=worker, args=(q,))
thread.start()

# 传递参数
for item in range(5):
    q.put(item)

q.put(None)  # 通知线程退出
thread.join()

在这个例子中,队列 q 被用来传递参数给线程。线程可以从队列中提取任务进行处理。这样既保证了线程的同步,也避免了参数直接传递的复杂性。

3. 使用线程本地数据

threading.local() 提供了一种在多线程环境中管理局部数据的方法。每个线程都有自己的本地存储空间,可以用于存储特定的参数和数据。

import threading

thread_local = threading.local()

def worker():
    print(f'Hello, {thread_local.name}')

def set_name(name):
    thread_local.name = name
    worker()

thread1 = threading.Thread(target=set_name, args=('Alice',))
thread2 = threading.Thread(target=set_name, args=('Bob',))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

在这个例子中,threading.local() 创建了一个线程本地的存储空间。每个线程可以独立地访问和修改 thread_local 中的数据,而不会影响其他线程。通过这种方式,可以实现线程独立的参数传递。

自定义多线程传参的最佳实践

在自定义线程时,选择适当的传参方式对于代码的可维护性和健壮性非常重要。这里有一些建议可以帮助你选择合适的方法:

  • 参数简单时,使用 argskwargs 来传递参数是最直接的方式。它利用了 threading.Thread 的内置功能,减少了代码的复杂性。
  • 当需要线程间通信时,使用队列或其他同步原语(如 EventSemaphore 等)是一个更好的选择。队列可以保证线程安全地传递数据,避免了资源竞争问题。
  • 需要局部线程数据时threading.local() 提供了一种轻量级的方式来管理每个线程的独立数据。它非常适合处理线程特定的配置或状态。

线程安全问题与参数传递的影响

多线程编程中的一个核心问题是线程安全。虽然传递参数是线程执行的必要部分,但如何确保线程安全地访问和处理这些参数同样重要。某些情况下,多个线程可能需要访问共享的数据或资源,而错误地访问这些资源可能导致竞态条件和不一致的结果。

在处理参数传递时,需要特别注意:

  • 数据的共享与保护:如果多个线程共享同一组数据(如全局变量、列表等),则需要使用锁(Lock)或条件变量(Condition)来保护这些数据。
  • 不可变数据:当参数是不可变的数据类型(如元组、字符串等)时,传递这些参数通常是安全的,因为不可变对象在 Python 中是线程安全的。
  • 深拷贝与浅拷贝:当传递可变对象时,特别是列表、字典等容器时,需要考虑是否应该使用拷贝操作来避免不同线程对同一数据进行修改。

下面是一个例子,展示如何使用 Lock 来确保线程安全地访问共享资源:

import threading

lock = threading.Lock()
shared_resource = []

def worker(item):
    with lock:
        shared_resource.append(item)
        print(f'Item {item} added.')

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

for t in threads:


    t.join()

print(f'Final shared resource: {shared_resource}')

在这个例子中,多个线程需要访问 shared_resource,这是一个共享的列表。使用 Lock 确保每个线程在访问和修改 shared_resource 时不会发生竞态条件。

结论

自定义多线程的传参方式在 Python 中有多种实现方式,最常见的就是通过 argskwargs 参数传递。同时,也可以使用更复杂的技术,如队列、线程本地数据和锁,来满足多线程应用中的参数传递和线程安全需求。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。