1. 多线程编程与线程安全相关重要概念

在我的上篇博文 聊聊Python中的GIL 中,我们熟悉了几个特别重要的概念:GIL,线程,进程, 线程安全,原子操作

以下是简单回顾,详细介绍请直接看聊聊Python中的GIL

  • GIL:  Global Interpreter Lock,全局解释器锁。为了解决多线程之间数据完整性和状态同步的问题,设计为在任意时刻只有一个线程在解释器中运行。
  • 线程:程序执行的最小单位。
  • 进程:系统资源分配的最小单位。
  • 线程安全:多线程环境中,共享数据同一时间只能有一个线程来操作。
  • 原子操作:原子操作就是不会因为进程并发或者线程并发而导致被中断的操作。

还有一个重要的结论:当对全局资源存在写操作时,如果不能保证写入过程的原子性,会出现脏读脏写的情况,即线程不安全。Python的GIL只能保证原子操作的线程安全,因此在多线程编程时我们需要通过加锁来保证线程安全。

最简单的锁是互斥锁(同步锁),互斥锁是用来解决io密集型场景产生的计算错误,即目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据。

下面我们会来介绍如何使用互斥锁。

2. Threading.Lock实现互斥锁的简单示例

我们通过Threading.Lock()来实现锁。

以下是线程不安全的例子:

>>> import threading
>>> import time
>>> def sub1():
global count
tmp = count
time.sleep(0.001)
count = tmp + 1
time.sleep(2) >>> count = 0
>>> def verify(sub):
global count
thread_list = []
for i in range(100):
t = threading.Thread(target=sub,args=())
t.start()
thread_list.append(t)
for j in thread_list:
j.join()
print(count) >>> verify(sub1)
14

在这个例子中,我们把

count+=1

代替为

tmp = count
time.sleep(0.001)
count = tmp + 1

是因为,尽管count+=1是非原子操作,但是因为CPU执行的太快了,比较难以复现出多进程的非原子操作导致的进程不安全。经过代替之后,尽管只sleep了0.001秒,但是对于CPU的时间来说是非常长的,会导致这个代码块执行到一半,GIL锁就释放了。即tmp已经获取到count的值了,但是还没有将tmp + 1赋值给count。而此时其他线程如果执行完了count = tmp + 1, 当返回到原来的线程执行时,尽管count的值已经更新了,但是count = tmp + 1是个赋值操作,赋值的结果跟count的更新的值是一样的。最终导致了我们累加的值有很多丢失。

下面是线程安全的例子,我们可以用threading.Lock()获得锁

>>> count = 0
>>> def sub2():
global count
if lock.acquire(1):
    #acquire()是获取锁,acquire(1)返回获取锁的结果,成功获取到互斥锁为True,如果没有获取到互斥锁则返回False
tmp = count
time.sleep(0.001)
count = tmp + 1
time.sleep(2)
lock.release() 一系列操作结束之后需要释放锁 >>> def verify(sub):
global count
thread_list = []
for i in range(100):
t = threading.Thread(target=sub,args=())
t.start()
thread_list.append(t)
for j in thread_list:
j.join()
print(count) >>> verify(sub2)
100

获取锁和释放锁的语句也可以用Python的with来实现,这样更简洁

>>> count = 0
>>> def sub3():
global count
with lock:
tmp = count
time.sleep(0.001)
count = tmp + 1
time.sleep(2) >>> def verify(sub):
global count
thread_list = []
for i in range(100):
t = threading.Thread(target=sub,args=())
t.start()
thread_list.append(t)
for j in thread_list:
j.join()
print(count) >>> verify(sub3)
100

3.  两种死锁情况及处理

死锁产生的原因

两种死锁:

3.1 迭代死锁与递归锁(RLock)

该情况是一个线程“迭代”请求同一个资源,直接就会造成死锁。这种死锁产生的原因是我们标准互斥锁threading.Lock的缺点导致的。标准的锁对象(threading.Lock)并不关心当前是哪个线程占有了该锁;如果该锁已经被占有了,那么任何其它尝试获取该锁的线程都会被阻塞,包括已经占有该锁的线程也会被阻塞

下面是例子,

#/usr/bin/python3
# -*- coding: utf-8 -*- import threading
import time count_list = [0,0]
lock = threading.Lock() def change_0():
global count_list
with lock:
tmp = count_list[0]
time.sleep(0.001)
count_list[0] = tmp + 1
time.sleep(2)
print("Done. count_list[0]:%s" % count_list[0]) def change_1():
global count_list
with lock:
tmp = count_list[1]
time.sleep(0.001)
count_list[1] = tmp + 1
time.sleep(2)
print("Done. count_list[1]:%s" % count_list[1]) def change():
with lock:
change_0()
time.sleep(0.001)
change_1() def verify(sub):
global count_list
thread_list = []
for i in range(100):
t = threading.Thread(target=sub, args=())
t.start()
thread_list.append(t)
for j in thread_list:
j.join()
print(count_list) if __name__ == "__main__":
verify(change)

