在使用多线程的应用下,如何保证线程安全,以及线程之间的同步,或者访问共享变量等问题是十分棘手的问题,也是使用多线程下面临的问题,如果处理不好,会带来较严重的后果,使用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. ESA2GJK1DH1K基础篇: 硬件使用说明

    开发板板载介绍 一.示意图 1.单片机:STM32C8T6 2.Wi-Fi模块:ESP8266 3.GPRS模块:Air202 4.温湿度传感器:DHT11 5.液晶:OLED(IIC) 6.继电器 ...

  2. p1842 奶牛玩杂技 题解

    感觉其他dalao讲的不是很明白啊,我这样的蒟蒻看不懂啊. 在luogu这个dalao遍地的地方我蒟蒻看个题解也不明白,我为跟我同病相怜的蒟蒻写一篇吧 其实真是不太明白,大部分题解都是只说 体重大的在 ...

  3. Spring Boot 知识笔记(thymleaf模板引擎)

    一.pom.xml中引入 <dependency> <groupId>org.springframework.boot</groupId> <artifact ...

  4. Spring Security教程之基于表达式的权限控制(九)

    目录 1.1      通过表达式控制URL权限 1.2      通过表达式控制方法权限 1.2.1     使用@PreAuthorize和@PostAuthorize进行访问控制 1.2.2   ...

  5. quantmod

    -quantmod(数据和图形) -TTR(技术分析) -blooter(账户管理) -FinancialInstrument(金融产品) -quantstrast(策略模型) -Performanc ...

  6. libevent笔记5:水位watermarks

    bufferevent中提供了对读写回调的触发条件及最大缓存长度的设置,即低高水位: 低水位:是读写回调函数的最低触发数据长度,当输入/输出缓存区中的数据长度小于低水位时,读/写回调函数不会被触发: ...

  7. C# PKCS7加密解密

    //加密字符串 public string Encryption(string toE) { //加密和解密必须采用相同的key,具体自己填写,但是必须为32位 "); RijndaelMa ...

  8. vue-cli中的element-ui的主题更改

    主题安装分为全局安装和局部安装(局部安装指的是项目内进行安装) 局部安装: 使用局部安装方便他人使用,他人直接安装主题需要的依赖就可以进行使用 局部安装的步骤 1.npm i element-them ...

  9. 用ADB 抓取和存储APP日志方法

    前置条件:电脑已经安装好adb (一)  进入adb目录下连接手机,检测出手机 CD 到SDK的platform-tools (二)adb logcat-c  清除日志  (三)adb  logcat ...

  10. python抓取每期双色球中奖号码,用于分析

    获取每期双色球中奖号码,便于观察,话不多说,代码如下 # -*- coding:utf-8 -*- # __author__ :kusy # __content__:获取每期双色球中奖号码 # __d ...