并发编程之多线程篇之三
主要内容:
一、GIL全局解释器锁
二、死锁现象和递归锁
1️⃣ GIL全局解释器锁
1、Cpython的GIL解释器锁的工作机制
1.1 在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势。 需要明确的一点是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。 1.2 GIL的介绍
GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制 同一时间内共享数据只能被一个任务所修改,进而保证数据安全。可以肯定的一点是:保护不同的数据的安全,就 应该加不同的锁。要想了解GIL,首先确定一点:每次执行python程序,都会产生一个独立的进程。例如python test1.py, python test2.py,python test3.py会产生3个不同的python进程。 验证进程的方法:
#test.py内容 import os,time print(os.getpid()) time.sleep(1000)#打开终端执行 python3 test.py#在windows下查看 tasklist |findstr python#在linux下下查看 ps aux |grep python
View Code
在一个python的进程内,不仅有test.py的主线程或者由该主线程开启的其他线程,还有解释器开启的
垃圾回收等解释器级别的线程,总之,毫无疑问,所有线程都运行在这一个进程内。
1.3 GIL原理分析
第一点:
所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码)例如:test.py定义一个函数work(代码内容如下图),在进程内所有线程都能访问到work的代码,于是我们可以开启三个线程 然后target都指向该代码,能访问到意味着就是可以执行。 第二点:
所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务, 首先需要解决的是能够访问到解释器的代码。 由以上两点不难得知:
如果多个线程的target=work,那么执行流程是多个线程先访问到解释器的代码,即拿到执行权限, 然后将target的代码交给解释器的代码去执行。 重点:
解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了一个问题:对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,解决这种问题没有什么高明的方法,就是加锁处理,即GIL,保证python解释器同一时间只能执行一个任务的代码。
如图所示:
2、 GIL 和 Lock Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock? 我们已经知道:锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据。保护不同的数据就应该加不同的锁。 所以,解释就是GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据), 后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock。如下图:
解释如下:
1、100个线程去抢GIL锁,即抢执行权限 2、肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire() 3、极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,被迫交出执行权限,即释放GIL 4、直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程
代码示例:
from threading import Thread,Lock import os,time def work():global nlock.acquire()temp=ntime.sleep(0.1)n=temp-1lock.release() if __name__ == '__main__':lock=Lock()n=100l=[]for i in range(100):p=Thread(target=work)l.append(p)p.start()for p in l:p.join()print(n) #结果肯定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全,不加锁则结果可能为99
3、
GIL与多线程
有了GIL的存在,意味着同一时刻同一进程中只有一个线程被执行。 我们已经知道进程可以利用多核,但是开销大,而python的多线程开销小,但却无法利用多核优势。也许你会认为那python多线程还有什么存在的意义呢?压根毛线用没有。。。 可事实并非如此... 在开讲之前,我们先来关注一下这几点:
1、cpu到底是用来做计算的,还是用来做I/O的?2、多cpu,意味着可以有多个核并行完成计算,所以多核提升的是计算性能3、每个cpu一旦遇到I/O阻塞,仍然需要等待,所以多核对I/O操作没什么用处
通俗的来理解的话,可以把一个工人比作CPU,
此时计算相当于工人在干活,I/O阻塞相当于为工人干活提供所需原材料的过程,工人干活的过程中如果没有原材料了,
则工人干活的过程需要停止,直到等待原材料的到来。如果你的工厂干的大多数任务都要有准备原材料的过程(I/O密集型),
那么你有再多的工人,意义也不大,还不如一个人,在等材料的过程中让工人去干别的活,反过来讲,如果你的工厂原材料都齐全,
那当然是工人越多,效率越高。
由此我们可知:
1、对计算来说,cpu越多越好,但是对于I/O来说,再多的cpu也没用 2、当然对运行一个程序来说,随着cpu的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析python的多线程到底有无用武之地
假设我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:
方案一:开启四个进程 方案二:一个进程下,开启四个线程
单核情况下,分析结果:
如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜 如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜
多核情况下,分析结果:
如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜 如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜
结论: 现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,
甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
实例测试:
1、 计算密集型:用多进程
#!/usr/bin/env python3 #-*- coding:utf-8 -*- # write by congcongfrom multiprocessing import Process from threading import Thread import time,osdef task():res = 10for i in range(10000000):res *= iif __name__ == '__main__':t_list = []print(os.cpu_count()) # 显示本机cpu核心数 4start = time.time()for i in range(16):p = Process(target=task) # 耗时 6.345133304595947#p = Thread(target=task) # 耗时:9.412602186203003 t_list.append(p)p.start()for p in t_list:p.join()stop = time.time()print('Running time is %s'%(stop-start))
View Code
2、IO密集型:用多线程
#!/usr/bin/env python3 #-*- coding:utf-8 -*- # write by congcongfrom multiprocessing import Process from threading import Thread import time,osdef work():time.sleep(2)if __name__ == '__main__':w_list = []start = time.time()for i in range(1000):p = Thread(target=work) # 耗时:2.1222898960113525#p = Process(target=work) # 耗时:255.03309321403503 大部分时间都消耗在创建进程上 w_list.append(p)p.start()for p in w_list:p.join()stop = time.time()print('Running time is %s'%(stop-start))
View Code
3、实际应用
多线程用于IO密集型,如socket,爬虫,web;
多进程用于计算密集型,如金融分析。
2️⃣ 死锁现象和递归锁
1、死锁现象
所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,
若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的
进程称为死锁进程,如下就是死锁:
#!/usr/bin/env python3 #-*- coding:utf-8 -*- # write by congcongfrom threading import Thread,Lock,RLock import timemuteA = Lock() muteB = Lock()class MyThread(Thread):def run(self): # run方法是固定方法 self.f1()self.f2()def f1(self):muteA.acquire()print('%s 拿到了锁A'%self.name)muteB.acquire()print('%s 拿到了锁B'%self.name)muteB.release()muteA.release()def f2(self):muteB.acquire()print('%s 拿到了锁B'%self.name)time.sleep(1)muteA.acquire()print('%s 拿到了锁A'%self.name)muteA.release()muteB.release() if __name__ == '__main__':for t in range(10):t = MyThread()t.start()
输出结果:
Thread-1 拿到了锁A Thread-1 拿到了锁B Thread-1 拿到了锁B Thread-2 拿到了锁A原因分析:此时线程卡住不动,即死锁(原因:Thread-1拿到了锁A,拿到了锁B完全没有竞争,Thread-1把f1执行完, 此时锁A锁B都释放,接着执行f2去拿锁B,则Thread-2此时可以拿到锁A,Thread-2又准备接着拿锁B,但锁B在Thread-1手中 ,即使Thread-1 停止休眠了,去取锁A,但锁A在Thread-2手中,所以两个线程都无法继续运行)
2、递归锁
针对上述死锁现象的解决方法就是递归锁。
递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源
可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子
如果使用RLock代替Lock,则不会发生死锁。
互斥锁和递归锁的区别:
递归锁可以连续acquire多次,而互斥锁只能acquire一次。
代码实例如下:
#!/usr/bin/env python3 #-*- coding:utf-8 -*- # write by congcong# 递归锁from threading import Thread,Lock,RLock import timemuteA = muteB = RLock() # 此时muteA和muteB是同一个互斥锁,初始锁为0,每acquire一次,自加1,每release一次,自减1,仅当值为0时,其他线程才能再获得锁运行 class MyThread(Thread):def run(self): # run方法是固定方法 self.f1()self.f2()def f1(self):muteA.acquire() # 锁为1print('%s 拿到了锁A' % self.name)muteB.acquire() # 锁为2print('%s 拿到了锁B' % self.name)muteB.release() # 锁减1 muteA.release() # 锁减1,变为0,完全释放def f2(self):muteB.acquire()print('%s 拿到了锁B' % self.name)time.sleep(1)muteA.acquire()print('%s 拿到了锁A' % self.name)muteA.release()muteB.release()if __name__ == '__main__':for t in range(5):t = MyThread()t.start()
思考一下,看你想的是否与下述结果相同:
Thread-1 拿到了锁A Thread-1 拿到了锁B Thread-1 拿到了锁B Thread-1 拿到了锁A Thread-2 拿到了锁A Thread-2 拿到了锁B Thread-2 拿到了锁B Thread-2 拿到了锁A Thread-4 拿到了锁A Thread-4 拿到了锁B Thread-4 拿到了锁B Thread-4 拿到了锁A Thread-3 拿到了锁A Thread-3 拿到了锁B Thread-3 拿到了锁B Thread-3 拿到了锁A Thread-5 拿到了锁A Thread-5 拿到了锁B Thread-5 拿到了锁B Thread-5 拿到了锁A
View Code
转载于:https://www.cnblogs.com/schut/p/9022335.html
并发编程之多线程篇之三相关推荐
- 并发编程之多线程篇之四
主要内容: 一.信号量 二.Event事件 三.定时器 四.线程queue 五.进程池与线程池 1️⃣ 信号量 1.信号量的理解 信号量也是一把锁,可以指定信号量为5,对比互斥锁同一时间只能有一个任务 ...
- 安琪拉教百里守约学并发编程之多线程基础
<安琪拉与面试官二三事>系列文章 一个HashMap能跟面试官扯上半个小时 一个synchronized跟面试官扯了半个小时 <安琪拉教鲁班学算法>系列文章 安琪拉教鲁班学算法 ...
- c+++11并发编程语言,C++11并发编程:多线程std:thread
原标题:C++11并发编程:多线程std:thread 一:概述 C++11引入了thread类,大大降低了多线程使用的复杂度,原先使用多线程只能用系统的API,无法解决跨平台问题,一套代码平台移植, ...
- week6 day4 并发编程之多线程 理论
week6 day4 并发编程之多线程 理论 一.什么是线程 二.线程的创建开销小 三.线程和进程的区别 四.为何要用多线程 五.多线程的应用举例 六.经典的线程模型(了解) 七.POSIX线程(了解 ...
- Java-gt;Android并发编程引气入门篇
Android的并发编程,即多线程开发,而Android的多线程开发模型也是源于Java中的多线程模型. 所以本篇也会先讲一些Java中的多线程理念,再讲解具体涉及的类,最后深入Android中的并发 ...
- Python 并发编程:PoolExecutor 篇
个人笔记,如有疏漏,还请指正. 使用多线程(threading)和多进程(multiprocessing)完成常规的并发需求,在启动的时候 start.join 等步骤不能省,复杂的需要还要用 1-2 ...
- java并发编程艺术——基础篇
这篇文章目的是为了总结一下这段时间看<java并发编程艺术>学到的东西,尝试用自己的话说出来对java多线程的理解和使用. 一.什么是多线程,为什么要用多线程,多线程带来的挑战 多线程定义 ...
- 【Java 并发编程】多线程、线程同步、死锁、线程间通信(生产者消费者模型)、可重入锁、线程池
并发编程(Concurrent Programming) 进程(Process).线程(Thread).线程的串行 多线程 多线程的原理 多线程的优缺点 Java并发编程 默认线程 开启新线程 `Ru ...
- 并发编程(一)多线程基础和原理
多线程基础 最近,准备回顾下多线程相关的知识体系,顺便在这里做个记录. 并发的发展历史 最早的计算机只能解决简单的数学运算问题,比如正弦. 余弦等.运行方式:程序员首先把程序写到纸上,然后穿 孔成卡片 ...
最新文章
- vue中常碰见的坑_Vue 与 Vuex 的第一次接触遇到的坑
- 绝对干货,教你4分钟插入1000万条数据到mysql数据库表,快快进来
- 不好好学C++还想做好算法?
- 将redis当做使用LRU算法的缓存来使用
- 使用Xcode 7 beta免费真机调试iOS应用程序
- maven中snapshot快照库与maven-metadata.xml
- 科技情报研究所工资_我们所说的情报是什么?
- CCNP-第十五篇-VXLAN(一)
- 一、optimizer_trace介绍
- 从零开始搭二维激光SLAM --- 基于g2o的后端优化的代码实现
- 用jsp编写一个猜26个小写英文字母的web小游戏
- Win10——使用WePE工具U盘重装系统
- STM32单片机最小系统
- eclipse之 Type Hierachy:Viewing the type hierarchy
- cups ipp oracle,Linux打印系统CUPS原理分析
- wps word中如何快速到目录页面
- DRV8824,DRV8825新的解决方案
- linode购买服务器
- mysql备份 1044_Navicat访问MySQL出现1044/1045错误的解决方法
- myrio与fpga编程_LabVIEW编程及MyRIO使用.ppt