示例中,我们有一个共享资源count_list,有两个分别取这个共享资源第一部分和第二部分的数字(count_list[0]和count_list[1])。两个访问函数都使用了锁来确保在获取数据时没有其它线程修改对应的共享数据。
现在,如果我们思考如何添加第三个函数来获取两个部分的数据。一个简单的方法是依次调用这两个函数,然后返回结合的结果。

这里的问题是,如有某个线程在两个函数调用之间修改了共享资源,那么我们最终会得到不一致的数据。

最明显的解决方法是在这个函数中也使用lock。然而,这是不可行的。里面的两个访问函数将会阻塞,因为外层语句已经占有了该锁

结果是没有任何输出,死锁。

为了解决这个问题,我们可以用threading.RLock代替threading.Lock

#/usr/bin/python3
# -*- coding: utf-8 -*- import threading
import time count_list = [0,0]
lock = threading.RLock() def change_0():
global count_list
with lock:
tmp = count_list[0]
time.sleep(0.001)
count_list[0] = tmp + 1
time.sleep(2)
print("Done. count_list[0]:%s" % count_list[0]) def change_1():
global count_list
with lock:
tmp = count_list[1]
time.sleep(0.001)
count_list[1] = tmp + 1
time.sleep(2)
print("Done. count_list[1]:%s" % count_list[1]) def change():
with lock:
change_0()
time.sleep(0.001)
change_1() def verify(sub):
global count_list
thread_list = []
for i in range(100):
t = threading.Thread(target=sub, args=())
t.start()
thread_list.append(t)
for j in thread_list:
j.join()
print(count_list) if __name__ == "__main__":
verify(change)

3.2 互相等待死锁与锁的升序使用

死锁的另外一个原因是两个进程想要获得的锁已经被对方进程获得,只能互相等待又无法释放已经获得的锁,而导致死锁。假设银行系统中,用户a试图转账100块给用户b,与此同时用户b试图转账500块给用户a,则可能产生死锁。
2个线程互相等待对方的锁,互相占用着资源不释放。

下面是一个互相调用导致死锁的例子:

#/usr/bin/python3
# -*- coding: utf-8 -*- import threading
import time class Account(object):
def __init__(self, name, balance, lock):
self.name = name
self.balance = balance
self.lock = lock def withdraw(self, amount):
self.balance -= amount def deposit(self, amount):
self.balance += amount def transfer(from_account, to_account, amount):
with from_account.lock:
from_account.withdraw(amount)
time.sleep(1)
print("trying to get %s's lock..." % to_account.name)
with to_account.lock:
to_account_deposit(amount)
print("transfer finish") if __name__ == "__main__":
a = Account('a',1000, threading.Lock())
b = Account('b',1000, threading.Lock())
thread_list = []
thread_list.append(threading.Thread(target = transfer, args=(a,b,100)))
thread_list.append(threading.Thread(target = transfer, args=(b,a,500)))
for i in thread_list:
i.start()
for j in thread_list:
j.join()

最终的结果是死锁:

trying to get account a's lock...
trying to get account b's lock...

即我们的问题是:

你正在写一个多线程程序,其中线程需要一次获取多个锁,此时如何避免死锁问题。
解决方案:
多线程程序中,死锁问题很大一部分是由于线程同时获取多个锁造成的。举个例子:一个线程获取了第一个锁,然后在获取第二个锁的 时候发生阻塞,那么这个线程就可能阻塞其他线程的执行,从而导致整个程序假死。 其实解决这个问题,核心思想也特别简单:目前我们遇到的问题是两个线程想获取到的锁,都被对方线程拿到了,那么我们只需要保证在这两个线程中,获取锁的顺序保持一致就可以了。举个例子,我们有线程thread_a, thread_b, 锁lock_1, lock_2。只要我们规定好了锁的使用顺序,比如先用lock_1,再用lock_2,当线程thread_a获得lock_1时,其他线程如thread_b就无法获得lock_1这个锁,也就无法进行下一步操作(获得lock_2这个锁),也就不会导致互相等待导致的死锁。简言之,解决死锁问题的一种方案是为程序中的每一个锁分配一个唯一的id,然后只允许按照升序规则来使用多个锁,这个规则使用上下文管理器 是非常容易实现的,示例如下:

