每一个程序的运行都是一个进程,而每个进程至少有一个线程,称之为主线程。举个例子:食堂吃饭是一个进程,为了让学生更快的吃上饭,我们会开多个窗口,每一个窗口就代表的是一个个的线程,如果只有一个窗口,就是单线程。
我们都知道程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。
进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
调度和切换:线程上下文切换比进程上下文切换要快得多。
在多线程操作系统中,进程不是一个可执行的实体。
threading.Thread()类
参数名 | 含义 |
---|---|
target | 线程调用的对象,就是目标函数 |
name | 为线程起个名字 |
args | 为目标函数传递实参,元祖 |
kwargs | 为目标函数关键字传参,字典 |
线程的启动
# 线程启动
import threading
# 最简单的线程程序
def worker():
print('I am working')
print('Fineshed')
t = threading.Thread(target=worker, name='worker') # 线程对象
t.start() # 启动
通过threading.Thread创建一个线程对象,target是目标函数,name可以指定名称。
但是线程没有启动,需要调用start方法。
线程之所以执行函数,是因为线程中就是执行代码的,而最简单的的封装就是函数,所以还是函数调用。
函数执行完,线程也就退出了。
线程的退出
Python没有提供线程退出的方法,线程在下面情况时退出:
1、线程函数内语句执行完毕
2、线程函数中抛出未处理的异常
# 线程的退出
import threading
import time
def worker():
count = 0
while True:
if count > 5:
break
time.sleep(2)
print("I'm working")
count += 1
t = threading.Thread(target=worker, name='worker') # 线程对象
t.start() # 启动
print('==End==')
# 输出结果
==End==
I'm working
I'm working
I'm working
I'm working
I'm working
I'm working
python的线程没有优先级、没有线程组的概念,也不能被销毁、停止、挂起,那也就没有恢复、中断了。
线程的传参
# 线程的传参
import threading
import time
def add(x,y):
print("{} + {} = {}".format(x, y, x+y, threading.current_thread().ident))
thread1 = threading.Thread(target=add, name='add', args=(4,5)) # 线程对象
thread1.start() # 启动线程
time.sleep(2)
thread2 = threading.Thread(target=add, name='add', args=(5,), kwargs={'y':4}) # 线程对象
thread2.start() # 启动线程
time.sleep(2)
thread3 = threading.Thread(target=add, name='add', kwargs={'x':4, 'y':5}) #线程对象
thread3.start()
线程传参和函数传参没什么区别,本质上就是函数传参。
threading的属性和方法
名称 | 含义 |
---|---|
name | 只是一个名字,一个标识,名称可以重名。getName(), setName()获取、设置这个名词 |
ident | 线程ID,它是非0整数。线程启动后才会有ID,否则为None。线程退出,此ID依旧可以访问。此ID可以重复使用 |
is_alive() | 返回线程是否活着 |
注意:线程的name,这是一个名称,可以重复;ID必须唯一,但可以在线程退出后再利用。
import threading
import time
def showthreadinfo():
print("currentthread = {}".format(threading.current_thread()))
print("main thread = {}".format(threading.main_thread()))
print("active count = {}".format(threading.active_count()))
def worker():
count = 0
showthreadinfo()
while True:
if (count > 5):
break
time.sleep(1)
count += 1
print("I'm working")
t = threading.Thread(target=worker, name='worker') # 线程对象
showthreadinfo()
t.start() # 启动
print('==End==')
# 输出:
currentthread = <_MainThread(MainThread, started 4320764736)>
main thread = <_MainThread(MainThread, started 4320764736)>
active count = 1
currentthread = <Thread(worker, started 123145549832192)>
==End==
main thread = <_MainThread(MainThread, stopped 4320764736)>
active count = 2
I'm working
I'm working
I'm working
I'm working
I'm working
I'm working
名称 | 含义 |
---|---|
start() | 启动线程。每一个线程必须且只能执行该方法一次 |
run() | 运行线程函数 |
start() 方法
import time
def worker():
count = 0
while True:
if (count > 5):
break
time.sleep(1)
count += 1
print("worker running")
class MyThread(threading.Thread):
def start(self):
print('start~~~~~~~~~~~')
super().start() # 调父类(就是Thread类)的start()
def run(self):
print('run~~~~~~~~~~~~~')
super().run() # 调父类的run方法
t = MyThread(name='worker', target=worker)
t.start()
#t.run()
# t.start()运行结果
start~~~~~~~~~~~
run~~~~~~~~~~~~~
worker running
worker running
worker running
worker running
worker running
worker running
# t.run()运行结果
run~~~~~~~~~~~~~
worker running
worker running
worker running
worker running
worker running
worker running
start()方法会调用run()方法,而run()方法可以运行函数
使用start方法启动线程,是启动了一个新的线程。但是使用run方法并没有启动新的线程,就是在主线程中调用了一个普通的函数而已。
因此,启动线程请使用start方法,才能启动多个线程。
多线程
一个进程中有多个线程,实现一种并发
import threading
import time
def worker():
count = 0
while True:
if (count > 3):
break
time.sleep(1)
count += 1
print("worker running")
print(threading.current_thread().name, threading.current_thread().ident)
class MyThread(threading.Thread):
def start(self):
print('start~~~~~~~~~~~')
super().start() # 调父类(就是Thread类)的start()
def run(self):
print('run~~~~~~~~~~~~~')
super().run() # 调父类的run方法
t1 = MyThread(name='worker1', target=worker)
t2 = MyThread(name='worker2', target=worker)
t1.start()
t2.start()
# 运行结果
start~~~~~~~~~~~
run~~~~~~~~~~~~~
start~~~~~~~~~~~
run~~~~~~~~~~~~~
worker running
worker1 123145457434624
worker running
worker2 123145462689792
worker running
worker running
worker1 123145457434624
worker2 123145462689792
worker running
worker running
worker2 123145462689792
worker1 123145457434624
worker running
worker running
worker1 123145457434624
worker2 123145462689792
线程安全
关于线程安全,有一个经典的“银行取钱”问题。从银行取钱的基本流程基本上可以分为如下几个步骤:
用户输入账户、密码,系统判断用户的账户、密码是否匹配。
用户输入取款金额。
系统判断账户余额是否大于取款金额。
如果余额大于取款金额,则取款成功;如果余额小于取款金额,则取款失败。
乍一看上去,这确实就是日常生活中的取款流程,这个流程没有任何问题。但一旦将这个流程放在多线程并发的场景下,就有可能出现问题。注意,此处说的是有可能,并不是说一定。也许你的程序运行了一百万次都没有出现问题,但没有出现问题并不等于没有问题!
按照上面的流程编写取款程序,井使用两个线程来模拟模拟两个人使用同一个账户井发取钱操作。此处忽略检查账户和密码的操作,仅仅模拟后面三步操作。下面先定义一个账户类,该账户类封装了账户编号和余额两个成员变量。
import threading
import time
class Account:
# 定义构造器
def __init__(self, account_no, balance):
# 封装账户编号、账户余额的两个成员变量
self.account_no = account_no
self.balance = balance
# 定义一个函数来模拟取钱操作
def draw(account, draw_amount):
# 账户余额大于取钱数目
if account.balance >= draw_amount:
# 吐出钞票
print(threading.current_thread().name\
+ "取钱成功!吐出钞票:" + str(draw_amount))
# time.sleep(0.001)
# 修改余额
account.balance -= draw_amount
print("\t余额为: " + str(account.balance))
else:
print(threading.current_thread().name\
+ "取钱失败!余额不足!")
# 创建一个账户
acct =Account("1234567" , 1000)
# 模拟两个线程对同一个账户取钱
threading.Thread(name='甲', target=draw , args=(acct , 800)).start()
threading.Thread(name='乙', target=draw , args=(acct , 800)).start()
先不要管程序中那行被注释掉的代码,上面程序是一个非常简单的取钱逻辑,这个取钱逻辑与实际的取钱操作也很相似。
多次运行上面程序,很有可能都会看到如下所示的错误结果。
>> 甲取钱成功!吐出钞票:800
乙方取钱成功!吐出钞票:800
余额为200
余额为-200
运行结果并不是银行所期望的结果(不过有可能看到正确的运行结果),这正是多线程编程突然出现的“偶然” 错误因为线程调度的不确定性。
假设系统线程调度器在注释代码处暂停,让另一个线程执行(为了强制暂停,只要取消程序中注释代码前的注释即可)。取消注释后,再次运行程序,将总可以看到如图 1 所示的错误结果。
问题出现了,账户余额只有 1000 元时取出了 1600 元,而且账户余额出现了负值,远不是银行所期望的结果。虽然上面程序是人为地使用 time.sleep(0.001) 来强制线程调度切换,但这种切换也是完全可能发生的(100000 次操作只要有 1 次出现了错误,那就是由编程错误引起的)。
同步锁(Lock)
之所以出现错误结果,是因为 run() 方法的方法体不具有线程安全性,程序中有两个并发线程在修改 Account 对象,而且系统恰好在注释代码处执行线程切换,切换到另一个修改 Account 对象的线程,所以就出现了问题。
为了解决这个问题,Python 的 threading 模块引入了锁(Lock)。threading 模块提供了 Lock 和 RLock 两个类,它们都提供了如下两个方法来加锁和释放锁:
- acquire(blocking=True, timeout=-1):请求对 Lock 或 RLock 加锁,其中 timeout 参数指定加锁多少秒。
- release():释放锁。
Lock 和 RLock 的区别如下:
- threading.Lock:它是一个基本的锁对象,每次只能锁定一次,其余的锁请求,需等待锁释放后才能获取。
- threading.RLock:它代表可重入锁(Reentrant Lock)。对于可重入锁,在同一个线程中可以对它进行多次锁定,也可以多次释放。如果使用 RLock,那么 acquire() 和 release() 方法必须成对出现。如果调用了 n 次 acquire() 加锁,则必须调用 n 次 release() 才能释放锁。
由此可见,RLock 锁具有可重入性。也就是说,同一个线程可以对已被加锁的 RLock 锁再次加锁,RLock 对象会维持一个计数器来追踪 acquire() 方法的嵌套调用,线程在每次调用 acquire() 加锁后,都必须显式调用 release() 方法来释放锁。所以,一段被锁保护的方法可以调用另一个被相同锁保护的方法。
Lock 是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程在开始访问共享资源之前应先请求获得 Lock 对象。当对共享资源访问完成后,程序释放对 Lock 对象的锁定。
在实现线程安全的控制中,比较常用的是 RLock。通常使用 RLock 的代码格式如下:
class X:
#定义需要保证线程安全的方法
def m () :
#加锁
self.lock.acquire()
try :
#需要保证线程安全的代码
#...方法体
#使用finally 块来保证释放锁
finally :
#修改完成,释放锁
self.lock.release()
使用 RLock 对象来控制线程安全,当加锁和释放锁出现在不同的作用范围内时,通常建议使用 finally 块来确保在必要时释放锁。
通过使用 Lock 对象可以非常方便地实现线程安全的类,线程安全的类具有如下特征:
该类的对象可以被多个线程安全地访问。
每个线程在调用该对象的任意方法之后,都将得到正确的结果。
每个线程在调用该对象的任意方法之后,该对象都依然保持合理的状态。
总的来说,不可变类总是线程安全的,因为它的对象状态不可改变;但可变对象需要额外的方法来保证其线程安全。例如,上面的 Account 就是一个可变类,它的 self.account_no和self._balance(为了更好地封装,将 balance 改名为 _balance)两个成员变量都可以被改变,当两个钱程同时修改 Account 对象的 self._balance 成员变量的值时,程序就出现了异常。下面将 Account 类对 self.balance 的访问设置成线程安全的,那么只需对修改 self.balance 的方法增加线程安全的控制即可。
将 Account 类改为如下形式,它就是线程安全的:
import threading
import time
class Account:
# 定义构造器
def __init__(self, account_no, balance):
# 封装账户编号、账户余额的两个成员变量
self.account_no = account_no
self._balance = balance
self.lock = threading.RLock()
# 因为账户余额不允许随便修改,所以只为self._balance提供getter方法
def getBalance(self):
return self._balance
# 提供一个线程安全的draw()方法来完成取钱操作
def draw(self, draw_amount):
# 加锁
self.lock.acquire()
try:
# 账户余额大于取钱数目
if self._balance >= draw_amount:
# 吐出钞票
print(threading.current_thread().name\
+ "取钱成功!吐出钞票:" + str(draw_amount))
time.sleep(0.001)
# 修改余额
self._balance -= draw_amount
print("\t余额为: " + str(self._balance))
else:
print(threading.current_thread().name\
+ "取钱失败!余额不足!")
finally:
# 修改完成,释放锁
self.lock.release()
上面程序中的定义了一个 RLock 对象。在程序中实现 draw() 方法时,进入该方法开始执行后立即请求对 RLock 对象加锁,当执行完 draw() 方法的取钱逻辑之后,程序使用 finally 块来确保释放锁。
程序中 RLock 对象作为同步锁,线程每次开始执行 draw() 方法修改 self.balance 时,都必须先对 RLock 对象加锁。当该线程完成对 self._balance 的修改,将要退出 draw() 方法时,则释放对 RLock 对象的锁定。这样的做法完全符合“加锁→修改→释放锁”的安全访问逻辑。
当一个线程在 draw() 方法中对 RLock 对象加锁之后,其他线程由于无法获取对 RLock 对象的锁定,因此它们同时执行 draw() 方法对 self._balance 进行修改。这意味着,并发线程在任意时刻只有一个线程可以进入修改共享资源的代码区(也被称为临界区),所以在同一时刻最多只有一个线程处于临界区内,从而保证了线程安全。
为了保证 Lock 对象能真正“锁定”它所管理的 Account 对象,程序会被编写成每个 Account 对象有一个对应的 Lock(就像一个房间有一个锁一样)。
上面的 Account 类增加了一个代表取钱的 draw() 方法,并使用 Lock 对象保证该 draw() 方法的线程安全,而且取消了 setBalance() 方法(避免程序直接修改 self._balance 成员变量),因此线程执行体只需调用 Account 对象的 draw() 方法即可执行取钱操作。
下面程序创建并启动了两个取钱线程:
import threading
import Account
# 定义一个函数来模拟取钱操作
def draw(account, draw_amount):
# 直接调用account对象的draw()方法来执行取钱操作
account.draw(draw_amount)
# 创建一个账户
acct = Account.Account("1234567" , 1000)
# 模拟两个线程对同一个账户取钱
threading.Thread(name='甲', target=draw , args=(acct , 800)).start()
threading.Thread(name='乙', target=draw , args=(acct , 800)).start()
上面程序中代表线程执行体的 draw() 函数无须自己实现取钱操作,而是直接调用 account 的 draw() 方法来执行取钱操作。由于 draw() 方法己经使用 RLock 对象实现了线程安全,因此上面程序就不会导致线程安全问题。
多次重复运行上面程序,总可以看到如下运行结果。
甲取钱成功!吐出钞票:800
余额为: 200
乙取钱失败!余额不足!
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采用如下策略:
不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。例如,上面 Account 类中的 account_no 实例变量就无须同步,所以程序只对 draw() 方法进行了同步控制。
如果可变类有两种运行环境,单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。在单线程环境中使用钱程不安全版本以保证性能,在多线程环境中使用线程安全版本。
daemon线程和non-daemon线程
进程靠线程执行代码,至少有一个主线程,其他线程是工作线程。主线程是第一个启动的线程。
父线程:如果线程A中启动了一个线程B,A就是B的父线程 子线程:B就是A的子线程
Python中,构造线程的时候,可以设置daemon属性,这个属性必须在start方法前设置好
源码:
# 在Thread的__init__方法中
if daemon is not None:
self._daemonic = daemon # 用户设定bool值
else:
self._daemonic = current_thread().daemon
self._ident = None
线程daemon属性,如果设定就是用户的设置,否则就取当前线程的daemon值。
主线程是non-daemon线程,即daemon = False
import time
import threading
def foo():
time.sleep(2)
for i in range(5):
print(i)
# 主线程是non-daemon线程
t = threading.Thread(target=foo, daemon=False)
t.start()
print('Main Thread Exiting')
# 输出结果
Main Thread Exiting
0
1
2
3
4
发现线程t依然执行,主线程已经执行完,但是一直等着线程t。
修改为t = threading.Thread(target=foo, daemon=True)后
# 输出结果
Main Thread Exiting
名称 | 含义 |
---|---|
daemon属性 | 表示线程是否是daemon线程,这个值必须在start()之前设置,否则引发RuntimeError异常 |
isDaemon() | 是否是daemon线程 |
setDaemon | 设置为daemon线程,必须在start方法之前设置 |
会发现程序立即结束了,根本没有等线程t。
名称 | 含义 |
---|---|
daemon属性 | 表示线程是否是daemon线程,这个值必须在start()之前设置,否则引发RuntimeError异常 |
isDaemon() | 是否是daemon线程 |
setDaemon | 设置为daemon线程,必须在start方法之前设置 |
总结:
- 线程具有一个daemon属性,可以显示设置为True或False,也可以不设置,则取默认值None。如果不设置daemon,就取当前线程的daemon来设置它。
- 主线程是non-daemon线程,即daemon=False。从主线程创建的所有线程的不设置daemon属性,则默认都是daemon=False,也就是non-daemon线程。
- Python程序在没有活着的non-daemon线程运行时退出,也就是剩下的只能是daemon线程,主线程才能退出,否则主线程就只能等待。
思考下面程序的输出:
import time
import threading
def bar():
time.sleep(5)
print('bar')
def foo():
for i in range(10):
print(i)
t = threading.Thread(target=bar, daemon=False)
t.start()
# 主线程是non-daemon线程
t = threading.Thread(target=foo, daemon=True)
t.start()
print('Main Thread Exiting')
# 输出结果
0
Main Thread Exiting
可以看到,并不会输出‘bar’这个字符串,进行修改:
time.sleep(2) # 在原先print上面加上这一句
print('Main Thread Exiting')
# 输出结果
0
1
2
3
4
5
6
7
8
9
Main Thread Exiting
bar
可以看到‘bar’字符串打印出来了
再看一个例子,看看主线程什么时候结束daemon线程
# 看看主线程何时结束daemon线程
import time
import threading
def foo(n):
for i in range(n):
print(i)
time.sleep(1)
t1 = threading.Thread(target=foo, args=(5,), daemon=True)
t1.start()
t2 = threading.Thread(target=foo, args=(10,), daemon=False)
t2.start()
time.sleep(2)
print('Main Thread Exiting')
# 输出结果
0
0
1
1
Main Thread Exiting
2
2
3
3
4
4
5
6
7
8
9
调换10和5看看效果
import time
import threading
def foo(n):
for i in range(n):
print(i)
time.sleep(1)
t1 = threading.Thread(target=foo, args=(10,), daemon=True) # 调换10和20看看效果
t1.start()
t2 = threading.Thread(target=foo, args=(5,), daemon=False)
t2.start()
time.sleep(2)
print('Main Thread Exiting')
# 输出结果
0
0
1
1
Main Thread Exiting
2
2
3
3
4
4
5
上例说明,如果有non-daemon线程的时候,主线程退出时,也不会杀掉所有的daemon线程,直到所有non-daemon线程全部结束,如果还有daemon线程,主线程需要退出,会结束所有daemon线程、退出。
join方法
mport time
import threading
def foo(n):
for i in range(n):
print(i)
time.sleep(1)
t1 = threading.Thread(target=foo, args=(10,), daemon=True)
t1.start()
t1.join()
print("Main Thread Exiting")
# 输出结果
0
1
2
3
4
5
6
7
8
9
Main Thread Exiting
然后取消join方法看一下结果:
# 输出结果
0
Main Thread Exiting
使用了join方法后,daemon线程执行完了,主线程才退出来。
join(timeout=None)是线程的标准方法之一。一个线程中调用另一个线程的join方法,调用者将被阻塞,直到被调用线程终止。一个线程可以被join多次。
timeout参数指定调用者多久,没有设置超时,就一直等到被调用线程结束。调用谁的join方法,就是join谁,就要等谁。
daemon线程应用场景
daemon thread的作用是:当你把一个线程设置为daemon,它会随主线程的退出而退出。
主要应用场景有:
- 后台任务。如:发送心跳包、监控,这种场景最多
- 主线程工作才有用的线程。如主线程中维护这公共的资源,主线程已经清理了,准备退出,而工作线程使用这些资源工作也没有意义了,一起退出最合适。
- 随时可以被终止的线程。如果主线程退出,想所有其他工作线程一起退出,就使用daemon=True来创建工作线程。
比如:开启一个线程定时判断WEB服务是否正常工作,主线程退出,工作线程也应该随着主线程退出一起退出。这种daemon线程一旦创建,就可以忘记它了,只看关系主线程什么时候退出就行了。简言之,daemon线程,简化了程序员手动关闭线程的工作。 - 如果在non-daemon线程A中,对另一个daemon线程B使用了join方法,这个线程B设置成daemon就没有什么意义了,因为non-daemon线程A总是要等待B。
- 如果在一个daemon线程C中,对另一个daemon线程D使用了join方法,只能说明C要等待D,主线程退出,C和D不管是否结束,也不管他们谁等谁,都要被杀掉
import time
import threading
def bar():
while True:
time.sleep(1)
print('bar')
def foo():
print("t1's daemon = {}".format(threading.current_thread().isDaemon()))
t2 = threading.Thread(target=bar)
t2.start()
print("t2's daemon = {}".format(t2.isDaemon()))
t1 = threading.Thread(target=foo, daemon=True)
t1.start()
time.sleep(3)
print("main thread exiting")
# 输出结果:
t1's daemon = True
t2's daemon = True
bar
bar
bar
main thread exiting
上例,只要主线程退出,2个工作线程都结束,可以使用join,让线程结束不了。
threading.local类
import threading
import time
def worker(): # 局部变量实现
x = 0
for i in range(100):
time.sleep(0.001)
x += 1
print(threading.current_thread(), x)
for i in range(10):
threading.Thread(target=worker).start()
# 输出结果
<Thread(Thread-1, started 7128)> 100
<Thread(Thread-2, started 12008)> 100
<Thread(Thread-3, started 11536)> 100
<Thread(Thread-4, started 9792)> 100
<Thread(Thread-5, started 11608)> 100
<Thread(Thread-7, started 2340)> 100
<Thread(Thread-6, started 12028)> 100
<Thread(Thread-8, started 12204)> 100
<Thread(Thread-9, started 11948)> 100
<Thread(Thread-10, started 11816)> 100
上例使用多线程,每个线程完成不同的计算任务。x是局部变量。
能否改造成使用全局变量完成?
import threading
import time
class A:
def __init__(self):
self.x = 0
global_data = A() # 全局变量
def worker():
global_data.x = 0
for i in range(100):
time.sleep(0.001)
global_data.x += 1
print(threading.current_thread(), global_data.x)
for i in range(10):
threading.Thread(target=worker).start()
# 输出结果
<Thread(Thread-2, started 11012)> 984
<Thread(Thread-4, started 11512)> 985
<Thread(Thread-3, started 9020)> 987
<Thread(Thread-5, started 9436)> 988
<Thread(Thread-1, started 11660)> 989
<Thread(Thread-7, started 8404)> 990
<Thread(Thread-8, started 7400)> 991
<Thread(Thread-6, started 8236)> 993
<Thread(Thread-10, started 4476)> 994
<Thread(Thread-9, started 11760)> 995
上例虽然使用了全局对象,但是线程之间互相干扰,导致了错误的结果。
能不能使用全局对象,还能保持每个线程使用不同的数据呢?Python提供了threading.local类,将这个实例得到一个全局对象,但是不同的线程使用这个对象存储的数据其他线程看不见。
import threading
import time
# 全局变量
global_data = threading.local()
def worker():
global_data.x = 0
for i in range(100):
time.sleep(0.001)
global_data.x += 1
print(threading.current_thread(), global_data.x)
for i in range(10):
threading.Thread(target=worker).start()
# 打印结果:
<Thread(Thread-3, started 5220)> 100
<Thread(Thread-7, started 4016)> 100
<Thread(Thread-4, started 11504)> 100
<Thread(Thread-2, started 10376)> 100
<Thread(Thread-1, started 11556)> 100
<Thread(Thread-8, started 8908)> 100
<Thread(Thread-6, started 4752)> 100
<Thread(Thread-10, started 9796)> 100
<Thread(Thread-5, started 10552)> 100
<Thread(Thread-9, started 10480)> 100
可以看到,结果显示和使用局部变量的结果一样。
再看一个threading.local的例子:
# threading.local例子
import threading
X = 'abc'
ctx = threading.local() # 注意这个对象所处的线程
ctx.x = 123
print(ctx, type(ctx), ctx.x)
def worker():
print(X)
print(ctx)
print(ctx.x)
print('working')
worker() # 普通函数调用
print()
threading.Thread(target=worker).start() # 另起一个线程
# 打印结果
<_thread._local object at 0x000000000299E570> <class '_thread._local'> 123
abc
<_thread._local object at 0x000000000299E570>
123
working
abc
<_thread._local object at 0x000000000299E570>
Exception in thread Thread-1:
Traceback (most recent call last):
File "C:\Users\pc\AppData\Local\Programs\Python\Python36\lib\threading.py", line 916, in _bootstrap_inner
self.run()
File "C:\Users\pc\AppData\Local\Programs\Python\Python36\lib\threading.py", line 864, in run
self._target(*self._args, **self._kwargs)
File "F:/PycharmProjects/0524_线程进程.py", line 127, in worker
print(ctx.x)
AttributeError: '_thread._local' object has no attribute 'x'
从运行结果来看,另起一个线程打印ctx.x出错了:
AttributeError: '_thread._local' object has no attribute 'x'
但是,ctx打印没有出错,说明能看到ctx,但是ctx中的x看不到,这个x不能跨线程。
threading.local类构建了一个大字典,其元素是每一线程实例的地址为key和线程对象引用线程单独的字典的映射,如下:
{ id(Thread) -> (ref(Thread), thread-local dict) }
通过threading.local实例就可在不同的线程中,安全地使用线程独有的数据,做到了线程间数据隔离,如同本地变量一样安全。
定时器Timer/延迟执行
threading.Timer继承自Thread,这个类用来定义多久执行一个函数。
class threading.Timer(interval, function, args=None, kwargs=None)
start方法执行之后,Timer对象会处于等待状态,等待了interval之后,开始执行function函数的。如果在执行函数之前的等待阶段,使用了cancel方法,就会跳过执行函数结束。
import threading
import logging
import time
FORMAT = '%(asctime)s %(threadName)s %(thread)d %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)
def worker():
logging.info('in worker')
time.sleep(2)
t = threading.Timer(5, worker)
t.setName('w1')
t.start() # 启动线程
print(threading.enumerate())
# 打印结果:
[<_MainThread(MainThread, started 7360)>, <Timer(w1, started 6428)>]
2018-05-27 15:40:13,706 w1 8052 in worker
在上面的基础上加入t.cancel()后:
t.cancel()
time.sleep(1)
print(threading.enumerate())
# 打印结果
[<_MainThread(MainThread, started 6660)>, <Timer(w1, started 7284)>]
[<_MainThread(MainThread, started 6660)>]
如果线程中worker函数已经开始执行,cancel就没有任何效果了。
总结:
Timer是线程Thread的子类,就是线程类,具有线程的能力和特征。
它的实例是能够延时执行目标函数的线程,在真正执行目标函数之前,都可以cancel它。
提前cancel:
import threading
import logging
import time
FORMAT = '%(asctime)s %(threadName)s %(thread)d %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)
def worker():
logging.info('in worker')
time.sleep(2)
t = threading.Timer(5, worker)
t.setName('w1')
t.cancel() # 提前取消
t.start() # 启动线程
print(threading.enumerate())
time.sleep(3)
print(threading.enumerate())
# 打印结果
[<_MainThread(MainThread, started 6684)>]
[<_MainThread(MainThread, started 6684)>]