开课城市

Python培训
美国上市Python培训机构

400-111-8989

Python培训

Python解读|什么是线程?如何使用线程?

  • 发布:Little monster
  • 来源:恋习Python
  • 时间:2019-04-22 17:08

今天我们为大家解读什么是线程?如何使用线程等相关内容,本文适合已经入门Python,并且想用线程来提升程序运行速度的Python工程师,一起来看看:

1. 什么是线程

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

在Python3中实现的大部分运行任务里,不同的线程实际上并没有同时运行:它们只是看起来像是同时运行的。

大家很容易认为线程化是在程序上运行两个(或多个)不同的处理器,每个处理器同时执行一个独立的任务。这种理解并不完全正确,线程可能会在不同的处理器上运行,但一次只能运行一个线程。

同时执行多个任务需要使用非标准的Python运行方式:用不同的语言编写一部分代码,或者使用多进程模块multiprocessing,但这么做会带来一些额外的开销。

由于Python默认的运行环境是CPython(C语言开发的Python),所以线程化可能不会提升所有任务的运行速度。这是因为和GIL(Global Interpreter Lock)的交互形成了限制:一次只能运行一个Python线程。

线程化的一般替代方法是:让各项任务花费大量时间等待外部事件。但问题是,如果想缩短等待时间,会需要大量的CPU计算,结果是程序的运行速度可能并不会提升。

当代码是用Python语言编写并在默认执行环境CPython上运行时,会出现这种情况。如果线程代码是用C语言写的,那它们就能够释放GIL并同时运行。如果是在别的Python执行环境(如IPython, PyPy,Jython, IronPython)上运行,请参考相关文档了解它们是如何处理线程的。

如果只用Python语言在默认的Python执行环境下运行,并且遇到CPU受限的问题,那就应该用多进程模块multiprocessing来解决。

在程序中使用线程也可以简化设计。本文中的大部分示例并不保证可以提升程序运行速度,其目的是使设计结构更加清晰、便于逻辑推理。

下面就来看看如何使用线程吧!

2. 创建线程

既然已经对什么是线程有了初步了解,下面让我们来学习如何创建一个线程。

Python标准库提供了threading模块,里面包含将在本文中介绍的大部分基本模块。在这个模块中,Thread类很好地封装了有关线程的子类,为我们提供了干净的接口来使用它们。

要启动一个线程,需要创建一个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")

查看日志语句,可以看到__main__部分正在创建并启动线程:

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

创建线程时,我们需要传递两个参数,第一个参数target是函数名,指定这个线程去哪个函数里面去执行代码,第二个参数args是一个元组类型,指定为这个函数传递的参数。在本例中,Thread运行函数thread_function(),并将1作为参数传递给该函数。

在本文中,我们用连续整数为线程命名。虽然threading.get_ident()方法能够为每一个线程生成唯一的名称,但这些名称通常会比较长,而且可读性差。

这里的thread_function()函数本身没做什么,它只是简单地记录了一些信息,并用time.sleep()隔开。

运行程序(注释掉倒数第二行代码),结果如下:

$ ./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

可以看到,线程Thread在__main__部分代码运行完后才结束。下一节会对这一现象做出解释,并讨论被注释掉那行代码。

2.1. 守护线程

在计算机科学中,守护进程daemon是一类在后台运行的特殊进程,用于执行特定的系统任务。

守护进程daemon在Python线程模块threading中有着特殊的含义。当程序退出时,守护线程将立即关闭。可以这么理解,守护线程是一个在后台运行,且不用费心去关闭它的线程,因为它会随程序自动关闭。

如果程序运行的线程是非守护线程,那么程序将等待所有线程结束后再终止。但如果运行的是守护线程,当程序退出时,守护线程会被自动杀死。

我们仔细研究一下上面程序运行的结果,注意看最后两行。当运行程序时,在__main__部分打印完all done信息后、线程结束前,有一个大约2秒的停顿。

这时,Python在等待非守护线程完成运行。当Python程序结束时,关闭过程的一部分是清理线程。

查看Python线程模块的源代码,可以看到thread ._shutdown()方法遍历所有正在运行的线程,并在每个非守护线程上调用.join()函数,检查它们是否已经结束运行。

因此,程序退出时需要等待,因为守护线程本身会在休眠中等待其他非守护线程运行结束。一旦thread ._shutdown()运行完毕并打印出信息,程序就可以退出。

守护线程这种自动退出的特性很实用,但其实还有其他的方法能实现相同的功能。我们先用守护线程重复运行一下上面的程序,看看结果。只需在创建线程时,添加参数daemon=True。

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

现在运行程序,结果如下:

$ ./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

添加参数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

添加参数daemon=True后

不同的地方是,之前输出的最后一行不见了,说明thread_function()函数没有机会完成运行。这是一个守护线程,所以当__main__部分运行完最后一行代码,程序终止,守护线程被杀死。

2.2. 加入一个线程

