Python并发编程(线程,Threading模块,守护线程,gil锁,)
一 背景知识
以前咱们已经了解了操做系统中进程的概念,程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。在多道编程中,咱们容许多个程序同时加载到内存中,在操做系统的调度下,能够实现并发地执行。这是这样的设计,大大提升了CPU的利用率。进程的出现让每一个用户感受到本身独享CPU,所以,进程就是为了在CPU上实现多道编程而提出的。php
2.有了进程为何还要线程html
#什么是线程:
#指的是一条流水线的工做过程,关键的一句话:一个进程内最少自带一个线程,其实进程根本不能执行,进程不是执行单位,是资源的单位,分配资源的单位
#线程才是执行单位
#进程:作手机屏幕的工做过程,刚才讲的
#咱们的py文件在执行的时候,若是你站在资源单位的角度来看,咱们称为一个主进程,若是站在代码执行的角度来看,它叫作主线程,只是一种形象的说法,其实整个代码的执行过程成为线程,也就是干这个活儿的自己称为线程,可是咱们后面学习的时候,咱们就称为线程去执行某个任务,其实那某个任务的执行过程称为一个线程,一条流水线的执行过程为线程
#进程vs线程
#1 同一个进程内的多个线程是共享该进程的资源的,不一样进程内的线程资源确定是隔离的
#2 建立线程的开销比建立进程的开销要小的多
#并发三个任务:1启动三个进程:由于每一个进程中有一个线程,可是我一个进程中开启三个线程就够了
#同一个程序中的三个任务须要执行,你是用三个进程好 ,仍是三个线程好?
#例子:
# pycharm 三个任务:键盘输入 屏幕输出 自动保存到硬盘
#若是三个任务是同步的话,你键盘输入的时候,屏幕看不到
#我们的pycharm是否是一边输入你边看啊,就是将串行变为了三个并发的任务
#解决方案:三个进程或者三个线程,哪一个方案可行。若是是三个进程,进程的资源是否是隔离的而且开销大,最致命的就是资源隔离,可是用户输入的数据还要给另一个进程发送过去,进程之间能直接给数据吗?你是否是copy一份给他或者通讯啊,可是数据是同一份,咱们有必要搞多个进程吗,线程是否是共享资源的,咱们是否是可使用多线程来搞,你线程1输入的数据,线程2能不能看到,你之后的场景仍是应用多线程多,并且起线程咱们说是否是很快啊,占用资源也小,还能共享同一个进程的资源,不须要将数据来回的copy!
进程有不少优势,它提供了多道编程,让咱们感受咱们每一个人都拥有本身的CPU和其余资源,能够提升计算机的利用率。不少人就不理解了,既然进程这么优秀,为何还要线程呢?其实,仔细观察就会发现进程仍是有不少缺陷的,主要体如今两点上:java
-
-
进程只能在一个时间干一件事,若是想同时干两件事或多件事,进程就无能为力了。python
-
进程在执行的过程当中若是阻塞,例如等待输入,整个进程就会挂起,即便进程中有些工做不依赖于输入的数据,也将没法执行。linux
-
若是这两个缺点理解比较困难的话,举个现实的例子也许你就清楚了:若是把咱们上课的过程当作一个进程的话,那么咱们要作的是耳朵听老师讲课,手上还要记笔记,脑子还要思考问题,这样才能高效的完成听课的任务。而若是只提供进程这个机制的话,上面这三件事将不能同时执行,同一时间只能作一件事,听的时候就不能记笔记,也不能用脑子思考,这是其一;若是老师在黑板上写演算过程,咱们开始记笔记,而老师忽然有一步推不下去了,阻塞住了,他在那边思考着,而咱们呢,也不能干其余事,即便你想趁此时思考一下刚才没听懂的一个问题都不行,这是其二。c++
如今你应该明白了进程的缺陷了,而解决的办法很简单,咱们彻底可让听、写、思三个独立的过程,并行起来,这样很明显能够提升听课的效率。而实际的操做系统中,也一样引入了这种相似的机制——线程。程序员
3.线程的出现算法
在传统操做系统中,每一个进程有一个地址空间,并且默认就有一个控制线程编程
线程顾名思义,就是一条流水线工做的过程,一条流水线必须属于一个车间,一个车间的工做过程是一个进程小程序
车间负责把资源整合到一块儿,是一个资源单位,而一个车间内至少有一个流水线
流水线的工做须要电源,电源就至关于cpu
因此,进程只是用来把资源集中到一块儿(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。
多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,至关于一个车间内有多条流水线,都共用一个车间的资源。
例如,北京地铁与上海地铁是不一样的进程,而北京地铁里的13号线是一个线程,北京地铁全部的线路共享北京地铁全部的资源,好比全部的乘客能够被全部线路拉。
二 进程和线程的关系
三 线程的特色
TCB包括如下信息: (1)线程状态。 (2)当线程不运行时,被保存的现场资源。 (3)一组执行堆栈。 (4)存放每一个线程的局部变量主存区。 (5)访问同一个进程中的主存和其它资源。 用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
四 线程的实际应用场景
开启一个字处理软件进程,该进程确定须要办不止一件事情,好比监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操做的都是同一块数据,于是不能用多进程。只能在一个进程里并发地开启三个线程,若是是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。
以前咱们将的socket是否是经过多进程去实现过呀,若是有500我的同时和我聊天,那我是否是要起500进程啊,能行吗?很差,对不对,那么怎么办,我就能够开几个进程,而后每一个进程里面开多个线程来处理多个请求和通讯。再举例:我用qq是一个进程,而后我和一我的聊天的时候,是否是还能够去接收别人给我发的消息啊,这个是否是并行的啊,就相似我一个进程开了多个线程来帮我并发接收消息。
五 内存中的线程
多个线程共享同一个进程的地址空间中的资源,是对一台计算机上多个进程的模拟,有时也称线程为轻量级的进程。
而对一台计算机上多个进程,则共享物理内存、磁盘、打印机等其余物理资源。多线程的运行也多进程的运行相似,是cpu在多个线程之间的快速切换。
不一样的进程之间是充满敌意的,彼此是抢占、竞争cpu的关系,若是迅雷会和QQ抢资源。而同一个进程是由一个程序员的程序建立,因此同一进程内的线程是合做关系,一个线程能够访问另一个线程的内存地址,你们都是共享的,一个线程干死了另一个线程的内存,那纯属程序员脑子有问题。
相似于进程,每一个线程也有本身的堆栈,不一样于进程,线程库没法利用时钟中断强制线程让出CPU,能够调用thread_yield运行线程自动放弃cpu,让另一个线程运行。
线程一般是有益的,可是带来了不小程序设计难度,线程的问题是:
1. 父进程有多个线程,那么开启的子线程是否须要一样多的线程
2. 在同一个进程中,若是一个线程关闭了文件,而另一个线程正准备往该文件内写内容呢?
所以,在多线程的代码中,须要更多的心思来设计程序的逻辑、保护程序的数据。
六 用户级线程和内核级线程(了解)
线程的实现能够分为两类:用户级线程(User-Level Thread)和内核线线程(Kernel-Level Thread),后者又称为内核支持的线程或轻量级进程。在多线程操做系统中,各个系统的实现方式并不相同,在有的系统中实现了用户级线程,有的系统中实现了内核级线程。
1.用户级线程
内核的切换由用户态程序本身控制内核切换,不须要内核干涉,少了进出内核态的消耗,但不能很好的利用多核Cpu。
在用户空间模拟操做系统对进程的调度,来调用一个进程中的线程,每一个进程中都会有一个运行时系统,用来调度线程。此时当该进程获取cpu时,进程内再调度出一个线程去执行,同一时刻只有一个线程执行。
2.内核级线程
内核级线程:切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态;能够很好的利用smp,即利用多核cpu。windows线程就是这样的。
3.用户级和内核级线程的对比
1 内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。 2 用户级线程的建立、撤消和调度不须要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的建立、撤消和调度都需OS内核提供支持,并且与进程的建立、撤消和调度大致是相同的。 3 用户级线程执行系统调用指令时将致使其所属进程被中断,而内核支持线程执行系统调用指令时,只致使该线程被中断。 4 在只有用户级线程的系统内,CPU调度仍是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。 5 用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是能够运行在任何状态下的程序
内核级线程的优缺点:
优势:当有多个处理机时,一个进程的多个线程能够同时执行。 缺点:由内核进行调度
用户级线程的优缺点:
优势: 线程的调度不须要内核直接参与,控制简单。 能够在不支持线程的操做系统中实现。 建立和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。 容许每一个进程定制本身的调度算法,线程管理比较灵活。 线程可以利用的表空间和堆栈空间比内核级线程多。 同一进程中只能同时有一个线程在运行,若是有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。另外,页面失效也会产生一样的问题。 缺点: 资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用
3.混合实现
用户级与内核级的多路复用,内核同一调度内核线程,每一个内核线程对应n个用户线程,用户和内核都能感知到的线程,用户建立一个线程,那么操做系统内核也跟着建立一个线程来专门执行你用户的这个线程。
在linux操做系统上也实现了这种混合的方式NPTL,看下面的介绍。
4.linux操做系统的NPTL
历史
在内核2.6之前的调度实体都是进程,内核并无真正支持线程。它是能过一个系统调用clone()来实现的,这个调用建立了一份调用进程的拷贝,跟fork()不一样的是,这份进程拷贝彻底共享了调用进程的地址空间。LinuxThread就是经过这个系统调用来提供线程在内核级的支持的(许多之前的线程实现都彻底是在用户态,内核根本不知道线程的存在)。很是不幸的是,这种方法有至关多的地方没有遵循POSIX标准,特别是在信号处理,调度,进程间通讯原语等方面。
很显然,为了改进LinuxThread必须获得内核的支持,而且须要重写线程库。为了实现这个需求,开始有两个相互竞争的项目:IBM启动的NGTP(Next Generation POSIX Threads)项目,以及Redhat公司的NPTL。在2003年的年中,IBM放弃了NGTP,也就是大约那时,Redhat发布了最初的NPTL。
NPTL最开始在redhat linux 9里发布,如今从RHEL3起内核2.6起都支持NPTL,而且彻底成了GNU C库的一部分。
设计
NPTL使用了跟LinuxThread相同的办法,在内核里面线程仍然被看成是一个进程,而且仍然使用了clone()系统调用(在NPTL库里调用)。可是,NPTL须要内核级的特殊支持来实现,好比须要挂起而后再唤醒线程的线程同步原语futex.
NPTL也是一个1*1的线程库,就是说,当你使用pthread_create()调用建立一个线程后,在内核里就相应建立了一个调度实体,在linux里就是一个新进程,这个方法最大可能的简化了线程的实现。
除NPTL的1*1模型外还有一个m*n模型,一般这种模型的用户线程数会比内核的调度实体多。在这种实现里,线程库自己必须去处理可能存在的调度,这样在线程库内部的上下文切换一般都会至关的快,由于它避免了系统调用转到内核态。然而这种模型增长了线程实现的复杂性,并可能出现诸如优先级反转的问题,此外,用户态的调度如何跟内核态的调度进行协调也是很难让人满意。
七 python与线程
1.全局解释器锁GIL(用一下threading模块以后再来看~~)
Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中能够“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
在多线程环境中,Python 虚拟机按如下方式执行:
a、设置 GIL;
b、切换到一个线程去运行;
c、运行指定数量的字节码指令或者线程主动让出控制(能够调用 time.sleep(0));
d、把线程设置为睡眠状态;
e、解锁 GIL;
d、再次重复以上全部步骤。
在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(因为在这期间没有Python的字节码被运行,因此不会作线程切换)编写扩展的程序员能够主动解锁GIL。
2.python线程模块的选择
Python提供了几个用于多线程编程的模块,包括thread、threading和Queue等。thread和threading模块容许程序员建立和管理线程。thread模块提供了基本的线程和锁的支持,threading提供了更高级别、功能更强的线程管理的功能。Queue模块容许用户建立一个能够用于多个线程之间共享数据的队列数据结构。
避免使用thread模块,由于更高级别的threading模块更为先进,对线程的支持更为完善,并且使用thread模块里的属性有可能会与threading出现冲突;其次低级别的thread模块的同步原语不多(实际上只有一个),而threading模块则有不少;再者,thread模块中当主线程结束时,全部的线程都会被强制结束掉,没有警告也不会有正常的清除工做,至少threading模块能确保重要的子线程退出后进程才退出。
就像咱们熟悉的time模块,它比其余模块更加接近底层,越是接近底层,用起来越麻烦,就像时间日期转换之类的就比较麻烦,可是后面咱们会学到一个datetime模块,提供了更为简便的时间日期处理方法,它是创建在time模块的基础上来的。又如socket和socketserver(底层仍是用的socket)等等,这里的threading就是thread的高级模块。
thread模块不支持守护线程,当主线程退出时,全部的子线程不论它们是否还在工做,都会被强行退出。而threading模块支持守护线程,守护线程通常是一个等待客户请求的服务器,若是没有客户提出请求它就在那等着,若是设定一个线程为守护线程,就表示这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。
Threading模块
multiprocess模块的彻底模仿了threading模块的接口,两者在使用层面,有很大的类似性,于是再也不详细介绍(官方连接)
咱们先简单应用一下threading模块来看看并发效果:
import time
from threading import Thread
#多线程并发,是否是看着和多进程很相似
def func(n):
time.sleep(1)
print(n)
#并发效果,1秒打印出了全部的数字
for i in range(10):
t = Thread(target=func,args=(i,))
t.start()
1.线程建立
from threading import Thread
import time
def sayhi(name):
time.sleep(2)
print('%s say hello' %name)
if __name__ == '__main__':
t=Thread(target=sayhi,args=('太白',))
t.start()
print('主线程')
方式1
import time
from threading import Thread
class Sayhi(Thread):
def __init__(self,name):
super().__init__()
self.name=name
def run(self):
time.sleep(2)
print('%s say hello' % self.name)
if __name__ == '__main__':
t = Sayhi('太白')
t.start()
print('主线程')
方式2
2.多线程与多进程
from threading import Thread
from multiprocessing import Process
import os
def work():
print('hello',os.getpid())
if __name__ == '__main__':
#part1:在主进程下开启多个线程,每一个线程都跟主进程的pid同样
t1=Thread(target=work)
t2=Thread(target=work)
t1.start()
t2.start()
print('主线程/主进程pid',os.getpid())
#part2:开多个进程,每一个进程都有不一样的pid
p1=Process(target=work)
p2=Process(target=work)
p1.start()
p2.start()
print('主线程/主进程pid',os.getpid())
pid(进程id)
那么哪些东西存在进程里,那些东西存在线程里呢?
进程:导入的模块、执行的python文件的文件所在位置、内置的函数、文件里面的这些代码、全局变量等等,而后线程里面有本身的堆栈(相似于一个列表,后进先出)和寄存器,里面存着本身线程的变量,操做(add)等等,占用的空间很小。
from threading import Thread
from multiprocessing import Process
import os
import time
def work():
print('hello')
if __name__ == '__main__':
s1 = time.time()
#在主进程下开启线程
t=Thread(target=work)
t.start()
t.join()
t1 = time.time() - s1
print('进程的执行时间:',t1)
print('主线程/主进程')
'''
打印结果:
hello
进程的执行时间: 0.0
主线程/主进程
'''
s2 = time.time()
#在主进程下开启子进程
t=Process(target=work)
t.start()
t.join()
t2 = time.time() - s2
print('线程的执行时间:', t2)
print('主线程/主进程')
'''
打印结果:
hello
线程的执行时间: 0.5216977596282959
主线程/主进程
'''
进程与线程开启效率比较
from threading import Thread
from multiprocessing import Process
import os
def work():
global n #修改全局变量的值
n=0
if __name__ == '__main__':
# n=100
# p=Process(target=work)
# p.start()
# p.join()
# print('主',n) #毫无疑问子进程p已经将本身的全局的n改为了0,但改的仅仅是它本身的,查看父进程的n仍然为100
n=1
t=Thread(target=work)
t.start()
t.join() #必须加join,由于主线程和子线程不必定谁快,通常都是主线程快一些,全部咱们要等子线程执行完毕才能看出效果
print('主',n) #查看结果为0,由于同一进程内的线程之间共享进程内的数据
# 经过一个global就实现了全局变量的使用,不须要进程的IPC通讯方法
内存数据共享
在这里咱们简单总结一下:
进程是最小的内存分配单位
线程是操做系统调度的最小党委
线程被CPU执行了
进程内至少含有一个线程
进程中能够开启多个线程
开启一个线程所须要的时间要远小于开启一个进程
多个线程内部有本身的数据栈,数据不共享
全局变量在多个线程之间是共享的
3.多线程实现socket(练习)
import multiprocessing
import threading
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(('127.0.0.1',8080))
s.listen(5)
def action(conn):
while True:
data=conn.recv(1024)
print(data)
msg = input('服务端输入:') #在多线程里面可使用input输入内容,那么就能够实现客户端和服务端的聊天了,多进程不能输入
conn.send(bytes(msg,encoding='utf-8'))
if __name__ == '__main__':
while True:
conn,addr=s.accept()
p=threading.Thread(target=action,args=(conn,))
p.start()
tcp_server.py
讲一讲代码
mport socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('127.0.0.1',8080))
while True:
msg=input('>>: ').strip()
if not msg:continue
s.send(msg.encode('utf-8'))
data=s.recv(1024)
print(data)
tcp_client.py
在socket通讯里面是否是有大量的I/O啊,recv、accept等等,咱们使用多线程效率更高,由于开销小。
4.Thread类的其余方法
Thread实例对象的方法
# isAlive(): 返回线程是否活动的。
# getName(): 返回线程名。
# setName(): 设置线程名。
threading模块提供的一些方法:
# threading.currentThread(): 返回当前的线程变量。
# threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
# threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果
其余方法
from threading import Thread
import threading
from multiprocessing import Process
import os
def work():
import time
time.sleep(3)
print(threading.current_thread().getName())
if __name__ == '__main__':
#在主进程下开启线程
t=Thread(target=work)
t.start()
print(threading.current_thread())#主线程对象
print(threading.current_thread().getName()) #主线程名称
print(threading.current_thread().ident) #主线程ID
print(threading.get_ident()) #主线程ID
print(threading.enumerate()) #连同主线程在内有两个运行的线程
print(threading.active_count())
print('主线程/主进程')
'''
打印结果:
<_MainThread(MainThread, started 14104)>
MainThread
14104
[<_MainThread(MainThread, started 14104)>, <Thread(Thread-1, started 17976)>]
主线程/主进程
Thread-1
'''
代码示例
from threading import Thread
import time
def sayhi(name):
time.sleep(2)
print('%s say hello' %name)
if __name__ == '__main__':
t=Thread(target=sayhi,args=('太白',))
t2=Thread(target=sayhi,args=('alex',))
t.start()
t2.start()
t.join() #由于这个线程用了join方法,主线程等待子线程的运行结束
print('主线程')
print(t.is_alive()) #因此t这个线程确定是执行结束了,结果为False
print(t2.is_alive()) #有多是True,有多是False,看子线程和主线程谁执行的快
'''
egon say hello
主线程
False
'''
join方法
5.守护线程
不管是进程仍是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。须要强调的是:运行完毕并不是终止运行
#1.对主进程来讲,运行完毕指的是主进程代码运行完毕
#2.对主线程来讲,运行完毕指的是主线程所在的进程内全部非守护线程通通运行完毕,主线程才算运行完毕
from threading import Thread
import time
def sayhi(name):
time.sleep(2)
print('%s say hello' %name)
if __name__ == '__main__':
t=Thread(target=sayhi,args=('taibai',))
t.setDaemon(True) #必须在t.start()以前设置
t.start()
print('主线程')
print(t.is_alive())
'''
主线程
True
'''
守护线程示例1
from threading import Thread
from multiprocessing import Process
import time
def func1():
while True:
print(666)
time.sleep(0.5)
def func2():
print('hello')
time.sleep(3)
if __name__ == '__main__':
# t = Thread(target=func1,)
# t.daemon = True #主线程结束,守护线程随之结束
# # t.setDaemon(True) #两种方式,和上面设置守护线程是同样的
# t.start()
# t2 = Thread(target=func2,) #这个子线程要执行3秒,主线程的代码虽然执行完了,可是一直等着子线程的任务执行完毕,主线程才算完毕,由于经过结果你会发现我主线程虽然代码执行完毕了,\
# 可是主线程的的守护线程t1还在执行,说明什么,说明个人主线程尚未完毕,只不过是代码执行完了,一直等着子线程t2执行完毕,我主线程的守护线程才中止,说明子线程执行完毕以后,个人主线程才执行完毕
# t2.start()
# print('主线程代码执行完啦!')
p = Process(target=func1,)
p.daemon = True
p.start()
p2 = Process(target=func2,)
p2.start()
time.sleep(1) #让主进程等1秒,为了能看到func1的打印效果
print('主进程代码执行完啦!') #经过结果你会发现,若是主进程的代码运行完毕了,那么主进程就结束了,由于主进程的守护进程p随着主进程的代码结束而结束了,守护进程被回收了,这和线程是不同的,主线程的代码完了并不表明主线程运行完毕了,须要等着全部其余的非守护的子线程执行完毕才算完毕
守护线程示例2
锁
1.GIL锁(Global Interpreter Lock)
首先,一些语言(java、c++、c)是支持同一个进程中的多个线程是能够应用多核CPU的,也就是咱们会听到的如今4核8核这种多核CPU技术的牛逼之处。那么咱们以前说过应用多进程的时候若是有共享数据是否是会出现数据不安全的问题啊,就是多个进程同时一个文件中去抢这个数据,你们都把这个数据改了,可是还没来得及去更新到原来的文件中,就被其余进程也计算了,致使数据不安全的问题啊,因此咱们是否是经过加锁能够解决啊,多线程你们想一下是否是同样的,并发执行就是有这个问题。可是python最先期的时候对于多线程也加锁,可是python比较极端的(在当时电脑cpu确实只有1核)加了一个GIL全局解释锁,是解释器级别的,锁的是整个线程,而不是线程里面的某些数据操做,每次只能有一个线程使用cpu,也就说多线程用不了多核,可是他不是python语言的问题,是CPython解释器的特性,若是用Jpython解释器是没有这个问题的,Cpython是默认的,由于速度快,Jpython是java开发的,在Cpython里面就是没办法用多核,这是python的弊病,历史问题,虽然众多python团队的大神在致力于改变这个状况,可是暂没有解决。(这和解释型语言(python,php)和编译型语言有关系吗???待定!,编译型语言通常在编译的过程当中就帮你分配好了,解释型要边解释边执行,因此为了防止出现数据不安全的状况加上了这个锁,这是全部解释型语言的弊端??)
可是有了这个锁咱们就不能并发了吗?当咱们的程序是偏计算的,也就是cpu占用率很高的程序(cpu一直在计算),就不行了,可是若是你的程序是I/O型的(通常你的程序都是这个)(input、访问网址网络延迟、打开/关闭文件读写),在什么状况下用的到高并发呢(金融计算会用到,人工智能(阿尔法狗),可是通常的业务场景用不到,爬网页,多用户网站、聊天软件、处理文件),I/O型的操做不多占用CPU,那么多线程仍是能够并发的,由于cpu只是快速的调度线程,而线程里面并无什么计算,就像一堆的网络请求,我cpu很是快速的一个一个的将你的多线程调度出去,你的线程就去执行I/O操做了,
2.同步锁
三个须要注意的点:
#1.线程抢的是GIL锁,GIL锁至关于执行权限,拿到执行权限后才能拿到互斥锁Lock,其余线程也能够抢到GIL,但若是发现Lock仍然没有被释放则阻塞,即使是拿到执行权限GIL也要马上交出来
#2.join是等待全部,即总体串行,而锁只是锁住修改共享数据的部分,即部分串行,要想保证数据安全的根本原理在于让并发变成串行,join与互斥锁均可以实现,毫无疑问,互斥锁的部分串行效率要更高
#3. 必定要看本小节最后的GIL与互斥锁的经典分析
GIL VS Lock
机智的同窗可能会问到这个问题,就是既然你以前说过了,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早期版本的遗留问题。
看一段代码:解释为何要加锁,若是下面代码中work函数里面的那个time.sleep(0.005),个人电脑用的这个时间片断,每次运行都呈现不一样的结果,咱们能够改改时间试一下。
from threading import Thread,Lock
import os,time
def work():
global n
# lock.acquire() #加锁
temp=n
time.sleep(0.1) #一会将下面循环的数据加大而且这里的时间改的更小试试
n=temp-1
# time.sleep(0.02)
# n = n - 1
'''若是这样写的话看不出来效果,由于这样写就至关于直接将n的指向改了,就比如从10,通过1次减1以后,n就直接指向了9,速度太快,看不出效果,那么咱们怎么办呢,找一个中间变量来接收n,而后对这个中间变量进行修改,而后再赋值给n,多一个给n赋值的过程,那么在这个过程当中间,咱们加上一点阻塞时间,来看效果,就像读文件修改数据以后再写回文件的过程。那么这个程序就会出现结果为9的状况,首先一个进程的全局变量对于全部线程是共享的,因为咱们在程序给中间变量赋值,而后给n再次赋值的过程当中咱们加了一些I/O时间,遇到I/O就切换,那么每一个线程都拿到了10,并对10减1了,而后你们都获得了9,而后再赋值给n,全部n等于了9'''
# lock.release()
if __name__ == '__main__':
lock=Lock()
n=100
l=[]
# for i in range(10000): #若是这里变成了10000,你在运行一下看看结果
for i in range(100): #若是这里变成了10000,你在运行一下看看结果
p=Thread(target=work)
l.append(p)
p.start()
for p in l:
p.join()
print(n) #结果确定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全
须要仔细研究的示例
上面这个代码示例,若是循环次数变成了10000,在个人电脑上就会出现不一样的结果,由于在线程切换的那个time.sleep的时间内,有些线程尚未被切换到,也就是有些线程尚未拿到n的值,因此计算结果就没准了。
锁一般被用来实现对共享资源的同步访问。为每个共享资源建立一个Lock对象,当你须要访问该资源时,调用acquire方法来获取锁对象(若是其它线程已经得到了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁:
import threading
R=threading.Lock()
R.acquire() #
#R.acquire()若是这里还有一个acquire,你会发现,程序就阻塞在这里了,由于上面的锁已经被拿到了而且尚未释放的状况下,再去拿就阻塞住了
'''
对公共数据的操做
'''
R.release()
经过上面的代码示例1,咱们看到多个线程抢占资源的状况,能够经过加锁来解决,看代码:
from threading import Thread,Lock
import os,time
def work():
global n
lock.acquire() #加锁
temp=n
time.sleep(0.1)
n=temp-1
lock.release()
if __name__ == '__main__':
lock=Lock()
n=100
l=[]
for i in range(100):
p=Thread(target=work)
l.append(p)
p.start()
for p in l:
p.join()
print(n) #结果确定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全
同步锁的引用
看上面代码的图形解释:
分析:
#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 current_thread,Thread,Lock
import os,time
def task():
global n
print('%s is running' %current_thread().getName())
temp=n
time.sleep(0.5)
n=temp-1
if __name__ == '__main__':
n=100
lock=Lock()
threads=[]
start_time=time.time()
for i in range(100):
t=Thread(target=task)
threads.append(t)
t.start()
for t in threads:
t.join()
stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n))
'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:0.5216062068939209 n:99
'''
#不加锁:未加锁部分并发执行,加锁部分串行执行,速度慢,数据安全
from threading import current_thread,Thread,Lock
import os,time
def task():
#未加锁的代码并发运行
time.sleep(3)
print('%s start to run' %current_thread().getName())
global n
#加锁的代码串行运行
lock.acquire()
temp=n
time.sleep(0.5)
n=temp-1
lock.release()
if __name__ == '__main__':
n=100
lock=Lock()
threads=[]
start_time=time.time()
for i in range(100):
t=Thread(target=task)
threads.append(t)
t.start()
for t in threads:
t.join()
stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n))
'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:53.294203758239746 n:0
'''
#有的同窗可能有疑问:既然加锁会让运行变成串行,那么我在start以后当即使用join,就不用加锁了啊,也是串行的效果啊
#没错:在start以后马上使用jion,确定会将100个任务的执行变成串行,毫无疑问,最终n的结果也确定是0,是安全的,但问题是
#start后当即join:任务内的全部代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的
#单从保证数据安全方面,两者均可以实现,但很明显是加锁的效率更高.
from threading import current_thread,Thread,Lock
import os,time
def task():
time.sleep(3)
print('%s start to run' %current_thread().getName())
global n
temp=n
time.sleep(0.5)
n=temp-1
if __name__ == '__main__':
n=100
lock=Lock()
start_time=time.time()
for i in range(100):
t=Thread(target=task)
t.start()
t.join()
stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n))
'''
Thread-1 start to run
Thread-2 start to run
......
Thread-100 start to run
主:350.6937336921692 n:0 #耗时是多么的恐怖
'''
互斥锁与join的区别(重点)
3.死锁与递归锁
进程也有死锁与递归锁,在进程那里忘记说了,放到这里一切说了额,进程的死锁和线程的是同样的,并且通常状况下进程之间是数据不共享的,不须要加锁,因为线程是对全局的数据共享的,因此对于全局的数据进行操做的时候,要加锁。
所谓死锁: 是指两个或两个以上的进程或线程在执行过程当中,因争夺资源而形成的一种互相等待的现象,若无外力做用,它们都将没法推动下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,以下就是死锁
from threading import Lock as Lock
import time
mutexA=Lock()
mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()
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)
#分析:当线程1执行完func1,而后执行到这里的时候,拿到了B锁,线程2执行func1的时候拿到了A锁,那么线程2还要继续执行func1里面的代码,再去拿B锁的时候,发现B锁被人拿了,那么就一直等着别人把B锁释放,那么就一直等着,等到线程1的sleep时间用完以后,线程1继续执行func2,须要拿A锁了,可是A锁被线程2拿着呢,尚未释放,由于他在等着B锁被释放,那么这俩人就尴尬了,你拿着个人老A,我拿着你的B,这就尴尬了,俩人就停在了原地
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提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源能够被屡次require。直到一个线程全部的acquire都被release,其余的线程才能得到资源。上面的例子若是使用RLock代替Lock,则不会发生死锁:
from threading import RLock as Lock
import time
mutexA=Lock()
mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()
典型问题:科学家吃面 ,看下面代码示例:
import time
from threading import Thread,Lock
noodle_lock = Lock()
fork_lock = Lock()
def eat1(name):
noodle_lock.acquire()
print('%s 抢到了面条'%name)
fork_lock.acquire()
print('%s 抢到了叉子'%name)
print('%s 吃面'%name)
fork_lock.release()
noodle_lock.release()
def eat2(name):
fork_lock.acquire()
print('%s 抢到了叉子' % name)
time.sleep(1)
noodle_lock.acquire()
print('%s 抢到了面条' % name)
print('%s 吃面' % name)
noodle_lock.release()
fork_lock.release()
for name in ['taibai','egon','wulaoban']:
t1 = Thread(target=eat1,args=(name,))
t2 = Thread(target=eat2,args=(name,))
t1.start()
t2.start()
和上面更难一些的死锁现象是同样的
import time
from threading import Thread,RLock
fork_lock = noodle_lock = RLock()
def eat1(name):
noodle_lock.acquire()
print('%s 抢到了面条'%name)
fork_lock.acquire()
print('%s 抢到了叉子'%name)
print('%s 吃面'%name)
fork_lock.release()
noodle_lock.release()
def eat2(name):
fork_lock.acquire()
print('%s 抢到了叉子' % name)
time.sleep(1)
noodle_lock.acquire()
print('%s 抢到了面条' % name)
print('%s 吃面' % name)
noodle_lock.release()
fork_lock.release()
for name in ['taibai','wulaoban']:
t1 = Thread(target=eat1,args=(name,))
t1.start()
for name in ['alex','peiqi']:
t2 = Thread(target=eat2,args=(name,))
t2.start()
递归锁解决死锁问题
递归锁大体描述: 当咱们的程序中须要两把锁的时候,你就要注意,别出现死锁,最好就去用递归锁。