Python多线程 threading
在很多程序设计问题中,都要求程序能够停下手头的工作,改为处理其他一些问题,再返回回来继续执行。可以通过多种途径达到这个目的。最开始的时候,那些掌握机器低级语言的程序员编写一些“中断服务例程”,主进程的暂停是通过硬件级的中断实现的。尽管这是一种有用的方法,但编出的程序很难移植,由此造成了另一类的代价高昂问题。中断对那些实时性很强的任务来说是很有必要的。但对于其他许多问题,只要求将问题划分进入独立运行的程序片断中,使整个程序能更迅速地响应用户的请求 。
最开始,线程只是用于分配单个处理器的处理时间的一种工具。但假如操作系统本身支持多个处理器,那么每个线程都可分配给一个不同的处理器,真正进入“并行运算”状态。从程序设计语言的角度看,多线程操作最有价值的特性之一就是程序员不必关心到底使用了多少个处理器。程序在逻辑意义上被分割为数个线程;假如机器本身安装了多个处理器,那么程序会运行得更快,毋需作出任何特殊的调校。根据前面的论述,大家可能感觉线程处理非常简单。但必须注意一个问题:共享资源!如果有多个线程同时运行,而且它们试图访问相同的资源,就会遇到一个问题。举个例子来说,两个线程不能将信息同时发送给一台打印机。为解决这个问题,对那些可共享的资源来说(比如打印机),它们在使用期间必须进入锁定状态。所以一个线程可将资源锁定,在完成了它的任务后,再解开(释放)这个锁,使其他线程可以接着使用同样的资源 。
多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的 。
threading
在Python中使用threading模块来进行多线程编程。threading中,线程只有两个状态,就绪和运行。
在单核cpu中,不存在真正的并行。多个线程看似是同时进行,其实是cpu使用很快的速度对多个线程逐个运行,每次运行一定的时间或者指令。
threading.Thread
主要参数 | 用法 |
---|---|
target | 目标函数 |
args | 函数参数 |
kwargs | 关键字传参 |
daemon | 是否是darmon线程 |
name | 线程名 |
Python中的线程比较简单,线程只有运行和结束状态。也无法主动终止或者挂起。一旦启动,除非发生异常或者主线程退出,线程都不会突然结束。
使用一个函数和参数来初始化threading.Thread类,并调用类方法start,就开启一个新线程了。
import threading
def add(x,y):
print(x + y)
t = threading.Thread(target=add, args=(4,5))
t.start()
调用start方法后,会创建一个新线程,并在新线程中调用类方法run。所以也可以通过继承threading.Thread类并重载run方法来实现多线程。
class MyThread(threading.Thread):
def __init__(self, x, y):
self.x = x
self.y = y
def run(self) -> None:
print(self.x + self.y)
daemon
-
Python中的daemon线程有别于Linux系统中的daemon进程
-
deamon属性是bool类型,只接受True和False
-
线程对象创建时,daemon的值默认会与创建对象的线程相同,主线程的默认值为False
主线程退出时所有线程都会退出。当主线程执行结束后,如果所有子线程中没有non-daemon线程(即daemon属性为False),主线程就会退出。如果有任意一个non-daemon的子线程,主线程就会处于stop状态等待直至所有non-daemon线程结束。
名称 | 用法 |
---|---|
daemon属性 | 需要在start()调用前修改否则引发RunTimeError |
isDaemon() | 是否为daemon线程 |
setDaemon | 同样需要在start()前设置 |
join
threading.Thread类有join方法
方法 | 参数 |
---|---|
join | timeout,阻塞到线程结束或者超过该时间 |
调用后当前线程会阻塞直至调用的线程对象对应的线程结束或者timeout为止。
在主线程末使用join也能达到non-daemon的效果
t = threading.Thread(target=add, args=(4, 5), daemon=True)
t.start()
#t.join()
while(t.is_alive()):
pass
使用while也能达到类似的效果
local
如果在多个线程中使用同一个全局变量,多个线程的修改就会相互影响,并且之间的影响是不可预测的。所以在threading模块中提供了一个local类隐性地为每个线程创建单独的变量。
用法:直接无参实例化一个threading.local对象,并在不同线程中给对象添加属性。
实现:
def __init__(self):
# The key used in the Thread objects' attribute dicts.
# We keep it a string for speed but make it unlikely to clash with
# a "real" attribute.
self.key = '_threading_local._localimpl.' + str(id(self))
# { id(Thread) -> (ref(Thread), thread-local dict) }
self.dicts = {}
local中使用线程对象的id来作为key构建dict,value为线程对象和线程字典组成的二元组,value中的dict存了在该线程给对象添加的所有属性。实际上在访问该对象属性时,访问的是各自的字典中的值,之间互不干扰。
注:很多时候使用局部变量能更方便的解决问题。
Timer类
threading.Timer类再start后不会马上执行,会在延迟时间后再执行,并且在执行前可以通过cancel方法来取消。
用法:与Thread相同,第一个位置参数为延迟时间。
线程安全
由于操作系统不存在真正的并行,每个线程都是执行一段时间就中止执行其他线程。当这些之间存在相互干扰的话,就会出现问题。例如,一个计算过程执行了一半,另外一个线程把其中的一个值给改了,这就会导致意料之外的结果,这样的操作是线程不安全的。
import threading
def foo():
for i in range(100):
print(i)
for i in range(5):
t = threading.Thread(target=foo)
t.start()
# result
#85
#63
#878668
在上述结果中,会出现多个结果在同一行,同时空多行的打印情况。可见print是不安全的,线程的中断影响到了打印效果。
解决线程安全问题的方法有多种,可以使用一个标志变量来设锁,当有一个线程进入打印时,上锁让其他线程等待。但是设锁是有性能代价的,需要具体需求来定。