守护线程用起来很方便,但如果想让守护线程运行完毕后再结束程序该怎么办?或者想让守护线程运行完后不退出程序呢?

让我们来看一下刚刚注释掉的那行代码:

# x.join()

要让一个线程等待另一个线程完成,可以调用.join()函数。如果取消对这行代码的注释,主线程将会暂停,等待线程x完成运行。

这个功能在守护线程和非守护线程上同样适用。如果用.join()函数加入了一个线程,则主线程将一直等待,直到被加入的线程运行完成。

3. 多线程

到目前为止,示例代码中只用到了两个线程:主线程和一个threading.Thread线程对象。

通常,我们希望同时启动多个线程,让它们执行不同的任务。先来看看比较复杂的创建多线程的方法,然后再看简单的。

这个复杂的创建方法其实前面已经展示过了:

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")

    threads = list()
    for index in range(3):
        logging.info("Main    : create and start thread %d.", index)
        x = threading.Thread(target=thread_function, args=(index,))
        threads.append(x)
        x.start()

    for index, thread in enumerate(threads):
        logging.info("Main    : before joining thread %d.", index)
        thread.join()
        logging.info("Main    : thread %d done", index)

这段代码和前面提到的创建单线程时的结构是一样的,创建线程对象,然后调用.start()方法。程序中会保存一个包含多个线程对象的列表,为稍后使用.join()函数做准备。

多次运行这段代码可能会产生一些有趣的结果:

Main    : create and start thread 0.
Thread 0: starting
Main    : create and start thread 1.
Thread 1: starting
Main    : create and start thread 2.
Thread 2: starting
Main    : before joining thread 0.
Thread 2: finishing
Thread 1: finishing
Thread 0: finishing
Main    : thread 0 done
Main    : before joining thread 1.
Main    : thread 1 done
Main    : before joining thread 2.
Main    : thread 2 done

仔细看一下输出结果,三个线程都按照预想的顺序创建0,1,2,但它们的结束顺序却是相反的!多次运行将会生成不同的顺序。查看线程Thread x: finish中的信息,可以知道每个线程都在何时完成。

线程的运行顺序是由操作系统决定的,并且很难预测。很有可能每次运行所得到的顺序都不一样,所以在用线程设计算法时需要注意这一点。

幸运的是,Python中提供了几个基础模块,可以用来协调线程并让它们一起运行。在介绍这部分内容之前,让我们先看看如何更简单地创建一组线程。

4. 线程池

我们可以用一种更简单的方法来创建一组线程:线程池ThreadPoolExecutor,它是Python中concurrent.futures标准库的一部分。(Python 3.2 以上版本适用)。

最简单的方式是把它创建成上下文管理器,并使用with语句管理线程池的创建和销毁。

用ThreadPoolExecutor重写上例中的__main__部分,代码如下:

import concurrent.futures

# [rest of code]

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")

    with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
        executor.map(thread_function, range(3))

这段代码创建一个线程池ThreadPoolExecutor作为上下文管理器,并传入需要的工作线程数量。然后使用.map()遍历可迭代对象,本例中是range(3),每个对象生成池中的一个线程。

在with模块的结尾,会让线程池ThreadPoolExecutor对池中的每个线程调用.join()。强烈建议使用线程池ThreadPoolExecutor作为上下文管理器,因为这样就不会忘记写.join()。

注:

使用线程池ThreadPoolExecutor可能会报一些奇怪的错误。例如,调用一个没有参数的函数,但将参数传入.map()时,线程将抛出异常。

不幸的是,线程池ThreadPoolExecutor会隐藏该异常,程序会在没有任何输出的情况下终止。刚开始调试时,这会让人很头疼。

运行修改后的示例代码,结果如下:

$ ./executor.py
Thread 0: starting
Thread 1: starting
Thread 2: starting
Thread 1: finishing
Thread 0: finishing
Thread 2: finishing

再提醒一下,这里的线程1在线程0之前完成,这是因为线程的调度是由操作系统决定的,并不遵循一个特定的顺序。

感谢您的阅读,以上就是为您解读的什么是线程、如何操作使用线程等相关内容,你都学会了吗?更多Python相关的内容尽在达内Python培训机构官网,敬请关注!

免责声明:内容和图片源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

预约申请免费试听课

填写下面表单即可预约申请免费试听!怕钱不够?可就业挣钱后再付学费! 怕学不会?助教全程陪读,随时解惑!担心就业?一地学习,可全国推荐就业!

上一篇:听说这些Python知识,很少有人知道!
下一篇:Python的高级特征及用法(部分)

如何运用Python编程处理大数据?用Python编程处理大数据的技巧是什么?

Python面向对象编程的知识点都在这了!

Python的高级特征及用法(部分)

Python解读|什么是线程?如何使用线程?

选择城市和中心
黑龙江省

吉林省

河北省

湖南省

贵州省

云南省

广西省

海南省