1 默认启动主线程
一般的,程序默认执行只在一个线程,这个线程称为主线程,例子演示如下:
导入线程相关的模块 threading
:
import threading
threading的类方法 current_thread()
返回当前线程:
t = threading.current_thread()
print(t) # <_MainThread(MainThread, started 139908235814720)>
所以,验证了程序默认是在MainThead
中执行。
t.getName()
获得这个线程的名字,其他常用方法,getName()
获得线程id
,isAlive()
判断线程是否存活等。
print(t.getName()) # MainThread
print(t.ident) # 139908235814720
print(t.isAlive()) # True
以上这些仅是介绍多线程的背景知识
,因为到目前为止,我们有且仅有一个"干活"的主线程
2 创建线程
创建一个线程:
my_thread = threading.Thread()
创建一个名称为my_thread
的线程:
my_thread = threading.Thread(name='my_thread')
创建线程的目的是告诉它帮助我们做些什么,做些什么通过参数target
传入,参数类型为callable
,函数就是可调用的:
def print_i(i):
print('打印i:%d'%(i,))
my_thread = threading.Thread(target=print_i,args=(1,))
my_thread
线程已经全副武装,但是我们得按下发射按钮,启动start(),它才开始真正起飞。
my_thread().start()
打印结果如下,其中args
指定函数print_i
需要的参数i,类型为元祖。
打印i:1
至此,多线程相关的核心知识点,已经总结完毕。但是,仅仅知道这些,还不够!光纸上谈兵,当然远远不够。
接下来,聊聊应用多线程编程,最本质的一些东西。
3 交替获得CPU时间片
为了更好解释,假定计算机是单核的,尽管对于cpython
,这个假定有些多余。
开辟3个线程,装到threads
中:
import time
from datetime import datetime
import threading
def print_time():
for _ in range(5): # 在每个线程中打印5次
time.sleep(0.1) # 模拟打印前的相关处理逻辑
print('当前线程%s,打印结束时间为:%s'%(threading.current_thread().getName(),datetime.today()))
threads = [threading.Thread(name='t%d'%(i,),target=print_time) for i in range(3)]
启动3个线程:
[t.start() for t in threads]
打印结果如下,t0
,t1
,t2
三个线程,根据操作系统的调度算法,轮询获得CPU时间片,注意观察,t2
线程可能被连续调度,从而获得时间片。
当前线程t0,打印结束时间为:2020-01-12 02:27:15.705235
当前线程t1,打印结束时间为:2020-01-12 02:27:15.705402
当前线程t2,打印结束时间为:2020-01-12 02:27:15.705687
当前线程t0,打印结束时间为:2020-01-12 02:27:15.805767
当前线程t1,打印结束时间为:2020-01-12 02:27:15.805886
当前线程t2,打印结束时间为:2020-01-12 02:27:15.806044
当前线程t0,打印结束时间为:2020-01-12 02:27:15.906200
当前线程t2,打印结束时间为:2020-01-12 02:27:15.906320
当前线程t1,打印结束时间为:2020-01-12 02:27:15.906433
当前线程t0,打印结束时间为:2020-01-12 02:27:16.006581
当前线程t1,打印结束时间为:2020-01-12 02:27:16.006766
当前线程t2,打印结束时间为:2020-01-12 02:27:16.007006
当前线程t2,打印结束时间为:2020-01-12 02:27:16.107564
当前线程t0,打印结束时间为:2020-01-12 02:27:16.107290
当前线程t1,打印结束时间为:2020-01-12 02:27:16.107741
4 多线程抢夺同一个变量
多线程编程,存在抢夺同一个变量的问题。
比如下面例子,创建的10个线程同时竞争全局变量a
:
import threading
a = 0
def add1():
global a
a += 1
print('%s adds a to 1: %d'%(threading.current_thread().getName(),a))
threads = [threading.Thread(name='t%d'%(i,),target=add1) for i in range(10)]
[t.start() for t in threads]
执行结果:
t0 adds a to 1: 1
t1 adds a to 1: 2
t2 adds a to 1: 3
t3 adds a to 1: 4
t4 adds a to 1: 5
t5 adds a to 1: 6
t6 adds a to 1: 7
t7 adds a to 1: 8
t8 adds a to 1: 9
t9 adds a to 1: 10
结果一切正常,每个线程执行一次,把a
的值加1,最后a
变为10,一切正常。
运行上面代码十几遍,一切也都正常。
所以,我们能下结论:这段代码是线程安全的吗?
NO!
多线程中,只要存在同时读取和修改一个全局变量的情况,如果不采取其他措施,就一定不是线程安全的。
尽管,有时,某些情况的资源竞争,暴露出问题的概率极低极低
:
但是在本例中,a = a + 1
这种修改操作,花费的时间太短了,短到我们无法想象。所以,线程间轮询执行时,都能get到最新的a值。所以,暴露问题的概率就变得微乎其微。
5 代码稍作改动,叫问题暴露出来
只要弄明白问题暴露的原因,叫问题出现还是不困难的。
想象数据库的写入操作,一般需要耗费我们可以感知的时间。
为了模拟这个写入动作,简化期间,我们只需要延长修改变量a
的时间,问题很容易就会还原出来。
import threading
import time
a = 0
def add1():
global a
tmp = a + 1
time.sleep(0.2) # 延时0.2秒,模拟写入所需时间
a = tmp
print('%s adds a to 1: %d'%(threading.current_thread().getName(),a))
threads = [threading.Thread(name='t%d'%(i,),target=add1) for i in range(10)]
[t.start() for t in threads]
重新运行代码,只需一次,问题立马完全暴露,结果如下:
t0 adds a to 1: 1
t1 adds a to 1: 1
t2 adds a to 1: 1
t3 adds a to 1: 1
t4 adds a to 1: 1
t5 adds a to 1: 1
t7 adds a to 1: 1
t6 adds a to 1: 1
t8 adds a to 1: 1
t9 adds a to 1: 1
看到,10个线程全部运行后,a
的值只相当于一个线程执行的结果。
下面分析,为什么会出现上面的结果:
以上最核心的三行代码:
tmp = a + 1
time.sleep(0.2) # 延时0.2秒,模拟写入所需时间
a = tmp
6 加上一把锁,避免以上情况出现
知道问题出现的原因后,要想修复问题,也没那么复杂。
通过python中提供的锁机制,某段代码只能单线程执行时,上锁,其他线程等待,直到释放锁后,其他线程再争锁,执行代码,释放锁,重复以上。
创建一把锁locka
:
import threading
import time
locka = threading.Lock()
通过 locka.acquire()
获得锁,通过locka.release()
释放锁,它们之间的这些代码,只能单线程执行。
a = 0
def add1():
global a
try:
locka.acquire() # 获得锁
tmp = a + 1
time.sleep(0.2) # 延时0.2秒,模拟写入所需时间
a = tmp
finally:
locka.release() # 释放锁
print('%s adds a to 1: %d'%(threading.current_thread().getName(),a))
threads = [threading.Thread(name='t%d'%(i,),target=add1) for i in range(10)]
[t.start() for t in threads]
执行结果如下:
t0 adds a to 1: 1
t1 adds a to 1: 2
t2 adds a to 1: 3
t3 adds a to 1: 4
t4 adds a to 1: 5
t5 adds a to 1: 6
t6 adds a to 1: 7
t7 adds a to 1: 8
t8 adds a to 1: 9
t9 adds a to 1: 10
一起正常,其实这已经是单线程顺序执行了,就本例子而言,已经失去多线程的价值,并且还带来了因为线程创建开销,浪费时间的副作用。
程序中只有一把锁,通过 try...finally
还能确保不发生死锁。但是,当程序中启用多把锁,还是很容易发生死锁。
注意使用场合,避免死锁,是我们在使用多线程开发时需要注意的一些问题。
7 总结
Python的多线程模型还有一些更深入的问题,在此不再展开,后续再讨论。
希望透过这篇文章,帮助你对多线程模型编程本质有些更清晰的认识。
如果觉得此文对你有用,欢迎转发。送人玫瑰,手留余香~Python与算法社区
本文分享自 程序员郭震zhenguo 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!