前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >我对Python多线程编程的通俗理解,希望帮助到你!

我对Python多线程编程的通俗理解,希望帮助到你!

作者头像
double
发布2020-02-12 16:13:09
4850
发布2020-02-12 16:13:09
举报
文章被收录于专栏:算法channel算法channel

1 默认启动主线程

一般的,程序默认执行只在一个线程,这个线程称为主线程,例子演示如下:

导入线程相关的模块 threading:

代码语言:javascript
复制
import threading

threading的类方法 current_thread()返回当前线程:

代码语言:javascript
复制
t = threading.current_thread()
print(t) # <_MainThread(MainThread, started 139908235814720)>

所以,验证了程序默认是在MainThead中执行。

t.getName()获得这个线程的名字,其他常用方法,getName()获得线程id,isAlive()判断线程是否存活等。

代码语言:javascript
复制
print(t.getName()) # MainThread
print(t.ident) # 139908235814720
print(t.isAlive()) # True

以上这些仅是介绍多线程的背景知识,因为到目前为止,我们有且仅有一个"干活"的主线程

2 创建线程

创建一个线程:

代码语言:javascript
复制
my_thread = threading.Thread()

创建一个名称为my_thread的线程:

代码语言:javascript
复制
my_thread = threading.Thread(name='my_thread')

创建线程的目的是告诉它帮助我们做些什么,做些什么通过参数target传入,参数类型为callable,函数就是可调用的:

代码语言:javascript
复制
def print_i(i):
    print('打印i:%d'%(i,))
my_thread = threading.Thread(target=print_i,args=(1,))

my_thread线程已经全副武装,但是我们得按下发射按钮,启动start(),它才开始真正起飞。

代码语言:javascript
复制
my_thread().start()

打印结果如下,其中args指定函数print_i需要的参数i,类型为元祖。

代码语言:javascript
复制
打印i:1

至此,多线程相关的核心知识点,已经总结完毕。但是,仅仅知道这些,还不够!光纸上谈兵,当然远远不够。

接下来,聊聊应用多线程编程,最本质的一些东西。

3 交替获得CPU时间片

为了更好解释,假定计算机是单核的,尽管对于cpython,这个假定有些多余。

开辟3个线程,装到threads中:

代码语言:javascript
复制
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个线程:

代码语言:javascript
复制
[t.start() for t in threads]

打印结果如下,t0,t1,t2三个线程,根据操作系统的调度算法,轮询获得CPU时间片,注意观察,t2线程可能被连续调度,从而获得时间片。

代码语言:javascript
复制
当前线程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:

代码语言:javascript
复制
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]

执行结果:

代码语言:javascript
复制
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!

多线程中,只要存在同时读取和修改一个全局变量的情况,如果不采取其他措施,就一定不是线程安全的。

尽管,有时,某些情况的资源竞争,暴露出问题的概率极低极低

本例中,如果线程0 在修改a后,其他某些线程还是get到的是没有修改前的值,就会暴露问题。

但是在本例中,a = a + 1这种修改操作,花费的时间太短了,短到我们无法想象。所以,线程间轮询执行时,都能get到最新的a值。所以,暴露问题的概率就变得微乎其微。

5 代码稍作改动,叫问题暴露出来

只要弄明白问题暴露的原因,叫问题出现还是不困难的。

想象数据库的写入操作,一般需要耗费我们可以感知的时间。

为了模拟这个写入动作,简化期间,我们只需要延长修改变量a的时间,问题很容易就会还原出来。

代码语言:javascript
复制
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]

重新运行代码,只需一次,问题立马完全暴露,结果如下:

代码语言:javascript
复制
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的值只相当于一个线程执行的结果。

下面分析,为什么会出现上面的结果:

这是一个很有说服力的例子,因为在修改a前,有0.2秒的休眠时间,某个线程延时后,CPU立即分配计算资源给其他线程。直到分配给所有线程后,根据结果反映出,0.2秒的休眠时长还没耗尽,这样每个线程get到的a值都是0,所以才出现上面的结果。

以上最核心的三行代码:

代码语言:javascript
复制
tmp = a + 1
time.sleep(0.2) # 延时0.2秒,模拟写入所需时间
a = tmp

6 加上一把锁,避免以上情况出现

知道问题出现的原因后,要想修复问题,也没那么复杂。

通过python中提供的锁机制,某段代码只能单线程执行时,上锁,其他线程等待,直到释放锁后,其他线程再争锁,执行代码,释放锁,重复以上。

创建一把锁locka:

代码语言:javascript
复制
import threading
import time

locka = threading.Lock()

通过 locka.acquire() 获得锁,通过locka.release()释放锁,它们之间的这些代码,只能单线程执行。

代码语言:javascript
复制
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]

执行结果如下:

代码语言:javascript
复制
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与算法社区

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-01-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员郭震zhenguo 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 本例中,如果线程0 在修改a后,其他某些线程还是get到的是没有修改前的值,就会暴露问题。
  • 这是一个很有说服力的例子,因为在修改a前,有0.2秒的休眠时间,某个线程延时后,CPU立即分配计算资源给其他线程。直到分配给所有线程后,根据结果反映出,0.2秒的休眠时长还没耗尽,这样每个线程get到的a值都是0,所以才出现上面的结果。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档