Python语法速查: 20. 线程与并发
本篇索引
(1)线程基本概念
(2)threading模块
(3)线程间同步原语资源
(4)queue
(1)线程基本概念
当应用程序需要并发执行多个任务时,可以使用线程。多个线程(thread)同时运行在一个进程(process)的内部, 它们可以共享访问本进程内的全局变量数据和资源。各个线程之间的调度由操作系统负责, 具体做法是:给每个线程分配一个小的时间片,并在所有的线程之间循环切换。在具有多核的CPU上, 操作系统有时会安排尽可能使用每个CPU,从而并行执行线程。
并发编程的复杂性在于,多个线程可能同时更新一个数据,导致数据的损坏或不一致(术语叫做:竞争), 要解决这个问题,必须使用互斥锁或其他类似的同步手段保护这些数据。
Python解释器使用了内部的GIL(Global Interpreter Lock,全局解释器锁), 这限制了Python程序只能在一个处理器上运行。如果一个应用程序的大部分是I/O密集型的, 那么使用线程是没有问题的。而如果是CPU密集型的,那使用多个线程没任何好处,还会降低运行速度。 因此对于计算密集型的任务,最好使用C扩展模块或multiprocessing
模块来代替。 C扩展具有释放解释器锁和并行运行的选项,前提是释放锁时不与解释器进行交互。 multiprocessing
模块将工作分派给不受锁限制的单独子进程。
使用多线程编程还要注意一个问题:开的线程的数量级不能太大。例如,一台使用线程的网络服务器对于100个线程工作情况良好, 但如果增加到10000个线程,性能就会变得非常糟糕。因为每个线程都需要有自己的系统资源, 而且还会产生线程上下文切换、锁和其他相关开销,算下来是个不小的开销。 对这种I/O密集型应用,比较常见的做法是将程序编写为:异步事件处理机制的结构,比如“协程”。 使用异步和协程的方式编程,可以比较轻松地处理诸如10000个连接的情况。
没有任何方法可以强制终止或挂起其他线程!这是设计上的原因。因此,线程只能自己挂起或自己终止。
(2)threading模块
threading
模块提供Thread
类和各种同步原语,用于编写多线程的程序。 threading
模块可创建:Thread
对象、Timer
对象、 Lock
对象、RLock
对象等。
● Thread对象
Thread
类用于表示单独的控制线程,创建新线程的语法如下:
Thread(group=None, target=None, name=None, args=(), kwargs={})
此函数创建一个新的线程实例,target
是一个可调用对象(线程启动时,run()
方法将调用此对象), name
是线程名称,默认将创建一个名为 “Target-N” 格式的唯一名称。 args
、kwargs
是传递给target
函数的参数元组、参数字典。
Thread实例支持以下属性和方法
属性或方法 | 类型 | 说明 |
---|---|---|
name |
属性 | 线程名称,这个字符串用于唯一标识线程。 |
ident |
属性 | 整数线程标识符,如果线程尚未启动,它的值为None。 |
daemon |
属性 | 布尔值,True表示为后台线程。它的初始值从创建线程的线程继承而来。 主线程(控制线程)不是后台线程(主线程的daemon为 False)。 通常 Python 解释器退出之前,会等待所有线程终止。但不会等待daemon为 True的线程。 当主线程结束后,如果其他线程的的daemon都为True,Python解释器将不等待那些线程, 整个Python程序将即时退出。 |
t.start() | 方法 | 在主线程中,调用此方法启动线程。 |
t.run() | 方法 | 线程启动时,将自动调用此方法。它将调用先前创建线程对象时,由target 指定的目标函数。 可以在Thread的子类中,重新定义此方法。 |
t.join([timeout]) | 方法 | 在主线程中调用此方法,功能是等待直到线程实例 t 终止或超时为止。timeout 是一个浮点数, 单位为“秒”。在线程启动之前不能调用此函数,否则会报错。 |
t.is_alive() | 方法 | 如果线程是活动的,返回True,否则返回False。从start() 方法返回的那一刻开始, 线程就是活动的,直到它的run() 方法终止为止。 |
● 线程使用实例
通常 Python 解释器退出之前,会等待所有线程终止。但是,若将创建的线程的daemon
设置为 True, 会使解释器在主线程结束后立即退出程序,这些daemon
为 True的线程将即时被销毁。
下例中,线程 t 每5秒从后台激活激活运行一次;30秒后,主线程结束,整个Python程序退出:
import threading
import time def clock(interval):
while True:
print("The time is %s" % time.ctime())
time.sleep(interval) t = threading.Thread(target=clock, args=(5,))
t.daemon = True
t.start() time.sleep(30)
下例为将同一个线程定义为一个 Thread 的子类:
import threading
import time class ClockThread(threading.Thread):
def __init__(self, interval):
threading.Thread.__init__(self)
self.daemon = True
self.interval = interval
def run(self):
while True:
print("The time is %s" % time.ctime())
time.sleep(self.interval) t = ClockThread(5)
t.start() time.sleep(30)
● Timer对象
Timer
对象用于在稍后某个时间执行一个函数,调用语法如下:
Timer(interval, func [,args [,kwargs]])
此函数创建定时器对象,在interval
秒之后运行函数func
, args
、kwargs
是传递给func
函数的参数元组、参数字典。
Timer实例支持以下方法:
属性或方法 | 类型 | 说明 |
---|---|---|
t.start() | 方法 | 启动定时器,func 函数将在指定间隔时间后执行。 |
t.cancel() | 方法 | 如果函数尚未执行,取消定时器。 |
● 实用工具函数
函数 | 说明 |
---|---|
active_count() | 返回当前活动的Thread对象数量。 |
current_thread() | 返回调用者的线程对象。 |
enumerate() | 列出当前所有活动的Thread对象。 |
local() | 返回local对象,用于保存线程本地的数据。应该保证此对象在每个线程中是唯一的。 |
setprofile(func) | 设置一个配置文件函数,用于已创建的所有线程。 func 在每个线程开始运行之前被传递给 sys.setprofile()函数。 |
settrace(func) | 设置一个跟踪函数,用于已创建的所有线程。 func 在每个线程开始运行前被传递给 sys.settrace()函数。 |
stack_size([size]) | 返回创建新线程时使用的栈大小。可选整数参数size 表示创建新线程时使用的栈大小。 size 的值可以是 32768(32 KB) 或更大,而且是 4096 的倍数。如果系统上不支持此操作, 将引发 ThreadError 异常。 |
(3)线程间同步原语资源
● 锁(Lock)
锁(或称为“互斥锁”)有2个状态:已锁定、未锁定。如果锁处于已锁定状态,尝试获取锁的线程将被阻塞, 直到锁被释放为止。如果有多个线程等待获取锁,当锁被释放时,只有一个线程能获得它,具体哪个线程随机。 创建锁的语法如下,初始状态为“未锁定”:
threading.Lock()
Lock实例支持以下方法
实例方法 | 说明 |
---|---|
lock.acquire([blocking]) | 获取锁,成功则返回 True。blocking 参数默认为 True,当锁为已锁定时,则阻塞本线程。 若blocking 设为 False,当无法获取锁时,将立即返回 False,不阻塞。 |
lock.realease() | 释放一个锁,当锁处于“未锁定”状态时、或从与原本调用acquire() 方法的线程不同的线程调用此方法, 将出现错误。 |
● 可重入锁(RLock)
可重入锁(RLock
)类似于Lock
对象,但同一个线程可以多次获取它。 这允许拥有锁的线程执行嵌套的acquire()
和release()
操作。 在这种情况下,只有最外层的release()
操作才能将锁重置为“未锁定”状态。
创建可重入锁的语法如下:
threading.RLock()
RLock实例支持以下方法
实例方法 | 说明 |
---|---|
rlock.acquire([blocking]) | 获取锁,成功获取则返回 True,而且递归级别被设置为1。如果此线程已拥有锁, 则锁的递归级别加1,而且立即返回。blocking 的含义同上。 |
rlock.realease() | 通过减少锁的递归级别来释放它。如果在减值后递归基本为0,锁将被置位“未锁定”状态。 否则,所继续保持“已锁定”状态。只能由目前拥有锁的线程来调用此函数 |
● 信号量(Semaphore)
信号量是一个基于计数器的同步原语,每次调用acquire()
方法时此计数器减1, 每次调用release()
方法时此计数器加1。如果计数器为0,acquire()
方法将会阻塞。 直到其他线程调用release()
方法为止。
创建信号量对象的语法如下(value是计数器初始值,默认为1):
threading.Semaphore([value])
以下创建有边界的信号量对象(BoundedSemaphore),区别是release()操作的次数不能超过acquire()次数:
threading.BoundedSemaphore([value])
Semaphore实例支持以下方法
实例方法 | 说明 |
---|---|
s.acquire([blocking]) | 获取信号量。若计数值大于0,则减1,然后立即返回。 若计数值为0,此方法将阻塞,直到另一个线程调用release() 方法为止。 blocking 的含义同上。 |
s.realease() | 通过将内部计数器的值加1来释放一个信号量。如果计数值为0,而且另一线程在等待, 则该线程将被唤醒。 |
“信号量”和“互斥锁”之间的差别在于,信号量可用于发信号。例如:可以从不同线程调用 acquire()
和release()
方法,以便在生产者和消费者线程之间进行通信。 也可以用后面的“条件变量”来达成。
● 事件(Event)
“事件”用于线程间通信。一个线程发出事件,一个或多个其他线程等待它。 Event
实例内部管理着一个标志,可以用set()
方法将它设为True, clear()
方法设为False,wait()
方法将阻塞线程,知道标志为True。
创建事件的语法如下:
threading.Event()
Event实例支持以下方法
实例方法 | 说明 |
---|---|
e.is_set() | 只有当内部标志为True时才返回True。 |
e.set() | 将内部标志置为True。等待它变为True的所有线程都将被唤醒。 |
e.clear() | 将内部标志置为False。 |
e.wait([timeout]) | 阻塞直到内部标志为True。如果进入时内部标志为True,此方法立即返回。 timeout 是一个浮点数,单位为秒,指定超时期限。 |
“事件”不适合用于生产者-消费者问题,因为事件只有True和False两种状态, 处理过程中信号可能丢失。最好使用“条件变量”。
● 条件变量(Condition)
“条件变量”是另一种同步原语,典型用于生产者-消费者问题。
创建条件变量的语法如下,lock是可选的Lock或RLock实例,若缺省则创建新的RLock实例
threading.Condition([lock])
Condition实例支持以下方法
实例方法 | 说明 |
---|---|
cv.acquire(*args) | 获取底层锁。此方法将调用底层锁上的acquire(*args)方法。 |
cv.realease() | 释放底层锁。此方法将调用底层锁上对应的release()方法。 |
cv.wait([timeout]) | 等待直到获得通知或出现超时为止。此方法在调用线程已经获取锁之后调用。 调用时,将释放底层锁,而且线程将进入后随眠状态,直到另一个线程在条件变量上执行 notify() 或notifyAll() 将其唤醒为止。在线程被唤醒之后, 线程将重新获取锁,方法也会返回。 timeout 是一个浮点数,单位为秒,指定超时期限。 |
cv.notify([n]) | 唤醒一个或多个等待此条件变量的线程。此方法只在调用线程已获取条件变量内部锁之后调用。 如果没有正在等待的线程,它就什么也不做。n 指定要唤醒的线程数量,默认为1。 |
cv.notify_all() | 唤醒所有等待此条件的线程。 |
下面为使用条件变量的模板:
import threading cv = threading.Condition() def producer():
while True:
cv.acquire()
produce_item()
cv.notify()
cv.release() def consumer():
while True:
cv.acquire()
while not item_is_available():
cv.wait() # 等待直到有项出现
cv.release()
consume_item()
如果存在多个线程等待同一个条件,notify()
操作可能唤醒他们中的一个或多个。 因此某个线程被唤醒后,可能发现它等待的条件不存在了,所以在consumer()函数中使用while循环, 如果线程醒来,但是生成的项已经消失,它就会回去等待下一个信号。
● 使用线程间资源的注意点
使用以上Lock
等之类的线程间资源时,必须非常小心,依赖锁的代码应保证出现异常时正确地释放锁 否则可能导致死锁,典型的代码如下所示:
try:
lock.acquire()
# 关键部分
......
finally:
lock.release()
使用上下文管理协议(with),更加简洁:
with lock:
# 关键部分
......
另外,编写代码时,一般应该避免同时获取多个锁。
(4)queue
queue
模块实现了各种“多生产者-多消费者队列”,可用于在执行的多个线程间安全地交换信息。 一般来说,线程间通信最佳的方式就是使用queue
,者比前面的诸如“锁”之类的资源都要好用。
queue
模块定义了以下3种不同的队列类型,创建语法与说明如下:
创建函数 | 说明 |
---|---|
Queue([maxsize]) | 创建一个FIFO(先进先出)队列。maxsize 是队列中可放入项的最大数量, 缺省或置0则队列大小为无穷大。 |
LifoQueue([maxsize]) | 创建一个LIFO(后进先出)队列。(即:堆栈) |
PriorityQueue([maxsize]) | 创建一个优先级队列,使用这种队列时,项应该是(priority, data) 形式的元组, 其中priority 是一个数字,数字越大优先级越高。 |
队列支持以下实例方法:
实例方法 | 说明 |
---|---|
q.qsize() | 返回队列的大小,因为其他线程可能正在更新队列,此方法返回的数字可能不可靠。 |
q.empty() | 如果队列为空,则返回True,否则返回False。 |
q.full() | 如果队列为满,则返回True,否则返回False。 |
q.put(item [,block [,timeout]]) | 将item 放入队列,如果block 为True(默认), 则调用者将阻塞直到队列中出现可用的空闲位置为止;如block 为False, 队列满时将引发Full异常。timeout 提供可选的超时值,单位为秒, 如果超时则引发Full异常。 |
q.put_nowait(item) | 等价于q.put(item, False) 方法。 |
q.get([block [,timeout]]) | 从队列中取出一项返回。如果block 为True(默认), 则调用者将阻塞直到队列中出现可取出的项为止;如block 为False, 队列空时将引发Full异常。timeout 提供可选的超时值,单位为秒, 如果超时则引发Full异常。 |
q.get_nowait() | 等价于q.get(False) 方法。 |
q.task_done() | 队列的消费者用来指示对于项的处理已结束。如果使用此方法, 那么从队列中每取出一项都应该手动调用一次。 此方法主要是辅助q.join() 方法用的。 |
q.join() | 阻塞直到队列中所有的项均被取出和处理完为止。当为队列中的每一项都调用过了一次 q.task_done() 方法,此方法将会直接返回。 |
● 使用队列的示例
注意下例中q.task_done()
和q.join()
的用法, 它们将使得线程在处理完所有项后关闭。
import threading
from queue import Queue class WorkerThread(threading.Thread):
def __init__(self, *args, **kwargs):
threading.Thread.__init__(self, *args, **kwargs)
self.input_queue = Queue() def send(self, item):
self.input_queue.put(item) def close(self):
self.input_queue.put(None)
self.input_queue.join() def run(self):
while True:
item = self.input_queue.get()
if item is None:
break
# 处理项
print(item)
self.input_queue.task_done()
# 完成
self.input_queue.task_done()
return # 使用示例
w = WorkerThread()
w.start()
w.send('hello')
w.send('world')
w.close()
Python语法速查: 20. 线程与并发的更多相关文章
- Python语法速查: 7. 函数基础
返回目录 (1)函数基本 ● 函数是第一类对象 Python中万物皆对象,所有对象都是第一类的(first class),函数也不例外,也是第一类对象.既然是对象,那就可以当作普通的对象数据处理,比如 ...
- Python语法速查: 4. 字符串常用操作
返回目录 (1)字符串常用方法 Python3中,字符串全都用Unicode形式,所以省去了很多以前各种转换与声明的麻烦.字符串属于序列,所有序列可用的方法(比如切片等)都可用于字符串. 注意:字符串 ...
- Python语法速查:目录
1. 数据类型与内置函数 2. 列表.元组.字典.集合操作 3. 字符串格式化 4. 字符串常用操作 5. 运算符.math模块.表达式 6. 循环与迭代 7. 函数基础 8. 类与对象 9. 函数进 ...
- Python语法速查: 13. 操作系统服务
返回目录 本篇索引 (1)sys模块 (2)os模块 (3)与Windows相关模块 (4)subprocess模块 (5)signal模块 (1)sys模块 sys模块用于Python解释器及其环境 ...
- Python语法速查: 3. 字符串格式化
返回目录 (1)简易字符串格式化 字符串属于不可变序列,只能生成新的,不能改变旧的.“字符串格式化”有点像以前C语言的sprintf,可以将若干变量代入格式化的字符串,生成一个符合要求的新字符串. 转 ...
- Python语法速查: 15. 常用数据结构
返回目录 本篇索引 (1)array (2)bisect (3)deque (4)defaultdict (5)namedtuple (6)heapq (7)itertools (1)array ar ...
- Python语法速查: 12. 文件与输入输出
返回目录 (1)文件基本操作 ● 文件常用操作 内置函数或方法 描述 open(name [,mode [,buffering]]) 内置函数.用来打开文件,返回一个文件对象(file对象).详见下述 ...
- Python语法速查: 5. 运算符、math模块、表达式
返回目录 (1)一些较容易搞错的运算符 一般简单的如加减乘除之类的运算符就不写了,这里主要列些一些容易搞错或忘记的运算符.运算符不仅仅只有号,有一些英文单词如 in, and 之类,也是运算符,并不是 ...
- Python语法速查: 14. 测试与调优
返回目录 本篇索引 (1)测试的基本概念 (2)doctest模块 (3)unittest模块 (4)调试器和pdb模块 (5)程序探查 (6)调优与优化 (1)测试的基本概念 对程序的各个部分建立测 ...
随机推荐
- Implementing Recurrent Neural Network from Scratch
Reading CSV file... Parsed 79171 sentences. Found 65376 unique words tokens. Using vocabulary size 8 ...
- 【MySQL】库的操作
"SQL语言主要用于存取数据.查询数据.更新数据和管理关系数据库系统,SQL语言由IBM开发. SQL语言分为3种类型: DDL语句 数据库定义语言:数据库.表.视图.索引.存储过程,例如C ...
- NPC脚本界面自定义美化参数说明
觉得NPC对话界面太单调了 可以自己定义: 在[@main]下面加上 #ACT OPENMERCHANTBIGDLG 参数(WIL文件序号 图片序号 是否可以移动(0,1) 显示位置(0=左上角,1 ...
- Vue ElementUI Tree组件 回显问题(设置选择父级时会全选所有的子级,有此业务场景是不适合的)
业务场景下有这样的问题 业务需求需要保存前端 半选节点 解决方案 let checked = this.$refs.menuTree.getCheckedKeys(); //此方法获取半选节点 let ...
- Plastic Sprayers Manufacturer - The Basic Components Of A Spray Bottle
From cleaning to personal beauty, many people use spray bottles every day, but few people know how t ...
- Linux : file命令
file xxx file命令用来探测给定文件的类型.file命令对文件的检查分为文件系统.魔法幻数检查和语言检查3个过程 命令选项: -b:列出辨识结果时,不显示文件名称: -c:详细显示指令执行过 ...
- ZOJ1005 Jugs
题意:有两个容量互质的容器,需要用这两个容器量出目标重量的水,找到其中一组解.bfs,使得搜索得到的解是步数最少的,遍历前驱法输出路径~ #include<bits/stdc++.h> u ...
- 学习笔记(7)- 基于LSTM的对话模型
LSTM based Conversation Models 本文介绍一种会话语言模型,结合了局部.全局的上下文,以及参与者的角色. 问题提出者 倾向于用"任何人"."如 ...
- 浅谈SPFA——洛谷P1576 最小花费 题解
想找原题请点击这里:传送门 原题: 题目描述 在n个人中,某些人的银行账号之间可以互相转账.这些人之间转账的手续费各不相同.给定这些人之间转账时需要从转账金额里扣除百分之几的手续费,请问A最少需要多少 ...
- QWidget: “Must construct a QApplication before a QWidget”
最近在做一个关于Qt的项目,在debug版本中没有任何问题,所以就想看看在Release版本下的运行情况,结果在开始运行时,出现如下图1-1所示的错误.在网上搜索答案,大多数是关于QWidget: M ...