#/usr/bin/python3
# -*- coding: utf-8 -*- import threading
import time
from contextlib import contextmanager thread_local = threading.local() @contextmanager
def acquire(*locks):
#sort locks by object identifier
locks = sorted(locks, key=lambda x: id(x)) #make sure lock order of previously acquired locks is not violated
acquired = getattr(thread_local,'acquired',[])
if acquired and (max(id(lock) for lock in acquired) >= id(locks[0])):
raise RuntimeError('Lock Order Violation') # Acquire all the locks
acquired.extend(locks)
thread_local.acquired = acquired try:
for lock in locks:
lock.acquire()
yield
finally:
for lock in reversed(locks):
lock.release()
del acquired[-len(locks):] class Account(object):
def __init__(self, name, balance, lock):
self.name = name
self.balance = balance
self.lock = lock def withdraw(self, amount):
self.balance -= amount def deposit(self, amount):
self.balance += amount def transfer(from_account, to_account, amount):
print("%s transfer..." % amount)
with acquire(from_account.lock, to_account.lock):
from_account.withdraw(amount)
time.sleep(1)
to_account.deposit(amount)
print("%s transfer... %s:%s ,%s: %s" % (amount,from_account.name,from_account.balance,to_account.name, to_account.balance))
print("transfer finish") if __name__ == "__main__":
a = Account('a',1000, threading.Lock())
b = Account('b',1000, threading.Lock())
thread_list = []
thread_list.append(threading.Thread(target = transfer, args=(a,b,100)))
thread_list.append(threading.Thread(target = transfer, args=(b,a,500)))
for i in thread_list:
i.start()
for j in thread_list:
j.join()

我们获得的结果是

100 transfer...
500 transfer...
100 transfer... a:900 ,b:1100
transfer finish
500 transfer... b:600, a:1400
transfer finish

成功的避免了互相等待导致的死锁问题。

