在使用多线程的应用下,如何保证线程安全,以及线程之间的同步,或者访问共享变量等问题是十分棘手的问题,也是使用多线程下面临的问题,如果处理不好,会带来较严重的后果,使用python多线程中提供Lock Rlock Semaphore Event Condition 用来保证线程之间的同步,后者保证访问共享变量的互斥问题

Lock & RLock:互斥锁 用来保证多线程访问共享变量的问题

Semaphore对象:Lock互斥锁的加强版,可以被多个线程同时拥有,而Lock只能被某一个线程同时拥有。

Event对象: 它是线程间通信的方式,相当于信号,一个线程可以给另外一个线程发送信号后让其执行操作。

Condition对象:其可以在某些事件触发或者达到特定的条件后才处理数据

一、线程同步锁(互斥锁)

概念

  • 线程同步,线程间协同,通过某种技术,让一个线程访问某些数据时,其他线程不能访问这些数据,直到该线程完 成对数据的操作。

  • 锁(Lock):一旦线程获得锁,其他试图获取锁的线程将被阻塞等待。

  • 锁:凡是存在共享支援争抢的地方都可以使用锁,从而保证只有一个使用者可以完全使用这个资源。

锁的应用场景

锁适用于访问和修改同一个共享资源的时候,即读写同一个资源的时候。

使用锁的注意事项:

1、少用锁,必要时用锁。使用了锁,多线程访问被锁的资源时,就成了串行,要么排队执行,要么争抢执行。 举例,高速公路上车并行跑,可是到了省界只开放了一个收费口,过了这个口,车辆依然可以在多车道 上一起跑。过收费口的时候,如果排队一辆辆过,加不加锁一样效率相当,但是一旦出现争抢,就必须 加锁一辆辆过。注意,不管加不加锁,只要是一辆辆过,效率就下降了。 ​

2、加锁时间越短越好,不需要就立即释放锁。 ​

3、一定要避免死锁。

线程锁与GIL(全局解释器锁)

需要注意的点:

1.线程抢的是GIL锁,GIL锁相当于执行权限,拿到执行权限后才能拿到互斥锁Lock,其他线程也可以抢到GIL,但如果发现Lock仍然没有被释放则阻塞,即便是拿到执行权限GIL也要立刻交出来

2.join是等待所有,即整体串行,而锁只是锁住修改共享数据的部分,即部分串行,要想保证数据安全的根本原理在于让并发变成串行,join与互斥锁都可以实现,毫无疑问,互斥锁的部分串行效率要更高

Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock?

 首先我们需要达成共识:锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据,

然后,我们可以得出结论:保护不同的数据就应该加不同的锁。

 最后,问题就很明朗了,GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock。

过程分析:所有线程抢的是GIL锁,或者说所有线程抢的是执行权限

  线程1抢到GIL锁,拿到执行权限,开始执行,然后加了一把Lock,还没有执行完毕,即线程1还未释放Lock,有可能线程2抢到GIL锁,开始执行,执行过程中发现Lock还没有被线程1释放,于是线程2进入阻塞,被夺走执行权限,有可能线程1拿到GIL,然后正常执行到释放Lock。。。这就导致了串行运行的效果

  既然是串行,那我们执行

  t1.start()

  t1.join

  t2.start()

  t2.join()

  这也是串行执行啊,为何还要加Lock呢,需知join是等待t1所有的代码执行完,相当于锁住了t1的所有代码,而Lock只是锁住一部分操作共享数据的代码。

深层原因

因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序 里的线程和 py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题,  这可以说是Python早期版本的遗留问题。

例子

加锁前

import time
from threading import Lock,Thread

# Lock 互斥锁加锁前
def func(lock):
global n
temp = n
time.sleep(0.2)
n = temp - 1


if __name__ == '__main__':

n = 10
t_lst = []
lock = Lock()
for i in range(10):
t = Thread(target=func, args=(lock,))
t.start()
t_lst.append(t)

for t in t_lst: t.join()
print(n) #结果很大可能为9

  

分析:多个线程同时得到n的值为10,然后同时执行n-1即10-1得到的最后的值为9.

加锁后

import time
from threading import Lock,Thread

# Lock 互斥锁
def func(lock):
global n
lock.acquire()
temp = n
time.sleep(0.2)
n = temp - 1
lock.release()


if __name__ == '__main__':

n = 10
t_lst = []
lock = Lock()
for i in range(10):
t = Thread(target=func, args=(lock,))
t.start()
t_lst.append(t)

for t in t_lst: t.join()
print(n) #结果为0

# 当多个线程都修改某一个共享数据的时候,需要进行同步控制。
#
# 线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
# 互斥锁为资源引入一个状态:锁定/非锁定。某个线程要更改共享数据时,先将其锁定,
# 此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,
# 其他的线程才能再次锁定该资源。
# 互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

  

