Python - - 并发编程 - - 线程
目录
- 线程的概念
- 线程的由来
- 进程和线程的区别
- 线程的特点
- 内存中的线程
- 用户级线程和内核级线程
- 线程和python
- 理论知识
- 线程的创建Threading.Thread类
- 锁
- 信号量
- 事件
- 条件
- 定时器
- 队列
- Python标准模块--concurrent.futures
1,线程概念
1.1 线程的由来
- 进程是CPU分配资源的最小单位
- 线程是CPU调度的最小单位
- 每个进程中至少有一个线程;
- 所以进程是包含线程的,同一进程的不同线程之间数据是共享的
- 开启进程的时间要比开启线程的时间长,CPU在进程之间的切换比在线程之间的切换要慢的多
- 如果有两个任务需要共享内存,有需要实现异步,就需要开启多线程
- 如果有两个任务需要隔离,就需要开启多线程
1.2 进程和线程的区别
- 1 地址空间和其它资源(如打开文件):
- 进程间相互独立,同一进程的多线程之间共享数据资源,某进程内的线程在其他进程不可见
- 2 通信:
- 进程间通信IPC(比如队列,管道),线程之间可以直接读写线程数据段(如全局变量)来进行通信
- 3 调用和切换:
- 线程上下文切换,比进程上下文切换要快得多
- 4 在多线程操作系统中,进程不是一个可执行的实体
- 因为线程是CPU调度任务的最小单位,进程只是CPU资源分配的最小单位
1.3 线程的特点
- 在多线程的操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。
- 1 轻型实体
- 线程中的实体基本上不拥有系统资源,只是有一点必不可少、‘’能保证独立运行的资源
- 线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Conrol Block)描述
- TCB包括以下信息:
- 1 线程状态
- 2 当线程不运行时,被保存的现场资源
- 3 一组执行堆栈
- 4 存放每个线程的局部变量主存区
- 5 访问同一个进程中的主存和其它资源
- 用户指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈
- TCB包括以下信息:
- 2 独立调用和分配的基本单位
- 在多线程OS中,线程是能独立运行的基本单位,因而也是独立调用和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)
- 3 共享进程资源
- 线程在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的进程id,这意味着,线程可以访问该进程的每一个内存资源;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程的线程共享内存和文件,所以线程之间相互通信不必调用内核。
- 4 可并发执行
- 在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机制与外围设备并行工作的能力
1.4 内存中的线程
- 多线程共享同一个进程的地址空间中的资源,是对一台计算机上多个进程的模拟,有时也称线程为轻量级的进程。
- 而对一台计算机上多个进程,则共享物理内存、磁盘、CPU等其他物理资源。多线程的运行也和多进程的运行类似,是CPU在多个线程之间的快速切换。
- 不同的是进程之间充满了敌意,彼此抢占、竞争CPU和内存的关系
- 类似于进程,每个线程也有自己的堆栈,不同于进程,线程库无法利用时钟中断强制线程让出CPU,可以调用 thread_yield 运行线程自动放弃CPU,让另一个线程运行。
- 线程通常是有益的,但是带来了不少程序设计难度,线程的问题是:
- 1 父进程有多个线程,那么开启的子线程是否需要同样多的线程
- 2 在同一个进程中,如果一个线程关闭了文件,而另一个线程正准备往该文件内写内容呢?
- 因此,在多线程的代码中,需要更多的心思设计程序的逻辑、保护程序的数据
1.5 用户级线程和内核级线程
- 线程的实现可以分为两类:用户级线程(User-Level Thread)和内核线线程(Kernel-Level Thread),后者又称为内核支持的线程或轻量级进程。在多线程操作系统中,各个系统的实现方式并不相同,在有的系统中实现了用户级线程,有的系统中实现了内核级线程。
1.5.1 用户级线程
- 内核的切换由用户态程序自己控制内核切换,不需要内核干涉,少了进出内核态的消耗,但不能很好的利用多核Cpu。
- 在用户空间模拟操作系统对进程的调度,来调用一个进程中的线程,每个进程中都会有一个运行时系统,用来调度线程。此时当该进程获取cpu时,进程内再调度出一个线程去执行,同一时刻只有一个线程执行.
1.5.2 内核级线程
- 内核级线程:切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态;可以很好的利用smp,即利用多核cpu。windows线程就是这样的。
1.5.3 用户级与内核级线程的对比
- 用户级线程和内核级线程的区别
- 1 内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。
- 2 用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。
- 3 用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。
- 4 在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。
- 5 用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是可以运行在任何状态下的程序。
- 内核线程的优缺点
- 优点:当有多个处理机时,一个进程的多个线程可以同时执行。
- 缺点:由内核进行调度
- 用户级线程的优缺点
- 优点:
- 线程的调度不需要内核直接参与,控制简单。
- 可以在不支持线程的操作系统中实现。
- 创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。
- 允许每个进程定制自己的调度算法,线程管理比较灵活。
- 线程能够利用的表空间和堆栈空间比内核级线程多。
- 同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。另外,页面失效也会产生同样的问题。
- 缺点:
- 资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用
- 优点:
1.5.4 混合实现
- 用户级与内核级的多路复用,内核同一调度内核线程,每个内核线程对应n个用户线程
- 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模型,通常这种模型的用户线程数会比内核的调度实体多。在这种实现里,线程库本身必须去处理可能存在的调度,这样在线程库内部的上下文切换通常都会相当的快,因为它避免了系统调用转到内核态。然而这种模型增加了线程实现的复杂性,并可能出现诸如优先级反转的问题,此外,用户态的调度如何跟内核态的调度进行协调也是很难让人满意。
2,线程和python
2.1 理论知识
2.1.1 全局解释器锁GIL
- python 代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要咋主循环中,同时只有一个线程在执行。虽然Python解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行
- 对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行
- 在多进程环境中,Python虚拟机按以下方式执行:
- 1 设置 GIL
- 2 切换到一个线程去运行
- 3 运行指定数量的字节码指令或者线程主动让出控制(可以调用time.sleep(0))
- 4 把线程设置问睡眠状态
- 5 解锁 GIL
- 再次重复以上所有步骤
- 在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁 GIL
- 其实python在多线程上并没有实现真正的多并发,python的解释器加了一把锁,使得同一时间只有一个线程被CPU执行(实际上是为了保证数据的安全);
- 如果是在高计算(不涉及IO操作,阻塞的)可以开多进程来实现;
- 如果是在高IO(input print 文件的读写,网络通信accept recv等)可以开多线程(比如在网络爬虫,其实IO操作等待的时间是很长的,即使真的有多线程并发,也是需要等的,所以这种情况下多线程并发执行并不会带来效率的提升)
2.1.2 Python线程模块的选择
- Python提供了几个用于多线程编程的模块,包括 thread、threading 和 Queue 等。
- Queue模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构。
- thread和threading模块允许程序员创建和管理线程,threading是thread的进阶版。
- thread模块提供了基本的线程和锁的支持,不支持守护线程,当主线程退出时,所有的子线程不论它们是否还在工作,都会被强行退出。
- threading模块支持守护线程,守护线程一般是一个等待客户请求的服务器,如果没有客户提出请求它就在那等着,如果设定一个线程为守护线程,就表示这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。
2.2 线程的创建Threading.Thread类
- multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性
2.2.1 线程的创建Threading.Thread类
- 多进线并发
import time,os
from threading import Thread
def func(n):
time.sleep(1) # 如果子线程这里不睡一秒,那么就是先打印子线程执行结果,然后才打印主线程的,因为开启线程的时间真的很短,还没执行到主线程的打印,开启子线程 就已经出结果了
print("子线程",n,os.getpid())
# 开多线程时不用写在if __name__=="__main__":中
print("主线程,",os.getpid())
for i in range(10):
t = Thread(target=func,args=(i,))
t.start()
# 进程里:导入的模块,文件所在的位置,内置的函数代码
# 线程里:执行代码的过程以及结果
import time,os
from threading import Thread
def func(a,b):
time.sleep(1)
n = a + b
print("子线程",n,os.getpid())
print("主线程,",os.getpid())
for i in range(10):
t = Thread(target=func,args=(i,5))
t.start()
- 内存数据的共享问题
# 全局变量在多个进程之间是共享的
import time,os
from threading import Thread
def func(a,b):
global g
g = 0
print("子线程",g,os.getpid()) # 打印结果为0
g = 100
t_lst = []
for i in range(10):
t = Thread(target=func,args=(i,5))
t.start()
t_lst.append(t)
for t in t_lst:t.join() # 等待子线程代码执行完,再执行主线程的代码
print("主线程",g) # 打印结果为0
- 面向对象(类)的方式开启多线程
import time
from threading import Thread
class MyTread(Thread): # 自定义类需继承自Thread
def __init__(self,arg):
super().__init__()
self.arg = arg
def run(self): # 必须在类内实现run()方法,线程创建之后 一旦执行t.start() 实质上就是执行类中的run()方法
time.sleep(1)
print(self.arg)
for i in range(10):
t = MyTread(i)
t.start()
2.2.2 使用多线程实现server端和多个客户端的通信
# server
import socket
from threading import Thread
def chat(conn):
conn.send(b"hello")
msg = conn.recv(1024).decode("utf-8")
print(msg)
conn.close()
sk = socket.socket()
sk.bind(("127.0.0.1",8080))
sk.listen()
while True:
conn,addr = sk.accept()
Thread(target=chat,args=(conn,)).start()
sk.close()
# client
import socket
sk = socket.socket()
sk.connect(("127.0.0.1",8080))
msg = sk.recv(1024)
print(msg)
inp = input(">>>").encode("utf-8")
sk.send(inp)
sk.close()
2.2.3 线程模块中的其它方法
- Thread实例对象的方法
- isAlive(): 返回线程是否活动的。
- getName(): 返回线程名。
- etName(): 设置线程名。
- threading模块提供的一些方法:
- hreading.currentThread(): 返回当前的线程变量。
- threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
- threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
import threading,time
def func(n):
time.sleep(0.5)
print(n,threading.current_thread(),threading.get_ident())
# 0 <Thread(Thread-1, started 39552)> 39552 # 线程名称 和 线程ID
for i in range(10):
threading.Thread(target=func,args=(i,)).start()
print(threading.active_count()) # 11 # 所有的线程数量(主线程+子线程)
print(threading.current_thread()) # <_MainThread(MainThread, started 42816)> 主线程
print(threading.enumerate()) # 所有线程对象(主线程+子线程)
print(len(threading.enumerate())) # 所有的线程数量(主线程+子线程)
2.2.4 守护线程
- 无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。需要强调的是:运行完毕并非终止运行
- 守护进程随着主进程代码的执行结束而结束
- 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束
- 守护线程会在主线程结束之后等待其它子线程的结束才结束
- 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
- 守护进程随着主进程代码的执行结束而结束
import time
from threading import Thread
def func1():
while True:
print("in func1 666")
time.sleep(1)
def func2():
print("in func2")
time.sleep(5)
t = Thread(target=func1,)
t.daemon = True
t.start()
t2 = Thread(target=func2,)
t2.start()
print("主线程")
import time
from threading import Thread
def func1():
while True:
print("in func1 666")
time.sleep(1)
def func2():
print("in func2")
time.sleep(5)
t = Thread(target=func1,)
t.daemon = True
t.start()
t2 = Thread(target=func2,)
t2.start()
t2.join()
print("主线程")
2.3 锁
2.3.1 同步锁
- 多个线程抢占资源
# 不加锁
import time
from threading import Lock,Thread
def func():
global n
temp = n
time.sleep(0.2)
n = temp - 1
n = 10
t_lst = []
for i in range(10):
t = Thread(target=func,)
t.start()
t_lst.append(t)
for t in t_lst:t.join()
print(n)
# 执行结果
9
# 加锁
import time
from threading import Lock,Thread
def func(lock):
global n
lock.acquire()
temp = n
time.sleep(0.2)
n = temp - 1
lock.release()
n = 10
t_lst = []
lock = Lock()
for i in range(10):
t = Thread(target=func,args=(lock,))
t.start()
t_lst.append(t)
for t in t_lst:t.join()
print(n)
# 执行结果
0
- 不加锁:未加锁部分并发执行,
- 加锁部分串行执行,速度慢,数据安全
2.3.2 死锁(互斥锁)与递归锁
-
进程也有死锁与递归锁
-
所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁
-
死锁
from threading import Lock as Lock
import time
mutexA=Lock()
mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()
# 结果呈现
死锁,等待中
-
解决方法,递归锁,在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()
# 结果呈现
123
- 科学家吃面
- 如果对数据进行加锁呢(就会使数据更加安全)即使时间片内数据没操作完,但是这个线程的锁还没释放,即使下一个线程可以操作数据了(GIL的锁轮到他了),但是没有拿到数据的钥匙(对数据加的锁)因为上一个线程占用了,数据没执行完,还没释放,这样就可以保证数据的安全性;
# 科学家吃面 - - 互斥锁
import time
from threading import Lock,Thread
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()
Thread(target=eat1,args=("egg",)).start()
Thread(target=eat2,args=("jin",)).start()
Thread(target=eat1,args=("ze",)).start()
Thread(target=eat2,args=("lufei",)).start()
# 结果呈现
egg抢到面条啦
egg抢到叉子拉
egg吃面
jin抢到叉子拉
ze抢到面条啦
- 如果在不同线程中对两个数据都需要加锁,一个线程拿到其中一把的钥匙,另一个线程拿到另一把,这样两个线程就都陷入阻塞状态(死锁)解决死锁的方法可以使用递归锁(上面的Lock其实叫互斥锁,acquire之后必须等到release 下一次的acquire才不会出现阻塞)但是递归锁,可以在一个线程中多次acquire 只要最后释放同等数量release 其他线程就可以有机会拿到递归锁的钥匙:
# 科学家吃面 - - 递归锁
import time
from threading import RLock,Thread
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()
Thread(target=eat1,args=("egg",)).start()
Thread(target=eat2,args=("jin",)).start()
Thread(target=eat1,args=("ze",)).start()
Thread(target=eat2,args=("lufei",)).start()
# 结果呈现
egg 抢到面条啦
egg 抢到叉子拉
egg 吃面
jin 抢到叉子拉
jin 抢到面条啦
jin 吃面
ze 抢到面条啦
ze 抢到叉子拉
ze 吃面
lufei 抢到叉子拉
lufei 抢到面条啦
lufei 吃面
2.4 信号量
- 同进程一样
- Semaphore 管理一个内置的计数器
- 每当调用 acquire() 时内置计数器 -1
- 调用release() 时内置计数器 +1
- 计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其它线程调用 release()
- 信号量本质上还是锁,只不过这个锁有很多把钥匙,同一时间只允许有限个线程进程操作(实现有限个数据的并发),但是线程开了很多个的(信号量的参数不是开的线程数。只是代表同一时间允许并发的线程数)
import time,random
from threading import Thread,Semaphore # 线程中导入信号量(其实本质还是锁,只不过一把锁可以有多把钥匙)
def func(sem,i):
sem.acquire()
print("Thread--%s--start" % i)
time.sleep(random.randint(1,3))
print("Thread--%s--done" % i)
sem.release()
start = time.time() # 统计开多个线程执行func()函数的时间
sem = Semaphore(4) # 同一时间只能有4个线程操作数据(最多实现四个并发)这里的并发其实也不是真正意义上的并发,只是时间片轮转,宏观上的并发,因为GIL
t_lst = [] # 把开的多个线程对象方法中列表中,最后统一 join() (其实就是想主线程等待子线程执行完毕)同时保证多个子线程是并发执行的
for i in range(10):
t = Thread(target=func,args=(sem,i))
t.start() # 开启线程,由于信号量参数为4,同一时间只允许四个线程同时操作(并发)
t_lst.append(t)
[t.join() for t in t_lst] # 主线程需要等待所有子线程执行完毕(主线程与子线程之间是同步)但是多个子线程之间仍然是异步并发
print("开了10个线程,但是同一时间只允许四个线程并发(使用了信号量)的时间为: %f" % (time.time()-start))
# 结果呈现
Thread--0--start
Thread--1--start
Thread--2--start
Thread--3--start
Thread--0--done
Thread--4--start
Thread--3--done
Thread--5--start
Thread--5--done
Thread--6--start
Thread--4--done
Thread--7--start
Thread--1--done
Thread--8--start
Thread--2--done
Thread--9--start
Thread--7--done
Thread--6--done
Thread--9--done
Thread--8--done
开了10个线程,但是同一时间只允许四个线程并发(使用了信号量)的时间为: 6.001524
2.4.1 信号量与线程池的异同点
- 相同:在信号量的acquire之后,同一时间只能有有限个线程(信号量的参数)同时操作数据,线程池也是同一时间只能有有限个线程操作数据
- 不同点:信号量是开了很多个线程,但是同一时间只能有(信号量的参数)个线程操作数据,而线程池是只开了(线程池中参数)个线程,这些线程轮流着执行任务,并没有开很多个线程;信号量是有几个任务就开几个线程,只不过这些线程不能同时执行而已
- 对有信号量限制的多个线程可以实现高并发(比如开了100个线程,但是信号量是4)只有被信号量acquire 和 release之间的代码才被限制为有限个线程执行,但是其余的代码其实还是可以实现高并发的(有多少个线程就有多少个线程并发执行)但是线程池就是线程执行的函数内的代码只有线程池的参数个线程并发执行:
import time,random
from threading import Thread,Semaphore # 线程中导入信号量(其实本质还是锁,只不过一把锁可以有多把钥匙)
def func(sem,i):
print("这部分的代码,有信号量的代码其实是可以实现多个线程(10)高并发的,因为这部分代码并没有加锁")
time.sleep(3)
print("但是对于线程池,就真的只开了(线程池的参数)个线程,所以这部分的代码也是只有有限个线程并发,并没有实现10个线程并发执行这段代码")
sem.acquire()
print("Thread--%s--start" % i)
time.sleep(random.randint(1,3))
print("Thread--%s--done" % i)
sem.release()
start = time.time() # 统计开多个线程执行func()函数的时间
sem = Semaphore(4) # 同一时间只能有4个线程操作数据(最多实现四个并发)这里的并发其实也不是真正意义上的并发,只是时间片轮转,宏观上的并发,因为GIL
t_lst = [] # 把开的多个线程对象方法中列表中,最后统一 join() (其实就是想主线程等待子线程执行完毕)同时保证多个子线程是并发执行的
for i in range(10):
t = Thread(target=func,args=(sem,i))
t.start() # 开启线程,由于信号量参数为4,同一时间只允许四个线程同时操作(并发)
t_lst.append(t)
[t.join() for t in t_lst] # 主线程需要等待所有子线程执行完毕(主线程与子线程之间是同步)但是多个子线程之间仍然是异步并发
print("开了10个线程,但是同一时间只允许四个线程并发(使用了信号量)的时间为: %f" % (time.time()-start))
- 其实信号量并不影响线程或者进程的并发,只是在枷锁的阶段进行流量限制;
2.5 事件
- 事件其实就是一种标志,事件默认阻塞状态
- event.isSet():返回event的状态值;
- event.wait():如果 event.isSet()==False将阻塞线程;
- event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
- event.clear():恢复event的状态值为False。
2.5.1 开线程模拟连接数据库
- 需求是: 先开一个线程检查数据库连接是否正常(比如网是否连接)就是使用time.sleep()随机睡几秒(1-3秒之间),然后设置事件的状态e.set() 然后开一个线程去执行数据库连接的操作(尝试连接,超过次数就说明连接失败)这个检查次数肯定是跟事件的状态有关的,每0.5秒检查一次事件的状态,一共检查三次,所以总共连接数据库的机会是3秒,如果数据库检查连接 随机睡的时间是1.5以内,就会在1.5之内把事件的状态设置为非阻塞,那么链接数据库需要1.5秒的这段时间就可以让跳出循环,执行连接成功:
from threading import Thread
from threading import Event
import time
import random
def conn_Mysql(): # 链接数据库
count=0
try:
while not e.is_set(): # 判断事件的状态,如果咋1.5秒以内检查数据库(check_conn0)的操作可以把e.set()设置上,就可以跳出循环,也就是连接成功
if count>=3: # check_conn 睡超过1.5秒,没来得及让事件变为非阻塞,这里循环了三次耗时1.5秒,就会抛出异常
raise TimeoutError
else:
count += 1
# time.sleep(0.5)
print("尝试连接第%s次"%count)
e.wait(0.5) # 一直阻塞变为只阻塞0.5秒 wait()只有在事件是阻塞状态时才会 进入阻塞状态,e.set()之后在执行e.wait()是没有任何效果的
print("连接成功")
except TimeoutError:
print("连接失败")
def check_conn():
time.sleep(random.randint(1,3)) # 随机睡,如果事件在1.5秒以内,就可以有机会设置事件非阻塞状态,因为conn_Mysql最多需要三次机会,一共1.5秒
e.set()
e=Event()
t2=Thread(target=check_conn)
t2.start()
t=Thread(target=conn_Mysql)
t.start()
# 结果呈现
尝试连接第1次
尝试连接第2次
连接成功
2.6 条件
- python提供的Condition对象提供了对复杂线程同步问题的支持。
- Condition被称为条件变量,除了提供与Lock类似的acquire和release方法外,还提供了wait和notify以及notify_all方法。
- wait:False 状态 会影响wait一直处于等待状态
- notify:(int数据类型) 回调 以指定每次开多少线程去执行任务
- notify_all:就是所有剩余的线程都去执行任务
from threading import Thread
from threading import Condition
def func(n):
con.acquire() # 需要锁,拿钥匙,注意Condition中的锁都是递归锁(所以才可以多次require)
con.wait() # 拿到钥匙之后还要等待,con.notify()放行几个线程来执行任务func 线程执行到这里时,会等着con.notify的参数
print("thread---%s"%n)
con.release() # 执行完之后释放钥匙
con=Condition() # 条件(锁+wait wait的作用就是线程拿到锁之后还要等着 看条件的notify参数 决定放几个线程来执行任务)
for i in range(10):
t=Thread(target=func,args=(i,)) # 创建线程
t.start() # 开启线程,但是有几个线程去操作数据,是根据条件condition的notify参数决定的
while True:
info=input(">>>") # 让用户输入数字,来决定允许几个线程去操作数据
if info=="q":
break # 必须得所有创建的线程都执行func,此时输入q才可以正常退出
con.acquire()
if info=="all":
con.notify_all() # 创建的所有线程都去执行func函数
else:
con.notify(int(info))
con.release()
2.7 定时器
- 定时器,定时开启一个线程 执行任务,接收两个参数
- 第一个就是隔多久开启,单位秒,
- 第二个参数就是开启线程需要执行的任务,也就是函数名
from threading import Timer
from threading import Thread
import time
def func1():
while True:
time.sleep(3)
print("in func1")
t=Thread(target=func1) # 开启一个线程执行任务func1,func1是一个死循环,所以开的线程t会一直存在,等着执行func1函数
t.start()
def func2():
print("in func2")
while True:
t=Timer(3,func2) # 每隔三秒开启一个线程执行func2函数,线程并不是一直存在,而是每隔一端时间去开启一个线程,执行打印任务
t.start()
t.join()
- 第一种 就是开一个线程,执行func1函数(死循环)所以开的线程会一直存在,第二种 是每隔一段时间去开一个线程执行func2()函数;
- 当sleep()的时间较短时,使用第一种方法,while 循环应该放在子线程内部;
- 当sleep()的时间较长,使用第二种方法,while循环放在主线程,也就是每隔一段时间开一个线程去执行任务;
2.8 队列
- queue队列 :使用import queue,用法与进程Queue一样
- queue is especially useful in threaded programming when information must be exchanged safely between multiple threads.
2.8.1 FIFO 先进先出的队列
class queue.Queue(maxsize=0)
import queue
q=queue.Queue() # 线程安全的,队列是先进先出FIFO
q.put("jin") #
q.put("egg")
print(q.qsize()) # 获得队列的大小
print(q.get()) # FIFO 所以先打印hello
print(q.get())
# 结果呈现
2
jin
egg
2.8.2 LOFI 后进先出的队列---栈
class queue.LifoQueue(maxsize=0)
# last in fisrt out
import queue
q = queue.LifoQueue() # 栈 先进后出
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())
# 结果呈现
3
2
1
2.8.3 带有优先级的队列
import queue
q = queue.PriorityQueue() # 优先级列表 从前到后,从小到大,ascii码的顺序
q.put((20,"a")) # put()需要接收一个元组(第一个参数是优先级,第二个是所要传的内容)
q.put((10,"b"))
q.put((30,"c"))
q.put((1,"z"))
q.put((1,"d"))
q.put((1,"?"))
print(q.get())
print(q.get())
# 结果呈现
(1, '?')
(1, 'd')
2.8.4 使用多线程借助队列实现生产者消费者模型
import queue
from threading import Thread
import time
import random
def procuder(name,food):
for i in range(10):
q.put((food,i))
# q.put((i,(food,i))) # 如果使用的是带有优先级的队列
print("%s生产%s--%s"%(name,food,i))
def consumer(name):
time.sleep(random.random())
food,i=q.get()
print("%s消费了%s"%(name,food))
q=queue.Queue()
# q=queue.LifoQueue() # 后进先出的队列
# q=queue.PriorityQueue() # 传参时需要传一个元组(第一个参数时优先级,第二个参数是内容)
t1=Thread(target=procuder,args=("xuanxuan","酸奶酪")) # 创建一个线程执行生产者--procuder
t1.start()
for i in range(10):
t=Thread(target=consumer,args=(i,)) # 创建10个线程执行consumer消费者
t.start()
2.9 Python标准模块--concurrent.futures
- 开启线程的成本要比开启进程的成本低,但是也不能任意的开线程
- 开进程池的数目是 cpu个数+1;
- 开线程池的数目是cpu 个数*5;
concurrent.futures
模块提供了高度封装的异步调用接口ThreadPoolExecutor
线程池,提供异步调用ProcessPoolExecutor
进程池,提供异步调用Both implement the same interface, which is defined by the abstract Executor class.
- 基本方法
- 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)
- 回调函数
- submit(fn, *args, **kwargs)
2.9.1 ThreadPoolExecutor
- submit()
from concurrent import futures
import time,random
def func(n):
time.sleep(random.random())
print("thread--%s"%n)
thread_pool=futures.ThreadPoolExecutor(5) # 一般开的线程池,开的线程数是cpu个数*5 但是这里我就开五个线程
for i in range(10): # 有10个任务需要执行。开五个线程轮流去执行这10个任务
thread_pool.submit(func,i) # thread_pool.submit() 相当于t=Thread(func,i)创建线程 和t.start() 开启线程两步
# 结果呈现
thread--4
thread--1
thread--3
thread--6
thread--0
thread--5
thread--2
thread--7
thread--9
thread--8
- result()
- futures.ThreadPoolExexutor(num)是可以接收线程执行函数的返回值的,等到的对象使用result()方法去获取
from concurrent import futures
import time,random
def func(n):
time.sleep(random.random())
print("thread--%s"%n)
return n * "*" # 线程执行函数具有返回值
thread_pool=futures.ThreadPoolExecutor(5)
for i in range(10):
ret = thread_pool.submit(func,i)
print(ret.result())
# futures.ThreadPoolExecutor(num) 建立线程池对象thread_pool,然后使用thread_pool线程池对象.submit(func,args)方法开启线程
# 如果线程执行的函数具有返回值,则submit()的结果是可以拿到返回值的,直接使用ret.result()方法获取
# 但是这样 多个线程之间执行就变同步了,因为每当执行一个线程就需要result()获取线程执行函数的返回值,就需要把函数执行完,才可以拿到,然后才会轮到下一个线程执行
# 结果呈现
thread--0
thread--1
*
thread--2
**
thread--3
***
thread--4
****
thread--5
*****
thread--6
******
thread--7
*******
thread--8
********
thread--9
*********
- 多个线程之间异步并发执行
from concurrent import futures
import time,random
def func(n):
time.sleep(random.random())
print("thread--%s" % n)
return n*"*" # 线程执行函数具有返回值
thread_pool=futures.ThreadPoolExecutor(5)
ret_lst=[] # 存放开启线程执行函数的返回值,先同时开启很多个线程,但是并不一定要去执行函数拿到返回值,最后统一获取返回值,这样多个线程仍然是异步并发执行的
for i in range(10):
ret=thread_pool.submit(func,i)
ret_lst.append(ret)
[print(ret.result()) for ret in ret_lst]
# 这样多个线程就实现异步并发,之前是一个线程必须去执行函数才能拿到返回值,接着才轮到下一个线程,这样就可以异步开多个线程,先不一定要真的去执行func
# 这种方法可以实现多个线程异步,而且打印多个线程的返回值,有可能是跟上面线程执行函数打印的结果交叉
# (因为很有可能有的线程执行的快,结果都要返回了,其他线程可能才去执行函数,做线程内的打印)
# 结果呈现
thread--2
thread--0
thread--5
thread--4
thread--6
thread--1
*
**
thread--3
***
****
*****
******
thread--8
thread--9
thread--7
*******
********
*********
- shutdown()
- 在所有线程异步执行完函数内代码打印,然后再统一打印线程执行函数的返回值,可以使用thread_pool.shutdown() 相当于进程池在主进程中p.close() p.join() 是一样的道理(就是不允许往进程池添加任务,然后让主进程等待主线程执行完毕)
from concurrent import futures
import time,random
def func(n):
time.sleep(random.random())
print("thread--%s" % n)
return n*"*"
thread_pool=futures.ThreadPoolExecutor(5)
ret_lst=[]
for i in range(10):
ret=thread_pool.submit(func,i)
ret_lst.append(ret)
thread_pool.shutdown() # 相当于进程池的p.close() p.join() 等待上面所有线程执行函数结束(异步并发),才会统一打印线程的执行函数的返回值
[print(ret.result()) for ret in ret_lst]
# 结果呈现
thread--2
thread--1
thread--4
thread--0
thread--5
thread--3
thread--8
thread--6
thread--7
thread--9
*
**
***
****
*****
******
*******
********
*********
2.9.2 map的用法
- map() 得到的多个线程执行任务是异步并发的,但是多个线程执行函数有返回值时,map()是获取不到的;
from concurrent import futures
import time,random
def func(n):
time.sleep(random.random())
print("thread--%s"%n)
return n*"*" # 使用map()开启线程,即使线程执行函数有返回值,map()也拿不到
thread_pool=futures.ThreadPoolExecutor(5) # 线程池有五个线程
thread_pool.map(func,range(10)) # map()函数接收第一个参数是多个线程需要执行的任务,第二个参数是一个可迭代的对象---10个任务,但是五个线程去执行
# 结果呈现
thread--1
thread--2
thread--3
thread--4
thread--7
thread--6
thread--5
thread--0
thread--9
thread--8
2.9.3 回调函数
- 回调函数
add_done_callback
---参数就是多线程执行函数的返回值
from concurrent import futures
import time,random
def func(n):
time.sleep(random.random())
print("thread--%s"%n)
return n*"*" # 线程执行函数的返回值传给回调函数
def call_func(args): # 回调函数的参数接收的是多线程执行函数的返回值
print(args.result()) # submit()可以拿到多线程执行函数的返回值,但是得使用result()才可以获得
thread_pool=futures.ThreadPoolExecutor(5) # 线程池开五个线程
for i in range(10): # 有10个任务,但是开五个线程轮流执行
thread_pool.submit(func,i).add_done_callback(call_func)
# 结果呈现
thread--1
*
thread--0
thread--4
****
thread--2
**
thread--3
***
thread--8
********
thread--9
*********
thread--6
******
thread--5
*****
thread--7
*******