在上述代码中,有几点语法需要解释:

  • 1. 装饰器@contextmanager是用来让我们能用with语句调用锁的,从而简化锁的获取和释放过程。关于with语句,大家可以参考浅谈 Python 的 with 语句(https://www.ibm.com/developerworks/cn/opensource/os-cn-pythonwith/)。简言之,with语句在调用时,先执行 __enter__()方法,然后执行with结构体内的语句,最后执行__exit__()语句。有了装饰器@contextmanager. 生成器函数中 yield 之前的语句在 __enter__() 方法中执行,yield 之后的语句在 __exit__() 中执行,而 yield 产生的值赋给了 as 子句中的 value 变量。
  • 2. try和finally语句中实现的是锁的获取和释放。
  • 3. try之前的语句,实现的是对锁的排序,以及锁排序是否被破坏的判断。

今天我们主要讨论了Python多线程中如何保证线程安全,互斥锁的使用方法。另外着重讨论了两种导致死锁的情况:迭代死锁与互相等待死锁,以及这两种死锁的解决方案:递归锁(RLock)的使用和锁的升序使用。

对于多线程编程,我们将在下一篇文章讨论线程同步(Event)问题,以及对Python多线程模块(threading)进行总结。

参考文献:

1. 深入理解 GIL:如何写出高性能及线程安全的 Python 代码 http://python.jobbole.com/87743/

2. Python中的原子操作 https://www.jianshu.com/p/42060299c581

3. 详解python中的Lock与RLock https://blog.csdn.net/ybdesire/article/details/80294638

4. 深入解析Python中的线程同步方法 https://www.jb51.net/article/86599.htm

5.  Python中死锁的形成示例及死锁情况的防止 https://www.jb51.net/article/86617.htm

6.  举例讲解 Python 中的死锁、可重入锁和互斥锁 http://python.jobbole.com/82723/

7.  python基础之多线程锁机制

8. python--threading多线程总结

9. Python3入门之线程threading常用方法

10. 浅谈 Python 的 with 语句 https://www.ibm.com/developerworks/cn/opensource/os-cn-pythonwith/

Python中的多线程编程,线程安全与锁(一)的更多相关文章

  1. Python中的多线程编程,线程安全与锁(二)

    在我的上篇博文Python中的多线程编程,线程安全与锁(一)中,我们熟悉了多线程编程与线程安全相关重要概念, Threading.Lock实现互斥锁的简单示例,两种死锁(迭代死锁和互相等待死锁)情况及 ...

  2. python中的多线程编程与暂停、播放音频的结合

    先给两个原文链接: https://blog.csdn.net/u013755307/article/details/19913655 https://www.cnblogs.com/scolia/p ...

  3. Python中的多线程编程

    前言: 线程是操作系统能够进行运算调度的最小单位(程序执行流的最小单元) 它被包含在进程之中,是进程中的实际运作单位 一个进程中可以并发多个线程每条线程并行执行不同的任务 (线程是进程中的一个实体,是 ...

  4. python中的进程、线程(threading、multiprocessing、Queue、subprocess)

    Python中的进程与线程 学习知识,我们不但要知其然,还是知其所以然.你做到了你就比别人NB. 我们先了解一下什么是进程和线程. 进程与线程的历史 我们都知道计算机是由硬件和软件组成的.硬件中的CP ...

  5. Python中的并发编程

    简介 我们将一个正在运行的程序称为进程.每个进程都有它自己的系统状态,包含内存状态.打开文件列表.追踪指令执行情况的程序指针以及一个保存局部变量的调用栈.通常情况下,一个进程依照一个单序列控制流顺序执 ...

  6. python多进程与多线程编程

    进程(process)和线程(thread)是非常抽象的概念.多线程与多进程编程对于代码的并发执行,提升代码运行效率和缩短运行时间至关重要.下面介绍一下python的multiprocess和thre ...

  7. Python多进程与多线程编程及GIL详解

    介绍如何使用python的multiprocess和threading模块进行多线程和多进程编程. Python的多进程编程与multiprocess模块 python的多进程编程主要依靠multip ...

  8. Python中的并行编程速度

    这里主要想记录下今天碰到的一个小知识点:Python中的并行编程速率如何? 我想把AutoTool做一个并行化改造,主要目的当然是想提高多任务的执行速度.第一反应就是想到用多线程执行不同模块任务,但是 ...

  9. python基础-12 多线程queue 线程交互event 线程锁 自定义线程池 进程 进程锁 进程池 进程交互数据资源共享

    Python中的进程与线程 学习知识,我们不但要知其然,还是知其所以然.你做到了你就比别人NB. 我们先了解一下什么是进程和线程. 进程与线程的历史 我们都知道计算机是由硬件和软件组成的.硬件中的CP ...

随机推荐

  1. java基础必备单词讲解 day five

    Rectangle width high height area employee tool param version author math guess resources 之前单词复习 path ...

  2. node-inspector调试工具使用方法

    开发node.js程序使用的是javascript语言,其中最麻烦的还是调试,这里介绍一下node-inspector使用方法.具体资料可以看参考资料中的GITHUB文档. 工具/原料   node. ...

  3. 谈谈对bug的一点想法,说说做好日志记录的重要性

    说起程序猿,总绕不开的一个话题就是bug,估计每个程序猿听到某某测试跑过来一脸淫笑的告诉你这个功能有个bug的时候,总有种恨不得掐死他的想法.其实程序猿跟bug的关系,感觉有点像父亲和儿子的关系,自己 ...

  4. MPP(大规模并行处理)

    1. 什么是MPP? MPP (Massively Parallel Processing),即大规模并行处理,在数据库非共享集群中,每个节点都有独立的磁盘存储系统和内存系统,业务数据根据数据库模型和 ...

  5. 关于sql server 2008 r2 展开时报错:参数名:viewInfo ( Microsoft SqlServer Management SqlStudio Explorer )解决思路

    今天安装了sql server 2008 R2,安装成功之后我打开软件登陆都没问题,但是一展开选项就弹出错误提示框: 参数名:viewInfo 不能为空 (Microsoft SqlServer Ma ...

  6. 统计输入任意的字符中中英文字母,空格和其他字符的个数 python

    这里用到了三个函数: #判断是否为数字:str.isdigit()#是否为字母:str.isalpha()#是否为空格:str.isspace() def tongji(str): alpha = 0 ...

  7. POJ:3421-X-factor Chains(因式分解)(全排列)

    X-factor Chains Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 7986 Accepted: 2546 Descr ...

  8. 20145202马超GDB调试汇编堆栈过程分析

    20145202马超GDB调试汇编堆栈过程分析 esc :w保存,:wq保存并退出 x:删除错误的单个字母 dw:删除整个单词 gcc hello.c -o hello:运行hello.c gcc - ...

  9. samba server on Mac OS X Lion Server

    一般Mac共享通过配置wins,smb即可实现.注意在同一个工作组! 参考:http://computers.tutsplus.com/tutorials/how-to-set-up-an-smb-s ...

  10. centos使用--centos7.3配置LNMP

    目录 1 源的配置 2 安装软件 2.1 安装php7 2.2 安装nginx 2.3 安装mysql 2.4 安装vsftp (ftp登录配置) 3 开机启动设置 4 其它一些配置 4.1 git的 ...