二、线程死锁和递归锁

死锁:所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。在一个线程中,我们在部分代码中使用了锁lock.acquire()还没有释放锁时,调用了另一个方法,方法中又使用了锁,这样如果使用同一把普通的锁很容易出现死锁。

例子

from threading import Thread,Lock
import time
mutexA=Lock()
mutexB=Lock()

class MyThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
mutexA.acquire()
print('\033[41m%s 拿到A锁\033[0m' %self.name)

mutexB.acquire()
print('\033[42m%s 拿到B锁\033[0m' %self.name)
mutexB.release()

mutexA.release()

def func2(self):
mutexB.acquire()
print('\033[43m%s 拿到B锁\033[0m' %self.name)
time.sleep(2)

mutexA.acquire()
print('\033[44m%s 拿到A锁\033[0m' %self.name)
mutexA.release()

mutexB.release()

if __name__ == '__main__':
for i in range(10):
t=MyThread()
t.start()

'''
Thread-1 拿到A锁
Thread-1 拿到B锁
Thread-1 拿到B锁
Thread-2 拿到A锁
然后就卡住,死锁了
'''

  

解决办法:

递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了“可重入锁(递归锁)”:threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

面的例子如果使用RLock代替Lock,则不会发生死锁:

mutexA=mutexB=threading.RLock() #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止

例子

from threading import Thread,RLock,Lock
import time
# mutexA=Lock()
# mutexB=Lock()

mutexA = mutexB=RLock()


class MyThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
mutexA.acquire() #锁的计数 count = 1
print('\033[41m%s 拿到A锁\033[0m' %self.name)

mutexB.acquire() #锁的计数 count = 1+1 =2
print('\033[42m%s 拿到B锁\033[0m' %self.name)
mutexB.release() #锁的计数 count = 2-1= 1

mutexA.release() #锁的计数 count = 1-1= 0

def func2(self):
mutexB.acquire() #锁的计数 count = 1
print('\033[43m%s 拿到B锁\033[0m' %self.name)
time.sleep(2)

mutexA.acquire() #锁的计数 count = 1+1 =2
print('\033[44m%s 拿到A锁\033[0m' %self.name)
mutexA.release() #锁的计数 count = 2-1= 1

mutexB.release() #锁的计数 count = 1-1= 0

if __name__ == '__main__':
for i in range(10):
t=MyThread()
t.start()

  



三、信号量Semaphore

和Lock很像,信号量对象内部维护一个倒计数器,每当调用acquire()时内置计数器-1,调用release() 时内置计数器+1。当acquire方法发现计数为0就阻塞请求 的线程,直到其它线程对信号量release后,计数大于0,恢复阻塞的线程。

例子

from threading import Thread,Semaphore
import time

def func(sem,i):
sem.acquire()
print("被执行了一次,获取一个信号量 _value={}".format(sem._value))
print(i)
time.sleep(1)
print("释放一个信号量")
sem.release()


sem = Semaphore(5)
for i in range(20):
Thread(target=func,args=(sem,i)).start()

  


例子2

import threading, time


class myThread(threading.Thread):
def run(self): # 启动后,执行run方法
if semaphore.acquire(): # 加把锁,可以放进去多个(相当于5把锁,5个钥匙,同时有5个线程)
print(self.name)
time.sleep(5)
semaphore.release()


if __name__ == "__main__":
semaphore = threading.Semaphore(5) # 同时能有几个线程进去(设置为5就是一次5个线程进去),类似于停车厂一次能停几辆车

thrs = [] # 空列表
for i in range(100): # 100个线程
thrs.append(myThread()) # 加线程对象

for t in thrs:
t.start() # 分别启动

  

输出结果

Thread-1
Thread-2
Thread-3
Thread-4
Thread-5 #5个线程同时出来

Thread-8
Thread-6
Thread-9
Thread-7
Thread-10 #每隔3秒再打印5个出来

部分省略.......

  

与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程

参考资料

[1]https://blog.csdn.net/u013008795/article/details/91357383

[2]https://www.cnblogs.com/nuomin/p/7899675.html

