Python开发——Python 线程入门

An Intro to Threading in Python – Real Python        

1. 什么是线程?

        线程是一个独立的执行流程。这意味着您的程序将有两件事情同时发生。但对于大多数 Python 3 实现来说,不同的线程实际上并不是同时执行的:它们只是看起来是这样。

        人们很容易把线程想象成程序上运行着两个(或多个)不同的处理器,每个处理器同时执行一项独立的任务。这几乎是对的。线程可能运行在不同的处理器上,但它们一次只能运行一个。

        要同时运行多个任务,需要使用非标准的 Python 实现,用不同的语言编写部分代码,或者使用多进程,而多进程会带来一些额外的开销。

        由于 CPython 实现 Python 的工作方式,线程可能无法加快所有任务的速度。这是由于与 GIL 的交互作用,基本上每次只能运行一个 Python 线程。

        大部分时间都在等待外部事件的任务一般都适合使用线程。而那些需要大量 CPU 计算且等待外部事件的时间很少的问题,运行速度可能根本不会快。

        这适用于用 Python 编写并在标准 CPython 实现上运行的代码。如果您的线程是用 C 语言编写的,它们可以释放 GIL 并同时运行。如果您在不同的 Python 实现上运行,请查阅文档,了解它是如何处理线程的。

        如果您运行的是标准 Python 实现,只用 Python 编写,并且有 CPU 限制的问题,那么您应该选择多处理模块。

        将程序架构为使用线程也能提高设计的清晰度。您将在本教程中学到的大多数示例并不一定会因为使用了线程而运行得更快。在这些示例中使用线程有助于使设计更简洁、更易于推理。

        所以,让我们停止谈论线程,开始使用它吧!

 2. 开始线程

        现在您已经知道了线程是什么,让我们来学习如何创建一个线程。Python 标准库提供了线程,它包含了你在本文中将看到的大多数基元。本模块中的 Thread 很好地封装了线程,提供了一个简洁的接口来使用它们。

        要启动一个单独的线程,需要创建一个线程实例,然后告诉它 .start():

import logging

import threading

import time



def thread_function(name):

    logging.info("Thread %s: starting", name)

    time.sleep(2)

    logging.info("Thread %s: finishing", name)



if __name__ == "__main__":

    format = "%(asctime)s: %(message)s"

    logging.basicConfig(format=format, level=logging.INFO,

                        datefmt="%H:%M:%S")



    logging.info("Main    : before creating thread")

    x = threading.Thread(target=thread_function, args=(1,))

    logging.info("Main    : before running thread")

    x.start()

    logging.info("Main    : wait for the thread to finish")

     x.join()

    logging.info("Main    : all done")

        如果查看一下日志语句,就会发现主要部分是创建和启动线程:

x = threading.Thread(target=thread_function, args=(1,))

x.start()

        创建线程时,你会向它传递一个函数和一个包含该函数参数的列表。在本例中,你告诉线程运行 thread_function(),并将 1 作为参数传递给它。

        本文将使用顺序整数作为线程的名称。threading.get_ident()可以为每个线程返回一个唯一的名称,但这些名称通常既不简短也不易读。

        thread_function() 本身并没有做什么。它只是简单地记录一些信息,并在信息之间加入 time.sleep()。

        运行该程序(注释掉第 20 行)时,输出结果如下:

$ ./single_thread.py

Main    : before creating thread

Main    : before running thread

Thread 1: starting

Main    : wait for the thread to finish

Main    : all done

Thread 1: finishing

        你会注意到,线程在代码的 Main 部分结束后才结束。在下一节中,我们将回过头来讨论为什么会这样,并谈谈神秘的第 20 行。

3. 守护进程线程

        在计算机科学中,守护进程是在后台运行的进程。

        Python 线程对守护进程有更具体的含义。守护线程会在程序退出时立即关闭。思考这些定义的一种方法是,将守护线程视为一个在后台运行的线程,而不必担心关闭它。

        如果程序运行的线程不是守护进程,那么程序会等待这些线程完成后才终止。而作为守护进程的线程则会在程序退出时被杀死。

        让我们再仔细看看上面程序的输出结果。最后两行是最有趣的部分。当你运行程序时,你会发现在 __main__ 打印出 all done 消息后,在线程结束前有一个停顿(约 2 秒)。

        这个停顿是 Python 在等待非调用线程完成。当 Python 程序结束时,关闭过程的一部分就是清理线程例程。

        如果您查看 Python 线程的源代码,就会发现 threading._shutdown() 会遍历所有正在运行的线程,并对每一个没有设置守护进程标志的线程调用 .join()。

        因此,程序等待退出是因为线程本身处于休眠状态。一旦完成并打印了信息,.join() 就会返回,程序就可以退出了。

        通常,这种行为是你想要的,但我们还有其他选择。首先,让我们用守护进程线程重复该程序。方法是改变线程的构造,添加 daemon=True 标志:

