首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

使用 threading 编写多线程|Python 标准库

其实在 Python 中,多线程是不推荐使用的,除非明确不支持使用多进程的场景,否则的话,能用多进程就用多进程吧。写这篇文章的目的,可以对比多进程的文章来看,有很多相通的地方,看完也许会对并发编程有更好的理解。

GIL

Python(特指 CPython)的多线程的代码并不能利用多核的优势,而是通过著名的全局解释锁(GIL)来进行处理的。如果是一个计算型的任务,使用多线程 GIL 就会让多线程变慢。我们举个计算斐波那契数列的例子:

运行的结果你猜猜会怎么样?还不如不用多线程!

GIL 是必须的,这是 Python 设计的问题:Python 解释器是非线程安全的。这意味着当从线程内尝试安全的访问Python 对象的时候将有一个全局的强制锁。在任何时候,仅仅一个单一的线程能够获取 Python 对象或者 C API。每 100 个字节的 Python 指令解释器将重新获取锁,这(潜在的)阻塞了 I/O 操作。因为锁,CPU 密集型的代码使用线程库时,不会获得性能的提高(但是当它使用之后介绍的多进程库时,性能可以获得提高)。

那是不是由于 GIL 的存在,多线程库就是个「鸡肋」呢?当然不是。事实上我们平时会接触非常多的和网络通信或者数据输入/输出相关的程序,比如网络爬虫、文本处理等等。这时候由于网络情况和 I/O 的性能的限制,Python 解释器会等待读写数据的函数调用返回,这个时候就可以利用多线程库提高并发效率了。

线程对象

先说一个非常简单的方法,直接使用 Thread 来实例化目标函数,然后调用 来执行。

生成线程时可以传递参数给线程,什么类型的参数都可以。下面这个例子只传了一个数字:

还有一种创建线程的方法,通过继承 Thread 类,然后重写 方法,代码如下:

因为传递给 Thread 构造函数的参数 和 被保存成了带 前缀的私有变量,所以在子线程中访问不到,所以在自定义线程类中,要重新构造函数。

确定当前线程

每个 Thread 都有一个名称,可以使用默认值,也可以在创建线程时指定。

守护线程

默认情况下,在所有子线程退出之前,主程序不会退出。有些时候,启动后台线程运行而不阻止主程序退出是有用的,例如为监视工具生成“心跳”的任务。

要将线程标记为守护程序,在创建时传递 或调用,默认情况下,线程不是守护进程。

输出不包含守护线程的 ,因为在守护线程从 唤醒之前,其他线程,包括主程序都已经退出了。

如果想等守护线程完成工作,可以使用 方法。

输出信息已经包括守护线程的 。

默认情况下,无限期地阻止。也可以传一个浮点值,表示等待线程变为非活动状态的秒数。如果线程未在超时期限内完成,则无论如何都会返回。

由于传递的超时小于守护程序线程休眠的时间,因此 返回后线程仍处于“活动”状态。

枚举所有线程

方法可以返回活动 Thread 实例列表。由于该列表包括当前线程,并且由于加入当前线程会引入死锁情况,因此必须跳过它。

计时器线程

在延迟时间后开始工作,并且可以在该延迟时间段内的任何时间点取消。

此示例中的第二个计时器不会运行,并且第一个计时器似乎在主程序完成后运行的。由于它不是守护线程,因此在完成主线程时会隐式调用它。

同步机制

Semaphore

在多线程编程中,为了防止不同的线程同时对一个公用的资源(比如全部变量)进行修改,需要进行同时访问的数量(通常是 1)。信号量同步基于内部计数器,每调用一次 ,计数器减 1;每调用一次 ,计数器加 1。当计数器为 0 时, 调用被阻塞。

在这个例子中, 类只是为了展示在同一时刻,最多只有两个线程在运行。

Lock

Lock 也可以叫做互斥锁,其实相当于信号量为 1。我们先看一个不加锁的例子:

不加锁的情况下,结果会远远的小于 100。那我们加上互斥锁看看:

RLock

能够不被阻塞的被同一个线程调用多次。但是要注意的是 需要调用与 相同的次数才能释放锁。

先看一下使用 的情况:

在这种情况下,第二次调用将 被赋予零超时以防止它被阻塞,因为第一次调用已获得锁定。

再看看用替代的情况。

Condition

一个线程等待特定条件,而另一个线程发出特定条件满足的信号。最好说明的例子就是「生产者/消费者」模型:

可以看到生产者发送通知之后,消费者都收到了。

Event

一个线程发送/传递事件,另外的线程等待事件的触发。我们同样的用「生产者/消费者」模型的例子:

可以看到事件被 2 个消费者比较平均的接收并处理了。如果使用了 方法,线程就会等待我们设置事件,这也有助于保证任务的完成。

Queue

队列在并发开发中最常用的。我们借助「生产者/消费者」模式来理解:生产者把生产的「消息」放入队列,消费者从这个队列中对去对应的消息执行。

大家主要关心如下 4 个方法就好了:

put: 向队列中添加一个项。

get: 从队列中删除并返回一个项。

task_done: 当某一项任务完成时调用。

join: 阻塞直到所有的项目都被处理完。

这就是最简化的队列架构。

Queue 模块还自带了 PriorityQueue(带有优先级)和 LifoQueue(后进先出)2 种特殊队列。我们这里展示下线程安全的优先级队列的用法,PriorityQueue 要求我们 put 的数据的格式是,我们看看下面的例子:

其中消费者是故意让它执行的比生产者慢很多,为了节省篇幅,只随机产生 5 次随机结果。可以看到 put 时的数字是随机的,但是 get 的时候先从优先级更高(数字小表示优先级高)开始获取的。

线程池

面向对象开发中,大家知道创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。无节制的创建和销毁线程是一种极大的浪费。那我们可不可以把执行完任务的线程不销毁而重复利用呢?仿佛就是把这些线程放进一个池子,一方面我们可以控制同时工作的线程数量,一方面也避免了创建和销毁产生的开销。

线程池在标准库中其实是有体现的,只是在官方文章中基本没有被提及:

当然我们也可以自己实现一个:

线程池会保证同时提供 5 个线程工作,但是我们有 8 个待完成的任务,可以看到线程按顺序被循环利用了。

参考文章:

https://pymotw.com/3/threading/index.html

http://www.dongwm.com/archives/%E4%BD%BF%E7%94%A8Python%E8%BF%9B%E8%A1%8C%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B-%E7%BA%BF%E7%A8%8B%E7%AF%87/

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20201028A0IY9M00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券