翻译:老齐

译者注:与本文相关图书推荐:《Python大学实用教程》《跟老齐学Python:轻松入门》



本文将分两部分刊发。

第一部分


Python线程允许程序的不同部分同时运行,并可以简化设计。如果你对Python有一些经验,并且希望使用线程为程序加速,那么本文就是为你准备的!

什么是线程?

线程是一个独立的流,这意味着你的程序可以同时做两件事,但是,对于大多数Python程序,不同的线程实际上并不同时执行,它们只是看起来像是同时执行。

人们很容易认为线程是在程序上运行两个(或更多)不同的处理器,每个处理器同时执行一个独立的任务。这种看法大致正确,线程可能在不同的处理器上运行,但一个处理器一次只能运行一个线程。

要同时运行多个任务,不能用Python的标准方式实现,可以用不同的编程语言,或者多个进程实现,这样做的开发成本就高了。

由于用CPython实现了Python业务,线程可能不会加速所有任务,这是GIL(全称Global Interpreter Lock)的原因,一次只能运行一个Python线程。

如果某项任务需要花费大量时间等待外部事件,那么就可以应用多线程。如果是需要对CPU占用高并且花费很少时间等待外部事件,多线程可能枉费。

对于用Python编写并在标准CPython实现上运行的代码,这是正确的。如果你的线程是用C编写的,那么它们就能够释放GIL、并发运行。如果你在不同的Python实现上运行,也可以查看文档,了解它如何处理线程。

如果你正在运行一个标准的Python程序,只使用Python编写,并且有一个CPU受限的问题,那么你应该用多进程解决此问题。

将程序架构为使用线程也可以提高设计的清晰度。你将在下文中学习的大多数示例不一定会运行得更快,因为它们使用线程。在这些示例中使用线程有助于使设计更清晰、更易于推理。

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

创建一个线程

现在你已经知道了什么是线程,让我们来学习如何制作线程。Python标准库提供了线程模块threading,它包含了你将在本文中看到的大部分内容。在这个模块中,Thread是对线程的封装,提供了简单的实现接口。

要创建一个线程,需要创建Thread的实例,然后调用它的.start()方法:

import loggingimport threadingimport 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()

用函数thread_function()arg(1,)创建一个Thread实例。在本文中用整数作为线程的名称,threading.get_ident()可以返回线程的名称,但可读性较差。

thread_function()函数的作用不大,它只是记录一些日志消息,在这些消息之间加上time.sleep()

当你执行此程序时,输出将如下所示:

$ ./single_thread.pyMain : before creating threadMain : before running threadThread 1: startingMain : wait for the thread to finishMain : all doneThread 1: finishing

你会注意到代码的main部分结束之后,Thread才结束。后面会揭示这么做的原因。

守护线程

在计算机科学中,daemon是在后台运行的程序。

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

如果程序中正在执行的Threads不是daemons,则程序将在终止之前等待这些线程完成。然而,如果Threadsdaemons,当程序退出时,它们就终止了。

让我们更仔细地看看上面程序的输出,最后两行是有点意思的。当运行这个程序时,在__main__打印完all done后以及线程结束之前会暂停大约2秒。

这个暂停是Python等待非后台线程完成。当Python程序结束时,关闭操作是清除线程中的程序。

如果查看threading模块的源代码,你将看到threading._shutdown()方法,它会遍历所有正在运行的线程,并在每一个没有设置daemon标志的线程上调用.join()方法。

因此,程序在退出时会等待,因为线程本身正在sleep(time.sleep(2))中。一旦完成并打印了消息,.join() 将返回,程序才可以退出。

通常,这是你想要的,但是我们还有其他的选择。让我们首先使用一个daemon线程来重复这个程序。你可以修改Thread实例化时的参数,添加daemon=True:

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

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

$ ./daemon_thread.pyMain : before creating threadMain : before running threadThread 1: startingMain : wait for the thread to finishMain : all done

与前面不同的是,前面所输出的最后一行在这里没有了。thread_function()没有执行完,它是一个daemon线程,所以当_main__执行到达它的末尾时,程序结束,后台线程也就结束了。

线程实例的.join()方法

守护线程很方便,但是,如果要实现线程完全执行,而不是被迫退出,应该怎么办?现在让我们回到原始程序,看看注释掉的那一行:

# x.join()

要让一个线程等待另一个线程完成,可以调用.join()。取消对该行的注释,主线程将暂停并等待线程x,直到它运行结束。

你是否在程序中用守护线程或普通线程测试了这个问题?这并不重要。如果执行某个线程的.join()方法,该语句将一直等待,直到每个线程都完成。

使用多线程

到目前为止,示例代码只使用了两个线程:一个是主线程,另一个是以threading.Thread对象开始的线程。

通常,您会希望启动更多线程并让它们做一些有趣的工作。我们先来看看比复杂的方法,然后再看比较简单的方法。

启动多线程比较复杂的方法是你已经知道的:

import loggingimport threadingimport 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)

这段代码使用与上面看到的相同机制来启动线程,创建一个Thread实例对象,然后调用.start()。程序中生成一个由Thread实例组成的列表,后面再调用每个实例.join()方法。

多次运行此代码可能会产生一些有趣的结果。下面是我的机器的输出示例:

$ ./multiple_threads.pyMain : create and start thread 0.Thread 0: startingMain : create and start thread 1.Thread 1: startingMain : create and start thread 2.Thread 2: startingMain : before joining thread 0.Thread 2: finishingThread 1: finishingThread 0: finishingMain : thread 0 doneMain : before joining thread 1.Main : thread 1 doneMain : before joining thread 2.Main : thread 2 done

如果仔细检查输出,你将看到所有三个线程都按照你可能期望的顺序开始,但在本例中,它们是按照相反的顺序完成的!多次运行将产生不同的排序,可以通过查找Thread x: finishing消息来了解每个线程何时完成。

