线程(上)
-
进程:在已经了解了操作系统中进程的概念后,我们对进程有了一定的了解:
- 程序是不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,这种执行的程序就称之为进程.
- 程序和进程的区别就在于: 程序是指令的集合, 它是进程运行的静态描述文本; 进程是程序的一次执行活动,属于动态概念. 在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行,进程是计算机中最小的资源分配单位,线程是最小的执行单位
- 这样的设计,大大他搞了CPU的利用率. 进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的.
-
有了进程为何还要线程?
-
(1)什么是线程?
a.线程指的是流水线式的工作过程,一个进程内最少自带一个线程,其实进程根本不能执行,进程不是执行单位,而是资源单位,分配资源的单位.线程才是执行单位.
(2)进程与线程的对比:
a.同一个进程内的多个线程是共享该进程的资源的,不同进程内的线程资源是隔离的.
b.创建线程对资源的消耗远远小于创建进程的消耗
(3)进程有很多优点,它提供了多道编程,提高了计算机的利用率,让每个人感觉自己独享着CPU和其他资源.然而,进程也是有缺点的,主要体现在两点上:
a.进程只能在同一时间执行一个任务,如果想要同时执行两个或多个任务,进程就无能为力了.
b.进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行.
-
-
线程的出现
-
60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建,撤销与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程运行开销过大.
-
因此,在80年代,出现了能独立运行的基本单位--线程(Threads).
-
需要注意的是: 进程是资源分配的最小单位,线程是CPU调度的最小单位.每个进程中至少有一个线程.
-
在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程.我们可以把一个车间的工作过程看作是一个进程,把一条流水线工作的过程看作是一个线程.车间不仅要负责把资源整合到一起,而且一个车间内至少要有一条流水线.
所以总结来说: 进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是CPU上的执行单位.
多线程的概念是: 在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间.
-
-
线程和进程的关系
-
线程与进程的区别可归纳为以下4点:
-
地址空间和其他资源(如打开文件): 进程间相互独立,同一进程的各线程间共享. 某进程内的线程在其他进程不可见.
-
通信: 进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信----需要进程同步和互斥手段的辅助,以保证数据的一致性(类似于进程中的锁的作用).
-
调度和切换: 线程上下文切换比进程上下文切换要快得多.
-
在多线程操作系统中,进程不是一个可执行的实体,真正去执行程序的不是进程,而是线程.可以理解为进程就是一个线程的容器.
x并发 :同一时刻能同时接收多个客户端的请求
线程
轻型进程 轻量级的进程
在同一个进程中的多个线程是可以共享一部分数据的
线程的开启\销毁\切换都比进程要高效很多
1.多个进程可不可以利用多核(多个CPU) 可以
2.多个线程可不可以利用多核(多个CPU) 可以
多进程和多线程之间的区别
进程 数据隔离 开销大
线程 数据共享 开销小
python当中的多线程
不能访问多个cpu
是Cpython解释器导致的,GIL锁
GIL锁 = 全局解释器锁,导致了同一时刻只能有一个线程访问CPU
jpython pypy解释器中的多线程是可以访问多核的
-
-
一、线程的特点
-
在多线程的操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体. 线程具有以下属性:
-
1. 轻型实体:线程中的实体基本上不拥有系统资源,只是有一些必不可少的,能保证独立运行的资源,线程的实体包括程序,数据和TCB.线程是动态概念,它的动态特性有线程控制块TCB(Thread Control Block)描述。
xxxxxxxxxx
#TCB包括以下信息:
(1)线程状态.
(2)当线程不运行时,被保存的现场资源.
(3)一组执行堆栈.
(4)存放每个线程的局部变量主存区.
(5)访问同一个进程中的主存和其它资源.
用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈.
-
2. 独立调度和分派的基本单位:在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位.由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的线程).
-
3. 共享进程资源:线程在同一进程中的各个线程, 都可以共享该进程所拥有的资源, 这首先表现在: 所有线程都具有相同的进程id, 这意味着, 线程可以访问该进程的每一个内存资源; 此外, 还可以访问进程所拥有的已打开文件、定时器、信号量机构等. 由于同一个进程内的线程共享内存和文件, 所以线程之间互相通信不必调用内核
-
可并发执行:在一个进程中的多个线程之间, 可以并发执行, 甚至允许在一个进程中所有线程都能并发执行; 同样, 不同进程中的线程也能并发执行, 充分利用和发挥了处理机与外围设备并行工作的能力.
-
二、全局解释器锁GIL
-
Python代码的执行由Python虚拟机(也叫解释器主循环)来控制. Pyhton在设计之初就考虑到: 在主循环中同时只有一个线程在执行. 虽然Python解释器中可以"运行"多个线程,但在任意时刻只有一个线程在解释器中运行. 对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行.在多线程环境中,Python虚拟机按以下方式执行:
a. 设置GIL
b. 切换到一个线程去执行
c. 运行指定数量的字节码指令或者线程主动让出控制(可以调用time.sleep(0))
d. 把线程设置为睡眠状态
e. 解锁GIL
f. 再次重复以上所有步骤
在调用外部代码(如C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁GIL.
-
Python线程模块的选择
-
Pyhton提供了几个用于多线程编程的模块,包括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模块的接口,二者在使用层面有很大的相似性.
-
线程创建的两种方式
xxxxxxxxxx
#方式一
import time
from threading import Thread # 引入线程模块
def func(n): # 自定义一个函数
time.sleep(1)
print(n)
if __name__ == '__main__':
t = Thread(target=func, args=("hello,world",)) # 创建线程对象
t.start() # 开启线程
print("主线程结束")
#方式二
import time
from threading import Thread # 引入线程模块
class MyThread(Thread): # 自定义一个类
def __init__(self, n): # 传参n
super().__init__() # 自己想要传参,必须先super()执行父类的init方法,再写自己的实例变量
self.n = n
def run(self): # 自定义一个run()方法,内容不定,但是run名称不能变
time.sleep(1)
print(self.n)
if __name__ == '__main__':
t = MyThread("hello,world") # 创建线程对象
t.start() # 开启线程
print("主线程结束")
Thread实例对象的方法
# isAlive(): 返回线程是否活动的。
# getName(): 返回线程名。
# setName(): 设置线程名。
threading模块提供的一些方法:
# threading.currentThread(): 返回当前的线程变量。
# threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
# threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
#判断线程的存活
from threading import Thread
def func():
print(123)
t = Thread(target=func)
t.start()
print(t.is_alive()) # 返回线程是否是活动的
#结果
#123
#False
import time
from threading import Thread
def func():
time.sleep(0.1)
print(123)
t = Thread(target=func)
t.start()
print(t.is_alive())
#结果
#True
#123
#设置修改线程名
import time
from threading import Thread
def func():
time.sleep(0.1)
print(123)
t = Thread(target=func)
t.start()
print(t.is_alive()) # 返回线程是否活动的
print(t.getName()) #返回线程名
t.setName('t1') # 设置线程名
print(t.getName())
#执行输出:
#True
#Thread-1
#t1
#123
#输出现有存活线程
import time
from threading import Thread,currentThread,enumerate,activeCount
def func():
time.sleep(0.1)
#print(123)
t = Thread(target=func)
t.start()
print(currentThread) # 返回当前的线程变量
print(enumerate()) #返回一个包含正在运行的线程的list
print(activeCount()) # 返回正在运行的线程数量
#结果
<function current_thread at 0x000001C35EF6A268>
[<_MainThread(MainThread, started 20008)>, <Thread(Thread-1, started 20232)>]
2
MainThread, started 20008表示主线程,Thread-1, started 20232表示子线程。它会打印出2个。
所以activeCount的结果为2
-
进程与线程开启效率比较
xxxxxxxxxx
import time
from threading import Thread
from multiprocessing import Process
def func(n):
sum = 0
for i in range(n):
sum += i
if __name__ == '__main__':
t_start_time = time.time() #开始时间
t_list = []
for i in range(10):
t = Thread(target=func, args=(100,))
t.start()
t_list.append(t)
[tt.join() for tt in t_list]
t_end_time = time.time() #结束时间
t_dif_time = t_end_time - t_start_time #时间差
p_start_time = time.time() #开始时间
p_list = []
for ii in range(10):
p = Process(target=func, args=(100,))
p.start()
p_list.append(p)
[pp.join() for pp in p_list]
p_end_time = time.time() #结束时间
p_dif_time = p_end_time - p_start_time #时间差
print("线程的执行时间是>>>", t_dif_time)
print("进程的执行时间是>>>", p_dif_time)
print("主线程结束")
# 执行结果:
# 线程的执行时间是>>> 0.0010013580322265625
# 进程的执行时间是>>> 0.37227368354797363
# 主线程结束,
#从上面代码的结果中可以看出: 执行同一个任务,线程的执行时间远远小于进程的执行时间.因此,线程的效率是比较高的.
-
同一进程下线程是资源共享的
xxxxxxxxxx
from threading import Thread
tn = 0
def func():
global tn
tn += 1
t_l = []
for i in range(100):
t = Thread(target=func)
t.start()
t_l.append(t)
for t in t_l:t.join()
print(tn) #100
#但是进程之间的数据是隔离的
from multiprocessing import Process
pn = 0
def func():
global pn
pn += 1
if __name__ == '__main__':
p_l = []
for i in range(100):
p = Process(target=func)
p.start()
p_l.append(p)
for p in p_l:p.join()
print(pn) #0 因为所有的子进程并没有操作主进程的pn
-
线程共享数据时,数据是不安全的
xxxxxxxxxx
#数据共享时数据不安全
import time
from threading import Thread
num = 100 #全局变量
def func():
global num
# 模拟num-=1的"取值->计算->赋值"过程
mid = num #取值
mid = mid - 1 #计算
time.sleep(0.0001)
num = mid #赋值
if __name__ == '__main__':
t_list = []
for i in range(10): #创建10个子线程
t = Thread(target=func,)
t.start()
t_list.append(t)
[tt.join() for tt in t_list] # 主线程等待子线程执行结束
print('主线程结束,此时全局变量为>>>', num)
# 执行结果:
# 主线程结束,此时全局变量为>>> 99
#通过引入线程模块里的锁来解决不安全问题
-
-
线程中的其他方法
xxxxxxxxxx
#currentTread 当前线程的属性,不要忘了,主进程也有个主线程
import os
from threading import Thread,currentThread
def func():
t = currentThread()
print(t.name,t.ident,os.getpid()) #Thread-1 2940 10716
tobj = Thread(target=func)
tobj.start()
print('tobj :',tobj) #tobj : <Thread(Thread-1, started 2940)>
t2 = currentThread()
print(t2.name,t2.ident,os.getpid()) #MainThread 12360 10716
#例,# lst = [1,2,3,4,5,6,7,8,9,10]按照顺序把列表中的每一个元素都计算一个平方,使用多线程的方式,并且将结果按照顺序返回
import time
import random
from threading import Thread,currentThread
dic = {}
def func(i):
t = currentThread()
time.sleep(random.random())
dic[t.ident] = i**2
t_lst = []
for i in range(1,11):
t = Thread(target=func,args=(i,))
t.start()
t_lst.append(t) #按照顺序加进去
for t in t_lst:
t.join()
print(dic[t.ident]) #根据进程号来取
#面试题
from threading import active_count # 返回当前有多少个正在工作的线程
import time
import random
from threading import Thread,currentThread
dic = {}
def func(i):
t = currentThread()
time.sleep(random.random())
dic[t.ident] = i**2
for i in range(10):
Thread(target=func,args=(i,)).start()
print(active_count()) # ??? 结果1,因为主线程也算一个
#线程有terminate么?
#没有terminate 不能强制结束
#所有的子线程都会在执行完所有的任务之后自动结束
-
线程(threading)的一些其他方法总结
xxxxxxxxxx
from threading import Thread
import threading
import time
def work():
time.sleep(1)
print("子线程对象>>>", threading.current_thread()) # 子线程对象
print("子线程名称>>>", threading.current_thread().getName()) # 子线程名称
print("子线程ID>>>", threading.get_ident()) # 子线程ID
if __name__ == '__main__':
t = Thread(target=work) # 创建子线程
t.start() # 开启子线程
print("主线程对象>>>", threading.current_thread()) # 主线程对象
print("主线程名称>>>", threading.current_thread().getName()) # 主线程名称
print("主线程ID>>>", threading.current_thread().ident) # 主线程ID
print("主线程ID>>>", threading.get_ident()) # 主线程ID
time.sleep(1) # 阻塞住,此时主线程代码运行的同时子线程代码也在运行
print(threading.enumerate()) # 拿到所有正在运行的线程对象(包括主线程)
print(threading.active_count()) # 拿到所有正在运行的线程对象的数量
print("主线程/主进程执行完毕")
-
多线程的其他用法
- threading.local
threading.local()这个方法的特点用来保存一个全局变量,但是这个全局变量只有在当前线程才能访问,如果你在开发多线程应用的时候 需要每个线程保存一个单独的数据供当前线程操作,可以考虑使用这个方法,简单有效。举例:每个子线程使用全局对象a,但每个线程定义的属性a.xx是该线程独有的,Python提供了 threading.local 类,将这个类实例化得到一个全局对象,但是不同的线程使用这个对象存储的数据其它线程不可见(本质上就是不同的线程使用这个对象时为其创建一个独立的字典)。 #代码示例 import time import threading v = threading.local() def func(arg): # 内部会为当前线程创建一个空间用于存储:phone=自己的值 v.phone = arg**2 time.sleep(2) print(v.phone,arg) # 去当前线程自己空间取值 for i in range(10): t =threading.Thread(target=func,args=(i,)) t.start() #结果 4 2 0 0 1 1 49 7 9 3 …… #作用:内部自动为每个线程维护一个空间(字典),用于当前存取属于自己的值。保证线程之间的数据隔离。 { 线程ID: {...} 线程ID: {...} 线程ID: {...} 线程ID: {...} } #拓展 import time import threading INFO = {} class Local(object): def __getattr__(self, item): ident = threading.get_ident() return INFO[ident][item] def __setattr__(self, key, value): ident = threading.get_ident() if ident in INFO: INFO[ident][key] = value else: INFO[ident] = {key: value} # 实例化 obj = Local() def func(arg): # 调用对象的 __setattr__方法(“phone”,1) obj.phone = arg time.sleep(2) print(obj.phone, arg) if __name__ == '__main__': for i in range(10): t = threading.Thread(target=func, args=(i,)) t.start() # 效果类似
和进程类似。
四、信号量
-
同进程的一样, Semaphore管理一个内置的计数器:
每当调用acquire()时内置计数器-1;
调用release()时内置计数器+1;
计数器不能小于0;当计数器为0时, acquire()将阻塞线程直到其他线程调用release().
互斥锁同时只允许一个线程更改数据,而信号量Semaphore是同时允许一定数量的线程更改数据 。 假设商场里有4个迷你唱吧,所以同时可以进去4个人,如果来了第五个人就要在外面等待,等到有人出来才能再进去玩。 实现: 信号量同步基于内部计数器,每调用一次acquire(),计数器减1;每调用一次release(),计数器加1.当计数器为0时,acquire()调用被阻塞。这是迪科斯彻(Dijkstra)信号量概念P()和V()的Python实现。信号量同步机制适用于访问像服务器这样的有限资源。信号量与进程池的概念很像,但是要区分开,信号量涉及到加锁的概念 from threading import Thread,Semaphore import threading import time def func(): if sm.acquire(): # 加锁 print(threading.currentThread().getName() + " get semaphore") time.sleep(3) sm.release() # 解锁 if __name__ == '__main__': sm = Semaphore(5) # 创建信号量对象,限制锁内每次只能进入5个线程 for i in range(25): # 创建25个线程 t = Thread(target=func,) t.start() #与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程,semaphore 允许同一时刻n个线程执行这段代码
五、守护线程
-
无论是进程还是线程,都遵循: 守护进程(线程)会等待主进程(线程)运行完毕后被销毁. 需要强调的是: 运行完毕并非终止运行
#1. 对于主进程来说,运行完毕指的是主进程代码运行完毕,很好理解守护进程其实是依赖于主进程的,那么子进程也可以理解为一堆线程的集合体,那么主进程的代码结束,必须同时对子进程的资源进行回收,所以守护进程必须在主进程代码结束时结束 #2. 对于主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程全部执行完毕,主线程才算运行完毕.守护线程完全可以最后一个消失 解析: 1. 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束. 2. 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收). 因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束,因为进程执行结束是要回收资源的.
-
实力证明
import time from threading import Thread def func1(n): time.sleep(4) print(n) def func2(n): time.sleep(2) print(n) if __name__ == '__main__': t1 = Thread(target=func1, args=("我是子线程1号",)) t1.daemon = True #设置守护线程 t1.start() t2 = Thread(target=func2, args=("我是子线程2号",)) t2.start() print("主线程结束") # 执行结果: 显然守护线程还没执行,全部线程结束,所以被动结束 # 主线程结束 # 我是子线程2号 import time from threading import Thread def func1(n): time.sleep(1) print(n) def func2(n): time.sleep(2) print(n) if __name__ == '__main__': t1 = Thread(target=func1, args=("我是子线程1号",)) t1.deamon = True # 设置守护线程,必须放到start前面 t1.start() t2 = Thread(target=func2, args=("我是子线程2号",)) t2.start() print("主线程结束") # 执行结果: # 主线程结束 # 我是子线程1号 # 我是子线程2号 #对比两个例子的执行结果,可以看出,主线程等待所有非守护线程的结束才结束.当主线程的代码运行结束后,还要等待非守护线程执行完毕,在这个等待的过程中,守护线程并没有消亡,还在继续执行.
-
对比守护进程和守护线程:
- 守护进程: 主进程的代码执行完毕后,整个程序并没有结束,且主进程仍然存在着,因为主进程要等待其它子进程执行完毕,回收子进程的残余资源(为子进程收尸).总之,主进程的代码执行完毕后守护进程也跟着结束----守护进程随着主进程的消亡而消亡.
- 守护线程: 主线程的代码执行完毕后,整个程序并没有结束,且主线程仍然存在着,因为主线程要等待所有非守护线程执行完毕,随后,当所有线程全部执行完毕后,主线程结束,这也意味着主进程的结束,最后主进程回收所有资源.总之,主线程的代码执行完毕后要等待非守护线程执行完毕,在这个等待过程中,守护进程没有消亡,直到等待结束,随主线程的消亡而消亡.
四、互斥锁(同步锁)
-
互斥锁:在同一个线程中,不能连续acquire多次,并且可以做到多个线程中被锁的代码同时只有一个线程执行
#1.线程抢的是GIL锁,GIL锁相当于执行权限,拿到执行权限后才能拿到互斥锁Lock,其他线程也可以抢到GIL,但如果发现Lock仍然没有被释放则阻塞,即便是拿到执行权限GIL也要立刻交出来 #2.join是等待所有,即整体串行,而锁只是锁住修改共享数据的部分,即部分串行,要想保证数据安全的根本原理在于让并发变成串行,join与互斥锁都可以实现,毫无疑问,互斥锁的部分串行效率要更高 import time from threading import Thread def func(): global n temp = n time.sleep(1) n = temp -1 n = 100 t_lst = [] for i in range(100): t = Thread(target=func) t.start() t_lst.append(t) for t in t_lst:t.join() print(n) #99,因为多个线程抢夺同一个资源,没有秩序 #其实只要去掉赋值这个过程,直接每个进程在原有的数据上进行修改就不会出现问题,(不用枷锁) from threading import Thread def func(): global n n -= 1 n = 100 for i in range(100): t = Thread(target=func) t.start() print(n)
原因很简单,因为异步执行所有线程,几乎同时对全局的n进行操作,换句话说100可线程都取到n=100,得到的结果也都是99,所以造成了数据不安全。
-
死锁现象**: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁。只要实例化多把锁,并交替使用,都有可能产生死锁现象
- 只要是1把锁,递归锁永远不死锁
- 只要是2以及以上,交替使用(递归锁也会)
-
from threading import Lock lock = Lock() # 在同一个线程中,能够被一个锁的多个acquire阻住,这种锁就叫做互斥锁 lock.acquire() # 锁被拿走 lock.acquire() lock.acquire() #科学家吃面问题:要完成一件事情 需要两个必要因素,要想吃到面,需要: 叉子,面。资源的互相抢占的问题 —— 死锁 import time from threading import Thread,Lock noodle_lock=Lock() fork_lock=Lock() def eat1(noodle_lock,fork_lock,name): # 吃面 noodle_lock.acquire() # 面条加锁 print('%s 抢到了面条'%name) fork_lock.acquire() # 叉子加锁 print('%s 抢到了叉子'%name) print('%s 正在吃面'%name) fork_lock.release() # 叉子解锁 print('%s归还了叉子'%name) noodle_lock.release() # 面条解锁 print('%s归还了面条' % name) def eat2(noodle_lock,fork_lock,name): #也是吃面,不同的是:先抢叉子,再抢面 fork_lock.acquire() print('%s抢到了叉子' % name) time.sleep(0.5) noodle_lock.acquire() print('%s抢到了面'%name) print('%s正在吃面'%name) noodle_lock.release() print('%s归还了面' % name) fork_lock.release() print('%s归还了叉子' % name) noodle_lock = Lock() # 面条锁 fork_lock = Lock() # 叉子锁 t1 = Thread(target=eat1,args=(noodle_lock,fork_lock,'nazha')).start() # 最开始他有很大几率得到两把钥匙 t2 = Thread(target=eat2,args=(noodle_lock,fork_lock,'egon')).start() #他要叉,一有就拿走 t3 = Thread(target=eat1,args=(noodle_lock,fork_lock,'yuan')).start() #他要面,一有就拿走,两两卡死 t4 = Thread(target=eat2,args=(noodle_lock,fork_lock,'alex')).start()
五、递归锁(互斥锁的解决办法)
-
在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
-
从一定程度上可以避免死锁现象,使用递归锁也会产生死锁现象,也就是说在交叉使用锁时也会出现死锁
#简单例子 from threading import Thread,RLock rlock = RLock() # 创建递归锁 rlock.acquire() # 加第一个锁 print('***') rlock.acquire() # 加第二个锁 print('***') #结果: *** *** #结论:递归锁在同一个线程中对同一个锁多次acquire不会产生阻塞 from threading import Thread,RLock def func(rlock,flag): rlock.acquire() # 第一道锁 print(flag*10) rlock.acquire() # 第二道锁 print(flag * 10) rlock.acquire() # 第三道锁 print(flag * 10) rlock.acquire() # 第四道锁 print(flag * 10) rlock = RLock() # 创建递归锁 Thread(target=func,args=(rlock,'*')).start() # 传入递归锁和* Thread(target=func,args=(rlock,'-')).start() #结果 ********** ********** ********** ********** # 这里没有输出---------,因为线程进去了没有出来 #解决办法 from threading import Thread,RLock def func(rlock,flag): rlock.acquire() # 第一道锁 print(flag*10) rlock.acquire() # 第二道锁 print(flag * 10) rlock.acquire() # 第三道锁 print(flag * 10) rlock.acquire() # 第四道锁 print(flag * 10) rlock.release() # 解锁 rlock.release() rlock.release() rlock.release() rlock = RLock() # 创建递归锁 Thread(target=func,args=(rlock,'*')).start() Thread(target=func,args=(rlock,'-')).start() #使用递归锁解决科学家吃面问题 import time from threading import Thread,RLock def eat1(noodle_lock,fork_lock,name): # 吃面 noodle_lock.acquire() # 拿到面条的整串钥匙 print('%s 抢到了面条'%name) fork_lock.acquire() # 拿到叉子的整串钥匙 print('%s 抢到了叉子'%name) print('%s 正在吃面'%name) fork_lock.release() # 叉子解锁 print('%s归还了叉子'%name) noodle_lock.release() # 面条解锁 print('%s归还了面条' % name) def eat2(noodle_lock,fork_lock,name): #也是吃面,不同的是:先抢叉子,再抢面 fork_lock.acquire() print('%s抢到了叉子' % name) time.sleep(0.5) noodle_lock.acquire() print('%s抢到了面'%name) print('%s正在吃面'%name) noodle_lock.release() print('%s归还了面' % name) fork_lock.release() print('%s归还了叉子' % name) noodle_lock = fork_lock = RLock() # 面条锁和叉子锁,表示一串钥匙 t1 = Thread(target=eat1,args=(noodle_lock,fork_lock,'nazha')).start() t2 = Thread(target=eat2,args=(noodle_lock,fork_lock,'egon')).start() t3 = Thread(target=eat1,args=(noodle_lock,fork_lock,'yuan')).start() t4 = Thread(target=eat2,args=(noodle_lock,fork_lock,'alex')).start()
六、GIL锁(Global Interpreter Lock)
-
在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势,但是其他python解释器能实现
-
首先需要明确的一点是
GIL
并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL. -
如果多个线程的target=work,那么执行流程是:
多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行
解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了一个问题:对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,解决这种问题没有什么高明的方法,就是加锁处理,如下图的GIL,保证python解释器同一时间只能执行一个任务的代码
-
GIL vs Lock
-
GIL保护的是解释器级的数据,保护用户自己的数据则需要自己加锁处理,如下图
-
既然Python已经有了一个GIL来保证同一时间只能有一个线程Laura执行,那么为什么还需要Lock呢? 首先我们需要达成共识: 锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据. 然后,我们可以得出结论: 保护不同的数据就应该加不同的锁. 于是,问题就比较明朗了,GIL与Lock是两把锁,保护的数据不一样:前者是解释器级别的(保护的是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock.
-
-
GIL与多线程
-
有了GIL的存在,同一时刻同一进程中只有一个线程被执行
听到这里,有的同学立马质问:进程可以利用多核,但是开销大,而python的多线程开销小,但却无法利用多核优势,也就是说python没用了,php才是最牛逼的语言,别着急啊,老娘还没讲完呢,要解决这个问题,我们需要在几个点上达成一致:
-
七、线程队列
-
Queue就是一个线程队列的类,自带lock锁,实现了线程安全的数据类型,队列是一个线程安全的数据类型
-
注意:
- 在多线程下都不准,因为多线程中随时可能会往里面添加值,或则删除值 q.empty() 判断是否为空 q.full() 判断是否为满 q.qsize() 队列的大小
-
queue.Queue(maxsize=0) -- 先进先出
q=queue.Queue() q.put('first') q.put('second') q.put('third') # q.put_nowait() #没有数据就报错,可以通过try来搞 print(q.get()) print(q.get()) print(q.get()) # q.get_nowait() #没有数据就报错,可以通过try来搞 # 执行结果: (先进先出) # first # second # third
-
queue.LifoQueue(maxsize=0) -- last in first out 后进先出
import queue q=queue.LifoQueue() #队列,类似于栈,后进先出的顺序 q.put('first') q.put('second') q.put('third') # q.put_nowait() print(q.get()) print(q.get()) print(q.get()) # q.get_nowait() # 执行结果:(后进先出) # third # second # first
-
queue.PriorityQueue(maxsize=0) -- 存储数据时可以设置优先级队列
from queue import PriorityQueue q = queue.PriorityQueue() # 创建栈 # put()方法放置一个元组进入栈,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高 q.put((-10, "a")) q.put((-5, "a")) # 负数也可以 # q.put((20,"ws")) # 如果两个值的优先级一样,那么按照后面的值的acsii码顺序来排序,如果字符串第一个数元素相同,比较第二个元素的acsii码顺序 # q.put((20,"wd")) # q.put((20,{"a": 11})) # TypeError: unorderable types: dict() < dict() 不能是字典 # q.put((20,("w", 1))) # 优先级相同的两个数据,他们后面的值必须是相同的数据类型才能比较,可以是元祖,也是通过元素的ascii码顺序来排序 q.put((20, "b")) q.put((20, "a")) q.put((0, "b")) q.put((30, "c")) print(q.get()) print(q.get()) print(q.get()) print(q.get()) print(q.get()) print(q.get()) # 数字越小优先级越高,优先级高的优先出队
-
八、线程池、进程池(python标准模块--->concurrent.futures)
-
早期的时候并没有线程池,现在Python提供了一个新的标准或者说内置的模块,这个模块里面提供了新的线程池和进程池.
-
concurrent.futures的用法
#1 介绍 concurrent.futures 模块提供了高度封装的异步调用接口 ThreadPoolExecutor:线程池,提供异步调用 ProcessPoolExecutor: 进程池,提供异步调用 Both implement the same interface, which is defined by the abstract Executor class. #2 基本方法 #submit(fn, *args, **kwargs) 异步提交任务 #map(func, *iterables, timeout=None, chunksize=1) 取代for循环submit的操作 #shutdown(wait=True) 相当于进程池的pool.close()+pool.join()操作 wait=True,等待池内所有任务执行完毕回收完资源后才继续 wait=False,立即返回,并不会等待池内的任务执行完毕 但不管wait参数为何值,整个程序都会等到所有任务执行完毕 submit和map必须在shutdown之前 #result(timeout=None) 取得结果,这是个阻塞的,一直会等相应的返回值 #add_done_callback(fn) 回调函数 # ProcessPoolExecutor的用法 from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor import os,time,random def task(n): print('%s is runing' %os.getpid()) time.sleep(random.randint(1,3)) return n**2 if __name__ == '__main__': executor=ProcessPoolExecutor(max_workers=3) #这是限制了使用进程,可以改为ThreadPoolEcecutor,效果一样 futures=[] for i in range(11): future=executor.submit(task,i) #多线程执行函数,第二个参数必须是一个参数,可以封装成元组 futures.append(future) executor.shutdown(True) print('+++>') for future in futures: #最后输出结果 print(future.result()) #map的用法 from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor import os,time,random def task(n): print('%s is runing' %os.getpid()) time.sleep(random.randint(1,3)) return n**2 if __name__ == '__main__': executor=ThreadPoolExecutor(max_workers=3) # for i in range(11): # future=executor.submit(task,i) ret_l=executor.map(task,range(1,12)) #map取代了for+submit,效果一致 print('+++>') for future in ret_l: #最后输出结果 print(future.result()) #池的误区 import time from concurrent.futures import ThreadPoolExecutor def func(i): print(i*'*') time.sleep(1) thread_pool = ThreadPoolExecutor(5) #创建一个最大可容纳2个任务的线程池 for i in range(6): thread_pool.submit(func,i) # 异步提交任务,往线程池里面加入一个任务 print('wahaha') #结果: * ** *** **** wahaha ***** #回调函数 import time from concurrent.futures import ThreadPoolExecutor def func(i): print(i*'*') time.sleep(1) return i**2 def callback(arg): #回调函数 print(arg.result()*'-') # 取得结果,并乘以- thread_pool = ThreadPoolExecutor(5) #创建一个最大可容纳2个任务的线程池 for i in range(6): ret = thread_pool.submit(func,i).add_done_callback(callback) # 异步提交任务,执行回调函数 thread_pool.shutdown() # 相当于进程池的pool.close()+pool.join()操作 print('wahaha') #执行输出 * ** *** **** ---------------- ***** ---- - --------- ------------------------- wahaha
-
九、线程事件(Event)
-
同进程的一样,线程的一个关键特性是
每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象
。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行. -
event的用法
event.isSet():#返回event的状态值; event.wait(): #如果 event.isSet()==False将阻塞线程;可设置时间 event.set(): #设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度; event.clear():#恢复event的状态值为False。
-
例如,有多个工作线程尝试链接MySQL,我们想要在链接前确保MySQL服务正常才让那些工作线程去连接MySQL服务器,如果连接不成功,都会去尝试重新连接。那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作。
import time import random from threading import Event,Thread # 连接数据库 def connect_db(e): count = 1 # 初始计数器 while count < 4: print('尝试第%s次检测连接'%count) e.wait(0.5) # 等待0.5,再去执行下面的代码,如果不写参数则会也一直在这等待 # 如果不传参数会一直等到事件为True为止 # 如果传参数 传一个时间参数,到时间后, count += 1 # 加1 if e.is_set(): # 判断状态是否为True print('连接成功') break else: print('连接失败') def check_conn(e): '''检测数据库是否可以连接''' time.sleep(random.randint(1,2)) # 等待1~2秒,显然这里时间太长 e.set() # 设置状态为True e = Event() Thread(target=check_conn,args=(e,)).start() Thread(target=connect_db,args=(e,)).start() #随机数,至少连接两次,如果等待2秒,e.wait(0.5)执行4次,才能达到2秒。但是whlie循环,不可能达到4。所以3次全部失败。第1种情况,等待1秒,e.wait(0.5)必然执行2次,才能达到1秒,状态变成True,输出连接成功。第2种情况,是偶然事件。因为CPU同一时刻只能执行一个线程。所以失败了3次,才输出连接成功。
-
你要做一件事情 是有前提的 你就先去处理前提的问题 —— 前提处理好了 把状态设置成True,来控制即将要做的事情可以开始
十、条件
-
使得线程等待,只有满足某条件时,才释放n个线程,Python提供的Condition对象提供了对复杂线程同步问题的支持。Condition被称为条件变量,除了提供与Lock类似的acquire和release方法外,还提供了wait和notify方法。线程首先acquire一个条件变量,然后判断一些条件。如果条件不满足则wait;如果条件满足,进行一些处理改变条件后,通过notify方法通知其他线程,其他处于wait状态的线程接到通知后会重新判断条件。不断的重复这一过程,从而解决复杂的同步问题。
from threading import Condition,Thread # acquire release # notify -- wait的行为 # 10线程 wait # notify(1) def func(i,con): con.acquire() # 进入锁定池 con.wait() # 等待通知,前后必须要有acquire和release print(i*'*') con.release() # 解锁 con = Condition() #条件变量 for i in range(10): Thread(target=func,args=(i,con)).start() while True: n = int(input('>>>')) #n 表示激活几个线程 con.acquire() # 加锁 con.notify(n) # 通知其他线程,其他处于wait状态的线程接到通知后会重新判断条件,解除wait状态。前后必须要有acquire和release con.release() # 解锁 #从结果上来看,发生数据混乱 #con.notify(n) 表示按照要求放开线程 #con.notify_all() 表示一次性放开线程 总结: semaphore 允许同一时刻n个线程执行这段代码 event 有一个内部的事件来控制wait的行为,控制的是所有的线程 condition 有一个内部的条件来控制wait的行为,它可以逐个或者分批次的控制线程的走向
十一、定时器
-
定时器,指定n秒后执行某个操作,你可能说为什么不用sleep,因为那是同步阻塞的,虽然可以用time.sleep(5)完成5秒的过程,但是它会阻塞主线程,如果用timer,就不会,它是异步的。
import time from threading import Timer def func(): print('*'*10) print('子线程',time.strftime("%Y-%m-%d %H:%M:%S")) t =Timer(5,func) # 要开始一个线程,等到5秒之后才开启并执行 t.start() print('主进程',time.strftime("%Y-%m-%d %H:%M:%S")) #执行输出: #主进程 2018-05-17 19:41:08 #********** #子线程 2018-05-17 19:41:13