第九章
- 线程
- python GIL全局解释器锁
- 线程
- 语法
- join
- 线程锁之LockRlock信号量
- 将线程变为守护进程
- Event事件
- queue队列
- 生产者消费者模型
- Queue队列
线程
一.什么是线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
一个线程是一个执行上下文,这是所有的信息,一个处理器需要执行一个指令流。
假设你正在读一本书,你想现在休息,但你想能够回来,并恢复阅读从确切的点,你停了。一个实现的方法就是记下页码,行数和字数。因此,你读一本书的执行上下文是这3个数字。
如果你有一个室友,她使用相同的技术,她可以把这本书,而你不使用它,并恢复阅读从她停下来的地方。然后你可以把它回来,从你的地方恢复。
线程以同样的方式工作。一个中央处理器给你的错觉,它同时做多个计算。它通过在每一个计算上花费一点时间。它可以这样做,因为它有一个每个计算的执行上下文。就像你可以和你的朋友分享一本书,许多任务可以共享一个处理器。
在一个更技术的层面上,一个执行上下文(因此一个线程)由中央处理器的寄存器的值组成。
最后:线程与进程不同。线程是一个执行的上下文,而一个进程是一堆与计算相关的资源。一个进程可以有一个或多个线程。
澄清:与进程相关联的资源包括内存页(进程中的所有线程都有相同的内存视图),文件描述符(例如,打开的套接字),和安全凭据(例如,启动进程的用户的身份)。
二.什么是过程
一个程序的一个执行实例被称为一个过程。
每个进程提供执行程序所需的资源。一个进程有一个虚拟地址空间,可执行代码,开放的处理系统对象,一个安全上下文,一个独特的过程标识符,环境变量,优先级类,最小和最大工作集大小,和至少一个执行线程。每个进程都是开始于一个单一的线程,通常被称为主线程,但可以创建额外的线程从它的任何线程。
三.线程和进程的区别
1.线程共享创建它的进程的地址空间;进程有自己的地址空间。
2.线程对其进程的数据段有直接的访问;进程有自己的父进程的数据段的副本。
3.线程可以直接与其他线程的过程;过程必须使用进程间通信与兄弟姐妹的过程。
4.新的线程很容易创建;新的进程需要重复的父进程。
5.线程可以对同一进程的线程进行相当程度的控制,过程只能对子进程进行控制。
6.更改主线程(取消、优先级更改等)可能会影响进程的其他线程的行为;父进程的更改不影响子进程。
python GIL全局解释器锁
在CPython,全局解释器锁,或吉尔,是一个互斥体防止多个本地线程执行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
Python threading模块
调用
直接调用:
1 import threading 2 import time 3 4 def sayhi(num): #定义每个线程要运行的函数 5 6 print("running on number:%s" %num) 7 8 time.sleep(3) 9 10 if __name__ == '__main__': 11 12 t1 = threading.Thread(target=sayhi,args=(1,)) #生成一个线程实例 13 t2 = threading.Thread(target=sayhi,args=(2,)) #生成另一个线程实例 14 15 t1.start() #启动线程 16 t2.start() #启动另一个线程 17 18 print(t1.getName()) #获取线程名 19 print(t2.getName())
继承调用:
1 import threading 2 import time 3 4 5 class MyThread(threading.Thread): 6 def __init__(self,num): 7 threading.Thread.__init__(self) 8 self.num = num 9 10 def run(self):#定义每个线程要运行的函数 11 12 print("running on number:%s" %self.num) 13 14 time.sleep(3) 15 16 if __name__ == '__main__': 17 18 t1 = MyThread(1) 19 t2 = MyThread(2) 20 t1.start() 21 t2.start()
线程锁(互斥锁Mutex)
一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况?
1 import time 2 import threading 3 4 def addNum(): 5 global num #在每个线程中都获取这个全局变量 6 print('--get num:',num ) 7 time.sleep(1) 8 num -=1 #对此公共变量进行-1操作 9 10 num = 100 #设定一个共享变量 11 thread_list = [] 12 for i in range(100): 13 t = threading.Thread(target=addNum) 14 t.start() 15 thread_list.append(t) 16 17 for t in thread_list: #等待所有线程执行完毕 18 t.join() 19 20 21 print('final num:', num )
正常来讲,这个num结果应该是0, 但在python 2.7上多运行几次,会发现,最后打印出来的num结果不总是0,为什么每次运行的结果不一样呢? 哈,很简单,假设你有A,B两个线程,此时都 要对num 进行减1操作, 由于2个线程是并发同时运行的,所以2个线程很有可能同时拿走了num=100这个初始变量交给cpu去运算,当A线程去处完的结果是99,但此时B线程 运算完的结果也是99,两个线程同时CPU运算的结果再赋值给num变量后,结果就都是99。那怎么办呢? 很简单,每个线程在要修改公共数据时,为了避免自己在还没改完的时候别人也来修改此数据,可以给这个数据加一把锁, 这样其它线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据。
*注:不要在3.x上运行,不知为什么,3.x上的结果总是正确的,可能是自动加了锁
加锁版本
1 import time 2 import threading 3 4 def addNum(): 5 global num #在每个线程中都获取这个全局变量 6 print('--get num:',num ) 7 time.sleep(1) 8 lock.acquire() #修改数据前加锁 9 num -=1 #对此公共变量进行-1操作 10 lock.release() #修改后释放 11 12 num = 100 #设定一个共享变量 13 thread_list = [] 14 lock = threading.Lock() #生成全局锁 15 for i in range(100): 16 t = threading.Thread(target=addNum) 17 t.start() 18 thread_list.append(t) 19 20 for t in thread_list: #等待所有线程执行完毕 21 t.join() 22 23 print('final num:', num )
GIL VS Lock
机智的同学可能会问到这个问题,就是既然你之前说过了,Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要 lock? 注意啦,这里的lock是用户级的lock,跟那个GIL没关系 ,具体我们通过下图来看一下+配合我现场讲给大家,就明白了。
RLock(递归锁)
说白了就是在一个大锁中还要再包含子锁
1 import threading,time 2 3 def run1(): 4 print("grab the first part data") 5 lock.acquire() 6 global num 7 num +=1 8 lock.release() 9 return num 10 def run2(): 11 print("grab the second part data") 12 lock.acquire() 13 global num2 14 num2+=1 15 lock.release() 16 return num2 17 def run3(): 18 lock.acquire() 19 res = run1() 20 print('--------between run1 and run2-----') 21 res2 = run2() 22 lock.release() 23 print(res,res2) 24 25 26 if __name__ == '__main__': 27 28 num,num2 = 0,0 29 lock = threading.RLock() 30 for i in range(10): 31 t = threading.Thread(target=run3) 32 t.start() 33 34 while threading.active_count() != 1: 35 print(threading.active_count()) 36 else: 37 print('----all threads done---') 38 print(num,num2)
Semaphore(信号量)
互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。
1 import threading,time 2 3 def run(n): 4 semaphore.acquire() 5 time.sleep(1) 6 print("run the thread: %s " %n) 7 semaphore.release() 8 9 if __name__ == '__main__': 10 11 num= 0 12 semaphore = threading.BoundedSemaphore(5) #最多允许5个线程同时运行 13 for i in range(20): 14 t = threading.Thread(target=run,args=(i,)) 15 t.start() 16 17 while threading.active_count() != 1: 18 pass #print threading.active_count() 19 else: 20 print('----all threads done---') 21 print(num)
Events
一个事件是一个简单的同步对象;
该事件表示一个内部标志,和线程
可以等待要设置的标志,或设置或清除标志本身。
事件= event()线程。
#客户端线程可以等待标志被设置
wait()事件。
#服务器线程可以设置或重置
set()事件。
clear()事件。
如果设置了标志,则等待方法不做任何事情。
如果该标志被清除,等待将阻止,直到它再次被设置。
任意数量的线程可以等待相同的事件。
通过Event来实现两个或多个线程间的交互,下面是一个红绿灯的例子,即起动一个线程做交通指挥灯,生成几个线程做车辆,车辆行驶按红灯停,绿灯行的规则。
1 import threading,time 2 import random 3 def light(): 4 if not event.isSet(): 5 event.set() #wait就不阻塞 #绿灯状态 6 count = 0 7 while True: 8 if count < 10: 9 print('