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

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!

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

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

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

但是在本例中,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的值只相当于一个线程执行的结果。

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

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

以上最核心的三行代码:

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与算法社区

发布了174 篇原创文章 · 获赞 325 · 访问量 30万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览