目录
-简述
~ 线程的概念
~ 引入线程的原因
~ 线程与进程的区别
~ Python 中的线程和 GIL
-创建线程
~ 通过 Thread 类
~ 通过自定义的线程类
-条件对象(Condition)
~ 常用方法及解析
~ 示例代码
-信号量对象(Semaphore)
~ 常用方法及解析
~ 示例代码
简述
线程的概念
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。如果把进程比作一个生产车间,那么线程就是车间里的一条生产线。一条线程指的是进程中一个单一顺序的控制流,一个进程中必定有一个线程(称为主线程),且可以并发多个线程,每条线程并行执行不同的任务。
同样是为了并行,为什么有了进程之后还要引入线程?
因为进程是资源的拥有者,创建、撤销和切换都存在较大的时空开销。而线程则只是执行单位,每一个线程所需要的资源都共享自它所在的进程,各种操作所需的开销就要小很多。有时候也把线程称为轻量级进程。
线程与进程的区别
- 线程共享创建它的进程的地址空间;进程具有自己的地址空间。
- 线程可以直接访问其所属进程的数据段;子进程具有其父进程数据段的副本。
- 线程可以直接与其所属进程中的其他线程通信;进程必须使用进程间通信与同级进程进行通信。
- 新线程很容易创建; 新进程需要复制父进程。
- 线程可以对同一进程的线程进行相当多的控制;进程只能控制子进程。
- 对主线程的更改(取消,优先级更改等)可能会影响该进程其他线程的行为;对父进程的更改不会影响子进程。
Python 中的线程和 GIL
与其他语言不同,由于 GIL(全局解释锁) 的存在,Python 中的线程不能实现并行机制。也就是说,Python 中的多线程不能利用多核的优势,即使你在一个进程中开启了多个线程,但在运行过程中,同一时刻只会有一个线程被运行。
不过呢,不能实现线程的并行机制并不是 Python 这门语言的锅,而且 Python 也完全可以实现这种机制,但由于时下最流行,最普遍的 Python 解释器 CPython 上有 GIL 这把锁,导致在 CPython 解释执行的代码在时同一时刻都只能运行一个线程。
GIL 并不是 Python 语言的特性。Python 可以完全不依赖 GIL ,如果想摆脱 GIL 只需要换一个解释器即可(如 JPython)。但那样的话你可能会面临更大的问题——许多现有的库不再被支持。
GIL 是加在 CPython 解释器 上的一把全局解释锁,它的作用是在解释器层面上确保线程安全,每个线程在执行时都会先获取GIL,这把锁会保证在同一时刻只有一个线程被运行,从而在解释器层面上保证线程安全(事实证明用这种方法保证线程安全会带来很大的弊端)。
虽不能并行,但在处理 IO 密集型任务时,Python 的多线程还是能有效提高执行效率的(通过 IO 阻塞或执行至一定时间时切换线程,从而提高 CPU 的利用率。)。对于计算密集型任务,多线程很不理想,因为这类任务需要持续使用CPU,但线程在达到执行时间的阈值之后会自动释放 CUP,这就造成了无意义的线程切换,反而造成了资源浪费。一般不建议用 Python 实现计算密集型任务,如果真的有这方面的需求,可以考虑使用 multiprocessing 模块,通过进程实现并行,或是直接换一种语言来实现。
创建线程
关于线程方面,Python 提供了 threading 模块,该模块实现了诸多方法和类来满足对线程的各种操作。
通过 Thread 类
Thread 类是 threading 模块中创建线程对象的类,通过对该类的实例化和其方法的调用,可以执行线程的创建、启动等操作。下面介绍一下 Thread 类
class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, daemon=None)
功能:创建一个线程对象
参数:
group:保留参数,用于以后扩展,现在没什么用
target:表示调用的对象(由 run() 方法调用),即线程要执行的任务
name:表示创建的线程的名字
args:表示调用对象的位置参数元组
kwargs:表示调用对象的关键字参数字典
daemon:守护线程标志,True表示该线程是守护线程,False 表示非守护。默认为继承创建该线程的线程守护模式
返回值:一个线程对象
属性
name:一个字符串,表示线程名,无语意,可重名
ident:线程标识符,线程未开始时为 None,开始后是一个非零整数,可通过 get_ident() 方法获取。线程标识符可被复用
native_id:一个非负整数(未启动时为 None),表示线程 ID,可在系统范围内唯一标识线程,线程终止后会被回收
说明:与进程ID相似,线程ID仅在创建线程到终止线程之间有效(确保系统范围内唯一)。
daemon:一个布尔值,表示守护线程标志。其值继承与创建线程
说明:必须在 start() 调用之前设置,否则会异常。主线程不是守护线程。当没有存活的非守护线程时,整个Python程序才会退出。
常用方法
start()
功能:开始线程活动,即开始执行 run() 方法
参数:无
返回值:None
说明:单个线程只能调用一次,否则引发异常
join(timeout=None)
功能:阻塞等待,直到线程结束或超时
参数:timeout是一个浮点数,表示阻塞的秒数,为默认值时表示阻塞至线程结束
返回值:None
说明 :判断是否超时在该方法后调用 is_alive() 才能判断
is_alive()
功能:判断线程是否存活
参数:无
返回值:存活为 True,否则返回 False
说明:在 run() 方法执行期间返回 True
还有 getName()、setName()、getDaemon()和setDaemon(),这些是旧的获取和设置线程名和守护状态的方法
通过自定义的线程类
自定义的线程类,其实就是定义一个 Thread 的子类。与自定义进程类一样,它也需要注意以下几点
- 该子类必须实现 run() 方法
- 子类只能覆盖 Thread 类中的 run() 和 __init __() 方法,其他的任何方法都不应该覆盖
- 如果重写了__init__方法,在执行其他任何操作之前,先调用父类的__init__方法
下面来举个栗子
import threading
import time
# 自定义的线程类
# class MyThread(threading.Thread):
# def __init__(self, name, func):
# super().__init__()
# self.name = name
# self.func = func
#
# def run(self) -> None:
# # self.func()
#
# print("这里是测试2")
# time.sleep(5)
def func():
print('这里是另一个线程')
time.sleep(2)
print('over')
if __name__ == "__main__":
my_Thread = threading.Thread(target=func, name='sonThread')
# my_Thread = MyThread(name='text', func=func)
my_Thread.setDaemon(True)
# 设置成守护线程
my_Thread.start()
# 启动线程
print(my_Thread.isDaemon())
# 返回线程是否是守护线程
time.sleep(5)
print('parent over')
PS:线程启动后会自动调用它的 run() 方法。在自定义线程类时,我们可以直接把线程要执行的任务写在 run() 方法中,也可以传入一个可调用对象,然后在 run() 中调用它。
线程间同步(锁)
线程中的 Lock 和 RLock 是与进程中的相差无几的,区别仅在于所在模块不同,所以这里不做多余的解释了。详情请参考上一篇博文:Python学习笔记 进程
条件对象(Condition)
条件对象总是与某种类型的锁相关联,看构造方法:class threading.Condition(lock=None)
,它可以通过手动传入一个锁,或者让它自动创建一个锁。条件对象将锁用于同步某些共享状态的权限,它允许一个或多个线程等待,直到被另一个线程唤醒。
常用方法及解析
acquire
说明:同于 lock.acquire(),这里的 lock 是指传入的参数,可以是 Lock 或 RLock
release()
说明:同于 lock.release(),这里的 lock 是指传入的参数,可以是 Lock 或 RLock
wait(timeout=None)
功能:等待,直到被唤醒或超时
参数:timeout 是一个浮点数,表示等待的时间,默认为直到被唤醒
返回值:超时返回 False,否则(被唤醒)返回 True
说明:这个方法释放底层锁,然后阻塞,直到在另外一个线程中调用同一个条件变量的 notify() 或 notify_all() 唤醒它,或者直到超时。一旦被唤醒或者超时,它重新获得锁并返回
wait_for(predicate, timeout=None)
功能:等待,直到条件计算为真或超时
参数:predicate 应该是一个返回可表示布尔值的可调用对象或判断式,timeout 表示最大等待时间
返回值:超时返回 False,否则返回 predicate 的返回值
说明:该方法会重复的调用 wait() 方法,直到满足判断是或发生超时,相当于如下代码
while not predicate():
cv.wait()
notify(n=1)
功能:唤醒最多 n 个等待这个条件的线程,如果没有线程等待,则相当于空操作
参数:n 表示唤醒线程的最大个数,默认为 1
返回值:None
说明:该方法必须在获取到底层锁的线程调用,否则会抛出 RuntimeError 异常;
另外,该方法不会释放锁,不会使本线程阻塞等待
notify_all()
功能:唤醒所有正在等待这个条件的线程
说明:与 notify() 类似
示例代码
import threading
import time
num, n = 0, 0
def cat():
global num, n
con.acquire()
while True:
if n == 2:
# 跳出循环并唤醒另一个线程,促使另一个线程也顺利结束,而不是无休止的运行
con.notify()
break
num += 1
print("双十一库存的香蕉数:%s" % str(num))
time.sleep(1)
if num >= 3:
print("差不多了,可以让顾客下单了")
con.notify()
# notify() 方法唤醒一个等待的线程,但它不会使本线程阻塞等待,不会释放锁
con.wait()
con.release()
# 释放锁
def shopaholic():
con.acquire()
global num, n
while True:
if n == 3:
print('还想下单?土你都要吃不起啦')
break
# 此条件用于跳出循环,从而使线程执行完毕
num -= 1
print("双十一香蕉库存数量:{}".format(num))
time.sleep(2)
if num == 0 and n != 3:
print("库存没了,店家赶紧去补充啊!")
con.notify()
# 唤醒另一个线程,执行完该句后,被唤醒的线程仍被阻塞,直到该线程获得锁
con.wait()
# 所以本线程需要释放锁,用 wait() 或 release() 方法
n += 1
con.release()
# wait() 方法释放锁,然后阻塞至被唤醒并获得锁,而 release() 方法只会释放锁,并继续往下执行
if __name__ == "__main__":
lock = threading.Lock()
con = threading.Condition(lock=lock)
thread1 = threading.Thread(target=cat)
thread2 = threading.Thread(target=shopaholic)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
信号量对象(Semaphore)
Semaphore 是一种同步锁,与 Lock 的区别是它可以允许多个线程访问同一资源。信号量对象的构造函数:class threading.Semaphore(value=1)
Semaphore 是实现信号量对象的类,信号量对象内部管理着一个原子性的计数器,计数器的值表示 release() 方法调用的次数减去 acquire() 的调用次数再加上一个初始值(即 value 的值)。如果需要, acquire() 方法将会阻塞直到可以返回而不会使得计数器变成负数。在没有显式给出 value 的值时,它默认为1。
简单来说,信号量对象总共允许 value 个线程同时访问资源,而内部计数器表示的是当前还可以加入的线程数,即计数器的值 = value - 当前的线程数。计数器为 0 时,则其他线程将阻塞等待,直到其值大于零才会有新的线程加入,这一点与进程池相似,即 出去一个进来一个。
常用方法及解析
acquire(blocking=True, timeout=None)
功能:阻塞或非阻塞的获取一个信号量,成功后计数器的值会减 1
参数:blocking 表示是否阻塞,默认值为 True
timeout 是一个浮点数,表示阻塞的秒数,若 blocking 为 False,则该参数会被忽略
返回值:获取成功返回 True,否则返回 False
release()
功能:释放一个信号量,使内部计数器加 1
参数:无
返回值:None
示例代码
import threading
import time
def waiter(num):
semaphore.acquire()
print('I am the waiter on the {}th.'.format(num))
time.sleep(2) # 因为线程执行时间一样,会同时结束,故看起来两个两个的输出
# time.sleep(num) # 若执行时间不同,就能看到一进一出的效果了
semaphore.release()
if __name__ == "__main__":
semaphore = threading.Semaphore(value=2)
lis = []
for i in range(10):
th = threading.Thread(target=waiter, args=(i,))
th.start()
lis.append(th)
for threads in lis:
threads.join()
事件对象(Event)
Event 是线程间通信的最简单机制之一:一个线程发出事件信号,而其他线程等待该信号,当收到信号时就执行相应的操作。
Event 是实现事件对象的类,事件对象内部管理着一个标志,调用 set() 方法可将其设为 True,当标志为 True 时,等待该标志的线程就会运行,当标志为 False 时,等待标志的线程就会阻塞等待,直到标志变为 True。
利用事件对象,我们可以在一个线程控制另一个线程的运行。(这让我想起了关键词 yield,利用这个关键词我们可以控制函数的分段运行)
常用方法及解析
is_set()
功能:获取标志状态
参数:无
返回值:当且仅当内部标志为 True 时 返回 True,否则返回False
set()
功能:将内部标志设置为 True
参数:无
返回值:None
说明:调用该方法后,所有等待这个事件的线程都将被唤醒,此时调用 wait() 方法的线程不会被阻塞
clear()
功能:将内部标志设置为 False
参数:无
返回值:None
说明:调用该方法后,调用 wait() 方法的线程将被阻塞
wait(timeout=None)
功能:阻塞线程直到内部标志为 True或超时
参数:timeout 是一个浮点数,表示阻塞的时间,默认无限制
返回值:None
说明:如果内部标志为 True,则该方法立刻返回,相当于空操作
示例代码
import threading
import time
def client():
print('肚子饿了,咱去找个地儿吃饭去')
print('来到了饭店门前')
event.set()
time.sleep(5)
print('吃饱了,推门而去')
event.set()
def waiter():
event.wait()
print('欢迎光临,客官您里边儿请')
event.clear()
event.wait()
print('谢谢惠顾,客官您慢走')
if __name__ == "__main__":
event = threading.Event()
th1 = threading.Thread(target=client,)
th2 = threading.Thread(target=waiter,)
lis = [th1, th2]
for threads in lis:
threads.start()
for threads in lis:
threads.join()
定时器对象(Timer)
了解一下就可以了
Timer 类表示一个操作应该在等待一定时间间隔之后运行,相当于一个定时器。Timer 类是 Thread 类的子类,所以它可以像一个自定义线程一样工作。
常用方法及解析
class threading.Timer(interval, function, args=[], kwargs={})
功能:创建一个定时器,在经历 interval 秒的间隔时间后,将用 参数 args h
和 关键字参数 kwargs 调用 function
参数:interval 表示计时时间,单位为秒
function 表示一个可调用对象
args 是可调用对象的位置参数列表,默认为空
kwargs 表示可调用对象的关键字参数字典
返回值:一个定时器对象
start() :启动定时器
cancel() :停止定时器并取消执行定时器将要执行的操作,仅在定时器仍处于等在状态是有效
threading 模块的一些函数
threading 模块提供了很多实用的方法,下面来介绍一下
active_count()
功能:返回当前存活的 Thread 对象的数量
参数:无
返回值:一个整型数,表示存活的线程数
说明:返回数等于 enumerate() 函数返回的列表长度
enumerate()
功能:以列表形式返回当前所有存活的 Thread 对象
参数:无
返回值:一个列表,其中元素是当前存活的 Thread 对象
说明:该列表包括守护线程,由 current_thread() 创建的虚拟线程对象和主线程,但不包括终止的线程和尚未启动的线程。
current_thread()
功能:返回与调用者的控制线程相对应的 Thread 对象,即获取表示当前线程的 Thread 对象。
参数:无
返回值:一个线程对象,表示调用该函数的线程
说明:如果调用者的控制线程不是通过 Thread 创建的,则返回功能受限的虚拟线程对象
main_thread()
功能:返回主线程对象
参数:无
返回值:一个线程对像
说明:正常情况下,主线程是启动 Python 解释器的线程
get_native_id()
功能:获取当前线程的 ID
参数:无
返回值:一个非负整数
说明:ID 可以在系统内唯一标示该线程,直到线程终止,ID 被 OS 回收。这是 3.8 版本的新功能
另,除了以上函数外,该模块还提供了excepthook()、get_ident()、settrace()等函数,详情请见官网