线程的特点:
- 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中实际运作的单位;
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 拥有自己独立的栈和共享的堆。标准线程由操作系统调度
- 调度与切换。线程上下文切换比进程上下文切换要快得多
线程的创建方式:
Python的标准库提供了两个模块:_thread和threading。_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。
线程的创建可以通过两种方式:
- 方法包装
- 类包装
线程的执行统一通过start()方法
# encoding=utf-8
# 方法包装创建线程
from threading import Thread
from time import sleep
def func1(name):
print(f"线程{name},Start")
for i in range(3):
print(f"线程:{name},{i}")
sleep(3)
print(f"线程{name},End")
if __name__ == '__main__':
print("主线程,Start")
# 创建线程
t1 = Thread(target=func1, args=("t1",))
t2 = Thread(target=func1, args=("t2",))
# 启动线程
t1.start()
t2.start()
print("主线程,End")
# 输出
"""
主线程,Start
线程t1,Start
线程:t1,0
线程t2,Start
线程:t2,0主线程,End
线程:t2,1线程:t1,1
线程:t2,2
线程:t1,2
线程t1,End
线程t2,End
Process finished with exit code 0
"""
#encoding=utf-8
# 类包装创建线程
from threading import Thread
from time import sleep
class MyThread(Thread):
def __init__(self,name):
Thread.__init__(self)
self.name = name
def run(self):
for i in range(3):
print(f"thread:{self.name}-{i}")
sleep(1)
if __name__ == '__main__':
print("主线程,Start")
# 创建线程(类方式)
t1 = MyThread('t1')
t2 = MyThread('t2')
# 启动线程
t1.start()
t2.start()
print("主线程,end")
# 输出
"""
主线程,Start
thread:t1-0
thread:t2-0主线程,end
thread:t2-1thread:t1-1
thread:t2-2thread:t1-2
Process finished with exit code 0
"""
join()和守护线程
之前的代码,主线程不会等待子线程结束。如果需要等待子线程结束后再结束主线程,可以使用join()方法。
# encoding=utf-8
from threading import Thread
from time import sleep
def func1(name):
for i in range(3):
print(f"thread:{name}:{i}")
sleep(1)
if __name__ == '__main__':
print("主线程,start")
# 创建线程
t1 = Thread(target=func1,args=("t1",))
t2 = Thread(target=func1,args=("t2",))
# 启动线程
t1.start()
t2.start()
# 主线程会等待t1,t2结束后,再往下执行
t1.join()
t2.join()
print("主线程,end")
# 输出
"""
主线程,start
thread:t1:0
thread:t2:0
thread:t2:1thread:t1:1
thread:t2:2thread:t1:2
主线程,end
Process finished with exit code 0
"""
在行为上还有一种叫守护线程,主要的特征是它的生命周期。主线程死亡,它也随之死亡。在Python中,线程通过setDaemon(True|False)来设置是否为守护线程。守护线程的作用是为其他线程提供便利服务,守护线程最典型的应用就是GC(垃圾收集器)
# encoding=utf-8
from threading import Thread
from time import sleep
class MyThread(Thread):
def __init__(self,name):
Thread.__init__(self)
self.name = name
def run(self):
for i in range(3):
print(f"thread:{self.name}:{i}")
sleep(1)
if __name__ == '__main__':
print("主线程,Start")
# 创建线程
t1 = MyThread('t1')
# t1设置为守护线程
# 3.10后被废弃,可以直接使用:t1.daemon = True
t1.setDaemon(True)
# 启动线程
t1.start()
print("主线程,end")
# 输出
"""
主线程,Start
thread:t1:0
主线程,end
Process finished with exit code 0
"""
全局解释器锁GIL问题
在Python中,无论有多少核,在Cpython解释器中永远都是假象。同一时间执行的线程只有一个。
GIL(Global Interpreter Lock)
Python代码的执行由Python虚拟机来控制,Python设计之初就考虑要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
线程同步
线程同步概念:处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这时候,我们就需要用到线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。
# encoding=utf-8
# 模拟两个人同时从同一个账户取钱
from threading import Thread
from time import sleep
class Account:
def __init__(self,money,name):
self.money = money
self.name = name
class Drawing(Thread):
"""
模拟去取款操作
"""
def __init__(self,drawingNum,account):
Thread.__init__(self)
self.drawingNum = drawingNum
self.account = account
self.expenseTotal = 0
def run(self):
if self.account.money < self.drawingNum:
return
sleep(1) # 判断完后阻塞。其他线程开始运行。
self.account.money -= self.drawingNum
self.expenseTotal += self.drawingNum
print(f"账户:{self.account.name},余额是:{self.account.money}")
print(f"账户:{self.account.name},总共取了:{self.expenseTotal}")
if __name__ == '__main__':
a1 = Account(100,"竹筒饭")
draw1 = Drawing(80,a1) # 定义取钱行为这个线程的对象
draw2 = Drawing(80,a1)
draw1.start()
draw2.start()
# 输出
"""
账户:竹筒饭,余额是:20账户:竹筒饭,余额是:-60
账户:竹筒饭,总共取了:80
账户:竹筒饭,总共取了:80
Process finished with exit code 0
"""
互斥锁
可以使用锁机制解决线程同步问题,锁机制要点:
- 必须使用一个锁对象
- 互斥锁的作用就是保证同一时刻只能有一个线程去操作共享数据,保证共享数据不会出现错误问题
- 使用互斥锁的好处是确保某段关键代码只能由一个线程从头到尾完整的去执行
- 使用互斥锁会影响代码的执行效率
- 同时持有多把锁,容易出现死锁的情况
互斥锁:对共享数据进行锁定,保证同一时刻只能一个线程去操作。
互斥锁是多个线程一起抢,抢到锁的线程先执行,没抢到的线程需要等待,等互斥锁使用完释放后,其他等待的线程再去抢这个锁。
threading模块中定义了Lock变量,这个变量本质是是一个函数,通过调用这个函数可以获取一把互斥锁。
# encoding=utf-8
from threading import Thread,Lock
from time import sleep
class Account:
def __init__(self,money,name):
self.money = money
self.name = name
class Drawing(Thread):
def __init__(self,drawingNum,account):
Thread.__init__(self)
self.drawingNum = drawingNum
self.account = account
self.expenseTotal = 0
def run(self):
lock1.acquire()
if self.account.money - self.drawingNum < 0:
print("账户余额不足")
return
sleep(1)
self.account.money -= self.drawingNum
self.expenseTotal += self.drawingNum
lock1.release()
print(f"账户:{self.account.name},余额是:{self.account.money}")
print(f"账户:{self.account.name},总共取出:{self.expenseTotal}")
if __name__ == '__main__':
a1 = Account(100,"竹筒饭")
lock1 = Lock()
draw1 = Drawing(80,a1)
draw2 = Drawing(80,a1)
draw1.start()
draw2.start()
# 输出
"""
账户:竹筒饭,余额是:20
账户:竹筒饭,总共取出:80
账户余额不足
Process finished with exit code 0
"""
死锁
在多线程程序中,死锁问题很大一部分是由于一个线程同时获取多个锁造成的。所以解决方案就是同一个代码块不要同时持有多个对象锁。
# encoding=utf-8
# 模拟一套厨具,两个厨师做菜
from threading import Thread,Lock
from time import sleep
def fun1():
lock1.acquire()
print("fun1拿到锅铲")
sleep(2)
lock2.acquire()
print("fun1拿到锅")
lock2.release()
print("fun1释放锅")
lock1.release()
print("fun1释放锅铲")
def fun2():
lock2.acquire()
print("fun2拿到锅")
lock1.acquire()
print("fun1拿到锅铲")
lock1.release()
print("fun1释放锅铲")
lock2.release()
print("fun1释放锅")
if __name__ == '__main__':
lock1 = Lock()
lock2 = Lock()
t1 = Thread(target=fun1)
t2 = Thread(target=fun2)
t1.start()
t2.start()
信号量(Semaphore)
互斥锁使用后,一个资源同时只有一个线程访问。如果某个资源我们想同时让N个线程访问,这时候可以使用信号量。
信号量控制同时访问资源的数量。信号量和锁相似,锁同一时间只允许一个对象(进程)通过,信号量同一时间允许多个对象(进程)通过。
应用场景:
- 在读写文件时,一般只能有一个线程在写,而读可以有多个线程同时进行,如果需要限制同时读文件的线程个数,就可以使用信号量。
- 在做爬虫抓取数据时
底层原理:
信号量底层就是一个内置的计数器。每当资源获取时(调用acquire)计数器-1,资源释放时(调用release)计数器+1。
# coding=utf-8
from threading import Thread
from time import sleep
from multiprocessing import Semaphore
"""
一个房间一次只允许两个人进入
"""
def home(name,se):
se.acquire() # 拿到一把钥匙
print(f"{name}进入了房间")
sleep(2)
print(f"{name}走出了房间")
se.release() # 还回一把钥匙
if __name__ == '__main__':
se = Semaphore(2) # 创建信号量对象,有两把钥匙
for i in range(7):
p = Thread(target=home,args=(f"People{i}",se))
p.start()
"""
输出结果:
People0进入了房间
People1进入了房间
People0走出了房间
People1走出了房间People2进入了房间
People3进入了房间
People3走出了房间
People4进入了房间People2走出了房间
People5进入了房间
People4走出了房间People5走出了房间
People6进入了房间
People6走出了房间
Process finished with exit code 0
"""
事件Event对象
Event主要用于唤醒正在阻塞等待状态的线程。Event对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,Event对象中的信号被设置为假。如果有线程等待一个Event对象,而这个Event对象的标志为假,那么这个线程会被一致阻塞直到该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个以及被设置为真的Event对象,那么它将忽略这个事件,继续执行。
Event()可以创建以恶搞事件管理标志,该标志(event)默认为False,event对象主要有四种方法可以调用:
方法名 | 说明 |
event.wait(timeout=None) | 调用该方法的线程会被阻塞,如果设置了timeout参数,超时后,线程会停止阻塞继续执行 |
event.set() | 将event的标志设置为True,调用wait方法的所有线程将被唤醒 |
event.release() | 将event的标志设置为False,调用wait方法的所有线程将被阻塞 |
event.is_set() | 判断event的标志是否为True |
# encoding=utf-8
"""
场景模拟:跑步比赛,只有当裁判鸣枪之后,
所有运动员才开始起跑
"""
import threading
import time
def run(name):
# 等待事件,进入阻塞状态
print(f"{name}已经启动")
print(f"{name}已进入起跑状态")
time.sleep(1)
event.wait()
# 收到事件后进入起跑状态
print(f"{name}收到通知")
print(f"{name}起跑")
if __name__ == '__main__':
event = threading.Event()
# 创建新线程
thread1 = threading.Thread(target=run,args=("Archer",))
thread2 = threading.Thread(target=run,args=("Saber",))
# 开启线程
thread1.start()
thread2.start()
time.sleep(9)
# 发送事件通知
print("************鸣枪起跑****************")
event.set()
"""
运行结果:
Archer已经启动
Archer已进入起跑状态
Saber已经启动
Saber已进入起跑状态
************鸣枪起跑****************
Saber收到通知
Saber起跑
Archer收到通知
Archer起跑
Process finished with exit code 0
"""
生产者消费者模式
多线程环境下,经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发写作模型,生产者消费者模式。
生产者:指的是负责生产数据的模块。这里的模块可能时方法、对象、线程、进程。
消费者:指的是处理数据的模块,这里的模块可能是方法、对象、线程、进程。
缓冲区:消费者不能直接使用生产者的数据,它们之间有缓冲区。生产者将生产好的数据放入缓冲区,消费者从缓冲区拿要处理的数据。
缓冲区是实现并发的核心,缓冲区的设置有3个好处:
- 实现线程的并发协作。有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者的消费情况。同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者的情况。这样就从逻辑上实现了生产者线程和消费者线程的分离。
- 解耦了生产者和消费者。生产者不需要和消费者直接打交道
- 解决忙闲不均,提高效率。生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费,消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据
缓冲区和queue对象
从一个线程向另一个线程发送数据最安全的方式可能就是使用queue库中的队列了。创建一个被多个线程共享的Queue对象,这些线程通过使用put()和get()操作来向队列中添加或者删除元素。Queue对象已经包含了必要的锁,所以你可以通过它在多个线程间安全的共享数据。
# encoding=utf-8
from queue import Queue
from threading import Thread
from time import sleep
def producer():
num = 1
while True:
if queue.qsize() < 5:
print(f"生产{num}号数据")
queue.put(f"数据{num}号")
num += 1
else:
print("缓冲区满了,等待消费者取走数据")
sleep(1)
def consumer():
while True:
print(f"获取数据:{queue.get()}")
sleep(3)
if __name__ == '__main__':
queue = Queue()
t = Thread(target=producer)
t.start()
c = Thread(target=consumer)
c.start()
c2 = Thread(target=consumer)
c2.start()