x = threading.Thread(target=thread_function, args=(1,), daemon=True)

        现在运行程序,应该会看到以下输出:

$ ./daemon_thread.py

Main    : before creating thread

Main    : before running thread

Thread 1: starting

Main    : wait for the thread to finish

Main    : all done

        这里的区别在于输出的最后一行不见了。 thread_function() 没有机会完成。它是一个守护线程,所以当 __main__ 的代码结束,程序想要结束时,守护线程就被杀死了。

4. 线程的使用场景

        线程非常适合处理以下类型的任务:

        1. I/O 密集型任务:例如文件读写、网络请求等。这类任务通常涉及大量的等待时间,线程可以在等待期间执行其他任务,从而提高效率。

        2. 后台任务:例如日志记录、监控系统状态、处理定时任务等。这类任务通常不需要立即的响应,可以在后台异步处理。

5. 高级线程操作

5.1 使用线程池

        在处理大量线程时,直接管理每个线程可能会变得复杂。此时,使用线程池(ThreadPoolExecutor)可以简化管理。

from concurrent.futures import ThreadPoolExecutor

import logging

import threading

import time



def thread_function(name):

    logging.info("Thread %s: starting", name)

    time.sleep(2)

    logging.info("Thread %s: finishing", name)



if __name__ == "__main__":

    format = "%(asctime)s: %(message)s"

    logging.basicConfig(format=format, level=logging.INFO,

                        datefmt="%H:%M:%S")



    logging.info("Main    : before creating thread pool")

    with ThreadPoolExecutor(max_workers=3) as executor:

        for i in range(5):

            executor.submit(thread_function, i)

    logging.info("Main    : all done")

        在这个例子中,ThreadPoolExecutor 创建了一个包含最多 3 个工作线程的线程池,并提交了 5 个任务。线程池会自动管理线程的生命周期和任务调度。

5.2 线程同步

        在线程之间共享数据时,需要注意线程安全问题。Python 提供了多种同步机制,例如锁(Lock)、信号量(Semaphore)和条件变量(Condition)。

        锁(Lock)

import threading



lock = threading.Lock()

shared_resource = 0



def thread_safe_increment():

    global shared_resource

    with lock:

        shared_resource += 1



threads = []

for _ in range(5):

    t = threading.Thread(target=thread_safe_increment)

    threads.append(t)

    t.start()



for t in threads:

    t.join()



print(shared_resource)

        信号量(Semaphore)

        信号量适用于需要限制同时访问资源的线程数量的场景。

import threading

import time



semaphore = threading.Semaphore(3)



def access_resource(name):

    with semaphore:

        print(f"{name} is accessing the resource")

        time.sleep(2)

        print(f"{name} is releasing the resource")



threads = []

for i in range(5):

    t = threading.Thread(target=access_resource, args=(f"Thread-{i}",))

    threads.append(t)

    t.start()



for t in threads:

    t.join()

5.3 线程间通信

        Python 提供了队列(Queue)来实现线程之间的安全通信。

import threading

import queue

import time



q = queue.Queue()



def producer():

    for i in range(5):

        print(f"Producing {i}")

        q.put(i)

        time.sleep(1)



def consumer():

    while True:

        item = q.get()

        if item is None:

            break

        print(f"Consuming {item}")

        time.sleep(2)



t1 = threading.Thread(target=producer)

t2 = threading.Thread(target=consumer)



t1.start()

t2.start()



t1.join()

q.put(None)

t2.join()

        在这个例子中,生产者线程将数据放入队列,而消费者线程从队列中取出数据进行处理。

6. 多线程的注意事项

        1. 避免死锁:当多个线程互相等待对方释放资源时,会导致程序卡死。

        2. GIL 的限制:由于 Python 的全局解释器锁(GIL),多线程并不能真正并行执行 CPU 密集型任务。可以考虑使用多进程来绕过这个限制。

        3. 资源清理:确保在程序结束时正确关闭和清理线程。

7. 结论

        通过以上的介绍和示例,相信您已经对 Python 中的线程有了更深入的理解。线程在处理 I/O 密集型任务和后台任务时非常有用,但在使用时需要注意线程同步和通信等问题,以避免线程安全问题。

猜你喜欢

转载自blog.csdn.net/weixin_65190179/article/details/139424824