Python之路(第四十四篇)线程同步锁、死锁、递归锁、信号量的更多相关文章

  1. Python之路【第十四篇】:AngularJS --暂无内容-待更新

    Python之路[第十四篇]:AngularJS --暂无内容-待更新

  2. Python之路【第十八篇】:Web框架们

    Python之路[第十八篇]:Web框架们   Python的WEB框架 Bottle Bottle是一个快速.简洁.轻量级的基于WSIG的微型Web框架,此框架只由一个 .py 文件,除了Pytho ...

  3. Python之路【第十六篇】:Django【基础篇】

    Python之路[第十六篇]:Django[基础篇]   Python的WEB框架有Django.Tornado.Flask 等多种,Django相较与其他WEB框架其优势为:大而全,框架本身集成了O ...

  4. Python之路【第十九篇】:爬虫

    Python之路[第十九篇]:爬虫   网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本.另外一些不常使用 ...

  5. Python之路【第十五篇】:Web框架

    Python之路[第十五篇]:Web框架   Web框架本质 众所周知,对于所有的Web应用,本质上其实就是一个socket服务端,用户的浏览器其实就是一个socket客户端. 1 2 3 4 5 6 ...

  6. Python之路【第十二篇】:JavaScrpt -暂无内容-待更新

    Python之路[第十二篇]:JavaScrpt -暂无内容-待更新

  7. Python之路(第四十五篇)线程Event事件、 条件Condition、定时器Timer、线程queue

    一.事件Event Event(事件):事件处理的机制:全局定义了一个内置标志Flag,如果Flag值为 False,那么当程序执行 event.wait方法时就会阻塞,如果Flag值为True,那么 ...

  8. Python之路【第十六篇】:Python并发编程|进程、线程

    一.进程和线程 进程 假如有两个程序A和B,程序A在执行到一半的过程中,需要读取大量的数据输入(I/O操作), 而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源. 是 ...

  9. Python之路【第十二篇】:Python面向对象高级

    一.反射 1 什么是反射 反射的概念是由Smith在1982年首次提出的,主要是指程序可以访问.检测和修改它本身状态或行为的一种能力(自省).这一概念的提出很快引发了计算机科学领域关于应用反射性的研究 ...

  10. Python之路(第三十八篇) 并发编程:进程同步锁/互斥锁、信号量、事件、队列、生产者消费者模型

    一.进程锁(同步锁/互斥锁) 进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的, 而共享带来的是竞争,竞争带来的结果就是错乱,如何控制,就是加锁处理. 例 ...

随机推荐

  1. redhat quay 安装试用

    最近redhat 开源了quay 容器镜像管理平台,参考官方文档跑的时候需要订阅,各种不好使,然后就自己基于源码构建了 一个镜像(使用官方的dockerfile,构建出来的太大了1.9G 以及push ...

  2. ESA2GJK1DH1K基础篇: 阿里云物联网平台: 云平台显示单片机采集的温湿度数据,控制设备继电器(基于GPRS模块,AT指令TCP_MQTT通信)

    实现的功能 上一节是使用的Wi-Fi模块连接的阿里云平台,这节呢咱用GPRS连接. 阿里云的设备配置还是按照上一节的,有个温度,有个湿度. 测试 一,打开单片机程序 二,根据自己的修改,然后下载进去单 ...

  3. 洛谷 p3870 开关 线段树模板

    这两天学了很长时间于是做了一道水题 我就用了模板,就连任何优化都没有 就AC了,复杂度也很爆炸10个点1500多毫秒 这个题就是把lazy[]改成记录下修改的次数,每次修改的时候mod 2,因为反过来 ...

  4. SqlServer事务语法及使用方法(转)

    原博:http://blog.csdn.net/xiaouncle/article/details/52891563 事务是关于原子性的.原子性的概念是指可以把一些事情当做一个不可分割的单元来看待.从 ...

  5. Cannot read lifecycle mapping metadata for artifact org.apache.maven.plugins:mav问题

    1.导致问题原因:从装系统,从win7改到win10 由于重装了系统,打开eclipse时,maven验证会出错,点击pom文件,会发现有红色的Cannot read lifecycle mappin ...

  6. varnish搭建cdn网络-------3.0.5版本

    CDN分发网络CDN的全称是Content Delivery Network,即内容分发网络.其基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快.更稳定.通过在 ...

  7. Android Studio 之 Navigation【1.页面之间的切换】

    1.创建 2个 Fragment ,下面两个include 不要勾 2.创建好 Fragment 后,打开layout中的 fragment.xml 文件,将里面默认的 textView 控件删除掉 ...

  8. Gamma阶段项目展示

    Gamma阶段项目展示 一. 团队成员介绍 姓名 Gamma职责 个人博客 张圆宁 PM,后端 个人博客 王文珺 后端 个人博客 牛宇航 后端 个人博客 申化文 后端 个人博客 汪慕澜 测试,部署 个 ...

  9. ASP.NET Core消息队列RabbitMQ基础入门实战演练

    一.课程介绍 人生苦短,我用.NET Core!消息队列RabbitMQ大家相比都不陌生,本次分享课程阿笨将给大家分享一下在一般项目中99%都会用到的消息队列MQ的一个实战业务运用场景.本次分享课程不 ...

  10. Linux+Nginx+Supervisor部署ASP.NET Core实操手册

    一.课程介绍 在上一节课程<ASP.NET Core托管和部署Linux实操演练手册>中我们学过net core的部署方式多样性和灵活性.我们通过远程工具输入dotnet 程序集名称.dl ...