Python3学习之路~9.3 GIL、线程锁之Lock\Rlock\信号量、Event
一 Python GIL(Global Interpreter Lock) 全局解释器锁
如果一个主机是单核,此时同时启动10个线程,由于CPU执行了上下文的切换,让我们宏观上看上去它们是并行的,但实际上在微观上它们永远是串行的。如果一个主机有四核,代表它可以真真正正同时执行4个任务,而不是假象。但是在Python中,无论你有多少核,它永远都是假象,实际上它永远同一时间只能执行一个线程。这是Python在开发之初的一个设计缺陷。因为Python是1989年就诞生了,当时主机只有一个核,它的创始人没有考虑多核的问题。
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
上面的核心意思就是,无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行。
首先需要明确的一点是GIL
并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL
归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。
这篇文章透彻的剖析了GIL对python多线程的影响,强烈推荐看一下:http://www.dabeaz.com/python/UnderstandingGIL.pdf
二 线程锁之Lock\Rlock\信号量
线程锁(互斥锁Mutex)
一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况?
- import threading
- import time
- def run(n):
- global num #在每个线程中都获取这个全局变量
- num+= #对此公共变量进行+1操作
- time.sleep()
- num = #设定一个共享变量
- t_objs = []
- for i in range():
- t = threading.Thread(target=run,args=("t%s" %i,))
- t.start()
- t_objs.append(t)
- for t in t_objs: #等待所有线程执行完毕
- t.join()
- print("num:",num)
正常来讲,这个num结果应该是100, 但在Ubuntu python 2.7上多运行几次,会发现,最后打印出来的num结果不总是0,为什么每次运行的结果不一样呢? 哈,很简单,假设你有A,B两个线程,此时都 要对num 进行加1操作, 由于2个线程是并发同时运行的,所以2个线程很有可能同时拿走了num=0这个初始变量交给cpu去运算,当A线程去处完的结果是1,但此时B线程运算完的结果也是1,两个线程同时CPU运算的结果再赋值给num变量后,结果就都是1。那怎么办呢? 很简单,每个线程在要修改公共数据时,为了避免自己在还没改完的时候别人也来修改此数据,可以给这个数据加一把锁, 这样其它线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据。
*注:不要在3.x上运行,不知为什么,3.x上的结果总是正确的,可能是自动加了锁
加锁版本
- import threading
- import time
- def run(n):
- lock.acquire() #2.获取锁
- global num
- num+=1
- lock.release() #3.释放锁
- time.sleep(2) #注意不要抱sleep放在线程锁,否则就真的变成串行了
- num = 0
- lock = threading.Lock() #1.生成一个锁的实例
- t_objs = []
- for i in range(50):
- t = threading.Thread(target=run,args=("t%s" %i,))
- t.start()
- t_objs.append(t)
- for t in t_objs:
- t.join()
- print("num:",num)
GIL VS Lock
之前说过了,Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock? 注意啦,这里的lock是用户级的lock,跟那个GIL没关系 ,具体我们通过下图来看一下就明白了。
那你又问了, 既然用户程序已经自己有锁了,那为什么C python还需要GIL呢?加入GIL主要的原因是为了降低程序的开发的复杂度,比如现在的你写python不需要关心内存回收的问题,因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序 里的线程和 py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题, 这可以说是Python早期版本的遗留问题。
RLock(递归锁)
当你的程序中有多处需要用到锁,甚至锁中还嵌套着锁时,就不要用线程锁,因为很容易会造成程序卡死,因为程序在很多锁中很容易找不到出来的路。这时你就需要用到递归锁。
递归锁说白了就是在一个大锁中还要再包含子锁。
- import threading, time
- def run1():
- print("grab the first part data")
- lock.acquire()
- global num
- num += 1
- lock.release()
- return num
- def run2():
- print("grab the second part data")
- lock.acquire()
- global num2
- num2 += 1
- lock.release()
- return num2
- def run3():
- lock.acquire()
- res = run1()
- print('--------between run1 and run2-----')
- res2 = run2()
- lock.release()
- print(res, res2)
- if __name__ == '__main__':
- num, num2 = 0, 0
- lock = threading.RLock() #生成递归锁的实例
- for i in range(10):
- t = threading.Thread(target=run3)
- t.start()
- while threading.active_count() != 1:
- print(threading.active_count())
- else:
- print('----all threads done---')
- print(num, num2)
Semaphore(信号量)
互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据。
- ####如下程序,可以直观的看到,每次都有5个线程同时运行
- ####信号量用途:连接池等
- import threading, time
- def run(n):
- semaphore.acquire()
- time.sleep(2)
- print("run the thread: %s\n" % n)
- semaphore.release()
- semaphore = threading.BoundedSemaphore(5) # 生成信号量实例,最多允许5个线程同时运行
- for i in range(20):
- t = threading.Thread(target=run, args=(i,))
- t.start()
- while threading.active_count() != 1:
- pass # print(threading.active_count())
- else:
- print('----all threads done---')
三 Event
事件是一个简单的同步对象。
事件代表一个内部标志位,线程可以等待标志位被设置,或者自己设置或清除标志位。
如果标志位被设置,则wait方法不会执行任何操作。
如果标志位被清除,则wait方法将被阻塞,直到标志位被再次设置。
允许多个线程等待同一事件。
- event = threading.Event() #客户端线程可以等待标志位被设置
- event.wait() #服务器线程可以设置或重置标志位
- event.set()
- event.clear()
- event.is_set()
通过Event来实现两个或多个线程间的交互,下面是一个红绿灯的例子,即起动一个线程做交通指挥灯,生成几个线程做车辆,车辆行驶按红灯停,绿灯行的规则。
- import threading, time
- def light():
- count = 0
- while True:
- if count < 5 :
- event.set() # 标志位设置,绿灯亮
- print("\33[42;1mgreen light is on...\033[0m")
- elif count >= 5 and count < 10:
- event.clear() #标志位清空,红灯亮
- print("\33[41;1mred light is on...\033[0m")
- else:
- print("\33[42;1mgreen light is on...\033[0m")
- count = 0
- time.sleep(1)
- count +=1
- def car(name):
- while True:
- if event.is_set():
- print("[%s] running..." % name)
- time.sleep(1)
- else:
- print("[%s] sees red light,waiting..." % name)
- event.wait() #如果标志位被清除,则wait方法将被阻塞,直到标志位被再次设置。
- print("green light is on,%s start going..." % name)
- if __name__ == '__main__':
- event = threading.Event()
- Light = threading.Thread(target=light)
- Light.start()
- for i in range(3):
- t = threading.Thread(target=car,args=(i,))
- t.start()
这里还有一个event使用的例子,员工进公司门要刷卡, 我们这里设置一个线程是“门”, 再设置几个线程为“员工”,员工看到门没打开,就刷卡,刷完卡,门开了,员工就可以通过。
- import threading
- import time
- import random
- def door():
- door_open_time_counter = 0
- while True:
- if door_swiping_event.is_set():
- print("\033[32;1mdoor opening....,count:%s\033[0m" %door_open_time_counter)
- door_open_time_counter +=1
- else:
- print("\033[31;1mdoor closed...\033[0m")
- door_open_time_counter = 0 #清空计时器
- door_swiping_event.wait()
- if door_open_time_counter >= 3:#门开了已经3s了,该关了
- door_swiping_event.clear()
- time.sleep(0.5)
- def staff(n):
- print("staff [%s] is comming..." % n )
- while True:
- if door_swiping_event.is_set():
- print("[%s] see door is opened, passing....." % n)
- break
- else:
- print("before set ",door_swiping_event.is_set())
- print("staff [%s] sees door got closed, swipping the card....." % n)
- door_swiping_event.set()
- print("after set ",door_swiping_event.is_set())
- time.sleep(0.5)
- door_swiping_event = threading.Event() #设置事件
- door_thread = threading.Thread(target=door)
- door_thread.start()
- for i in range(3):
- p = threading.Thread(target=staff,args=(i,))
- time.sleep(random.randrange(3))
- p.start()
刷卡开门
Timer
This class represents an action that should be run only after a certain amount of time has passed
Timers are started, as with threads, by calling their start()
method. The timer can be stopped (before its action has begun) by calling thecancel()
method. The interval the timer will wait before executing its action may not be exactly the same as the interval specified by the user.
- from threading import Timer
- def hello():
- print("hello, world")
- t = Timer(5.0, hello)
- t.start() # after 5 seconds, "hello, world" will be printed
- # t.cancel()
Python3学习之路~9.3 GIL、线程锁之Lock\Rlock\信号量、Event的更多相关文章
- Python3学习之路~0 目录
目录 Python3学习之路~2.1 列表.元组操作 Python3学习之路~2.2 简单的购物车程序 Python3学习之路~2.3 字符串操作 Python3学习之路~2.4 字典操作 Pytho ...
- Python3学习之路~10.1 多进程、进程间通信、进程池
一 多进程multiprocessing multiprocessing is a package that supports spawning processes using an API simi ...
- 全局解释器锁GIL & 线程锁
1.GIL锁(Global Interpreter Lock) Python代码的执行由Python虚拟机(也叫解释器主循环)来控制.Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行 ...
- Python3中最常用的5种线程锁你会用吗
前言 本章节将继续围绕threading模块讲解,基本上是纯理论偏多. 对于日常开发者来讲很少会使用到本章节的内容,但是对框架作者等是必备知识,同时也是高频的面试常见问题. 官方文档 线程安全 线程安 ...
- Python3学习之路~9.2 操作系统发展史介绍、进程与线程区别、线程语法、join、守护线程
一 操作系统发展史介绍 参考链接:http://www.cnblogs.com/alex3714/articles/5230609.html 二 进程与线程 进程: 对各种资源管理的集合 就可以称为进 ...
- Python3学习之路~8.5 SocketServer实现多并发
前面几节我们写的socket都只能实现服务端与一个客户端通信,并不能实现服务端与多客户端同时通信.接下来我们就来学习一下如何实现服务端同时与多个客户端通信,即并发. Socket Server soc ...
- Python3学习之路~9.4 队列、生产者消费者模型
一 队列queue 当必须在多个线程之间安全地交换信息时,队列在线程编程中特别有用. 队列的作用:1.解耦,使程序直接实现松耦合 2.提高处理效率 列表与队列都是有顺序的,但是他们之间有一个很大的区别 ...
- Python3学习之路~10.2 协程、Greenlet、Gevent
一 协程 协程,又称微线程,纤程.英文名Coroutine.一句话说明什么是线程:协程是一种用户态的轻量级线程. 协程拥有自己的寄存器上下文和栈.协程调度切换时,将寄存器上下文和栈保存到其他地方,在切 ...
- Python3学习之路~9.1 paramiko模块:实现ssh执行命令以及传输文件
我们一般使用linux的时候,都是在Windows上安装一个ssh客户端连接上去.那么从一台linux如何连接到另一条linux呢?使用ssh命令即可,因为每台linux机器自己都有一个ssh客户端. ...
随机推荐
- Python字符串编码——Unicode
ASCII码 我们知道,在计算机内部,所有的信息最终都表示为一个二进制的字符串.每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte).也就是 ...
- hexo博客的学习笔记
这篇文章主要的作用是作为 .md文件打开,内部的格式为一个初学者对hexo以及markdown语法运用的笔记 1.Hexo的写文格式 最开始为文章的属性部分,以三横杠-开始,-结束.里面记录了文章的标 ...
- 18岁,赚到了人生中的第一个10W!
大家好,我是九歌 今年我18岁,赚到了我人生中的第一个10W 截至2019年10月14日,我已经做了43天的公众号啦,粉丝也悄然增长到了1W8,感谢各位读者朋友给我的支持和鼓励. 相信大部分读者都是从 ...
- vagrant 虚拟机配置最佳实践
Mac VirtualBox Vagrant 管理虚拟机 这篇文章定位是在理解了 vagrant 相关概念之后,教你如何灵活玩转自己的虚拟机配置 本文为 @favoorr 常用的 Mac Virtua ...
- All Tips
Outlook分享心得 这是在爱奇艺的一场Outlook分享会上我记录的笔记. Read More 分享一点"关于应届生如何写简历"的人生经验 应届生如何写好一份求职简历是一件重要 ...
- 1、【Spark】Spark安装
本文基于的环境 Red Hat Linux Enterprise 7 x86_64 jdk 1.7.0_79 Python 2.7Spart spark-1.5.2-bin-hadoop2.6 官方要 ...
- tp5.1 请求时间格式化
当前时间:{$Request.time|date='Y-m-d H:i:s'} 注意database.php的配置!记录一下!
- .Net Core 使用jenkins 部署到远程服务器docker
最近一直在研究docker和kubernetes,小型的项目其实也用不到k8s,本文就是描述如何用jenkins 进行构建asp.net core 3.1项目并且发布到远程服务器docker容器中.本 ...
- 正式学习MVC 05
1.剃须刀模板razor的使用 1)混编 循环语法 @model List<MVCStudy.Models.Student> @{ ViewBag.Title = "List&q ...
- 前端每日实战:125# 视频演示如何用纯 CSS 创作一个失落的人独自行走的动画
效果预览 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/MqpOdR/ 可交互视频 此视频是 ...