线程的运行顺序由操作系统决定,很难预测,它可能(而且很可能)因运行而异,因此在设计使用线程的算法时需要注意这一点。

幸运的是,Python提供了几个模块,你稍后将看到这些模块用来帮助协调线程并使它们一起运行。在此之前,让我们看看如何更简单地管理一组线程。

使用ThreadPoolExecutor

有一种比上面看到的更容易启动多线程的方法,它被称为ThreadPoolExecutor,是标准库中的concurrent.futures的一员(从Python3.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.pyThread 0: startingThread 1: startingThread 2: startingThread 1: finishingThread 0: finishingThread 2: finishing

同样,请注意Thread 1是在Thread 0之前完成的,线程执行顺序的调度是由操作系统完成的,所遵循的计划也不易理解。

(未完待续)

关注微信公众号:老齐教室。读深度文章,得精湛技艺,享绚丽人生。


python qthread 线程退出_线程:概念和实现相关推荐

  1. python线程退出_python子线程退出及线程退出控制的代码

    下面通过代码给大家介绍python子线程退出问题,具体内容如下所示: def thread_func(): while True: #do something #do something #do so ...

  2. threadpoolexecutor创建线程池_线程池ThreadPoolExecutor源码分析

    什么是线程池 创建线程要花费昂贵的资源和时间,如果任务来了才创建那么响应时间会变长,而且一个进程能创建的线程数量有限.为了避免这些问题,在程序启动的时候就创建若干线程来响应出来,它们被称为线程池,里面 ...

  3. 线程退出和线程资源回收问题

    最近项目中遇到循环8M的内存泄漏问题,排查后发现是由于特殊场景下使子线程异常退出,主线程发现子线程退出便再次创建线程,此时未回收退出线程的资源造成了内存泄漏,导致数据库宿主机oom,此问题不仅导致内存 ...

  4. python threading timer 退出_解决Python中定时任务线程无法自动退出的问题

    python的线程有一个类叫Timer可以,用来创建定时任务,但是它的问题是只能运行一次,如果要重复执行,则只能在任务中再调用一次timer,但这样就存在新的问题了,就是在主进程退出后,不能正常退出子 ...

  5. python 线程退出_python线程退出

    广告关闭 腾讯云11.11云上盛惠 ,精选热门产品助力上云,云服务器首年88元起,买的越多返的越多,最高返5000元! 如果某线程并未使用很多 io 操作, 它会在自己的时间片内一直占用处理器(和 g ...

  6. 线程打印_线程知识回顾

    1.程序_进程_线程 2.在 Java 中创建线程的两种方式 1) 继承 Thread 2) 实现 Runnable 接口 3.线程的生命周期 4.同步与死锁---过多的同步就会导致死锁 1) 同步方 ...

  7. educoder 使用线程锁(lock)实现线程同步_线程间的通信(一)

    这篇文章主要从4个角度来讲多线程间的通信: 使用wait/notify实现线程间的通信 生产者/消费者模式的实现 方法join的使用 ThreadLocal类的使用 等待/通知机制的实现: (1)wa ...

  8. postgres 支持的线程数_线程池被打满了怎么处理呢,你是否真的了解线程池?

    0.前言 线程池,顾名思义就是线程的池子,在每次需要取线程去执行任务的时候,没必要每次都创建新线程执行,线程池就是起着维护线程的作用,当有任务的时候就取出一个线程执行,如果任务执行完成则把线程放回到池 ...

  9. mysql 关闭线程池_线程池安全的关闭方式

    对于一些定时任务或者网络请求服务将会使用线程池,当应用停机时需要正确安全的关闭线程池,如果处理不当,可能造成数据丢失,业务请求结果不正确等问题. 关闭线程池我们可以选择什么都不做,JVM 关闭时自然的 ...

最新文章

  1. 使用 Firefox攻击Web2.0应用(二)
  2. flutter设置文本最大宽度
  3. mysql pool not open_安装 MariaDb 时报错:Could not open mysql.plugin table
  4. 基于局部均方差相关信息的图像去噪及其在实时磨皮美容算法中的应用
  5. 留学申请中,你们怎么老让我做科研啊?
  6. anychart说明文档
  7. python3ubunton安装视频_ubuntu16.04安装python3的包报错
  8. 心疼吗?被指是“傻X” 罗永浩深夜怒怼网友
  9. day 18 tupian
  10. Oracle数据泵导入导出dmp数据文件详细教程
  11. Word文件打开的时候需要输入密码?
  12. 安卓逆向_3 --- 篡改apk名称和图标、修改包名实现应用分身、修改资源去广告、去除re管理器广告...
  13. Windows10 笔记本从睡眠状态唤醒时取消输入密码的方法
  14. 独家:海尔消金“重仓”医美,月放款量惊人
  15. 留学地 各国比较2021
  16. Linux驱动学习9(同步/异步与阻塞/非阻塞的区别 )
  17. Jetson AGX Orin刷机教程,奶奶看完都说会了!
  18. 20%的SOLARIS知识解决80%的问题
  19. java的各种jar下载网址
  20. register_chrdev_region()和registe_chrdev()

热门文章

  1. ORA-32004错误的解决方法
  2. Compressive sensing for large images
  3. WCF4.0新特性体验(6):路由服务Routing Service(下)
  4. 谷歌将比特币现金汇率纳入货币信息搜索
  5. 绿色番薯 GHOST XP SP3 新春贺岁版
  6. 如何才能正确的关闭Socket连接
  7. Android 跨进程通信基础
  8. 在系统中使用read函数读取文件内容
  9. Spring mvc-kaptcha 验证码
  10. uni-app 页面数据无法渲染原因总结