线程Thread
什么是线程

线程(Thread)特点:
- 线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 拥有自己独立的栈和共享的堆,共享堆,不共享栈,标准线程由操作系统调度;
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
线程的创建方式
Python的标准库提供了两个模块:_thread和threading,
_thread是低级模块,threading是高级模块,
对_thread进行了封装。
绝大多数情况下,我们只需要使用threading这个高级模块。
线程的创建可以通过分为两种方式:
1. 方法包装
2. 类包装
线程的执行统一通过start()方法
线程的创建方式(方法包装)
#encoding = utf-8
from threading import Thread
from time import sleep
#创建方法a1
def a1(name):
print(f'线程{name},start')
for i in range(3):
print(f'线程{name},{i}')
sleep(1) # 睡一秒
print(f'线程{name},end')
if __name__ == '__main__':
#主线程启动
print('主线程,start')
#方法创建线程t1,t2
t1 = Thread(target=a1,args=('t1',)) #target代表此线程启动执行的方法
t2 = Thread(target=a1,args=('t2',)) #args 代表此线程启动传入的参数,参数为元组
#启动创建的线程
t1.start()
t2.start()
print('主线程,end')
运行结果可能会出现换行问题,是因为多个线程抢夺控制台输出的IO流,是正常情况。
线程的创建方式(类包装)
#encoding = utf-8
from threading import Thread
from time import sleep
#定义类,继承Thread
class MyThread(Thread):
#初始化
def __init__(self,name):
Thread.__init__(self)
self.name = name
#重写run方法
def run(self):
print(f'线程{self.name},start')
for i in range(3):
print(f'线程{self.name},{i}')
sleep(1) # 睡一秒
print(f'线程{self.name},end')
if __name__ == '__main__':
print('主线程,start')
#类方法创建线程t1,t2
t1 = MyThread('t1')
t2 = MyThread('t2')
#启动创建的线程
t1.start() #执行run方法
t2.start()
print('主线程,end')
join()
之前的代码,主线程不会等待子线程结束。
如果需要等待子线程结束后,再结束主线程,可使用join()方法。
#encoding = utf-8
from threading import Thread
from time import sleep
#创建方法a1
def a1(name):
print(f'线程{name},start')
for i in range(3):
print(f'线程{name},{i}')
sleep(1) # 睡一秒
print(f'线程{name},end')
if __name__ == '__main__':
#主线程启动
print('主线程,start')
#创建线程t1
t1 = Thread(target=a1,args=('t1',)) #target代表此线程启动执行的方法
t2 = Thread(target=a1,args=('t2',)) #args 代表此线程启动传入的参数,参数为元组
#启动创建的线程
t1.start()
t2.start()
#join()使线程t1,t2结束后,主线程才能结束
t1.join()
t2.join()
print('主线程,end')
守护线程
在行为上还有一种叫守护线程,主要的特征是它的生命周期。
主线程死亡,它也就随之死亡。在python中,线程通过setDaemon(True|False)来设置是否为守护线程。
守护线程的作用:
- 守护线程作用是为其他线程提供便利服务,守护线程最典型的应用就是 GC (垃圾收集器)。
#encoding = utf-8
from threading import Thread
from time import sleep
#创建方法a1
def a1(name):
print(f'线程{name},start')
for i in range(3):
print(f'线程{name},{i}')
sleep(1) # 睡一秒
print(f'线程{name},end')
if __name__ == '__main__':
#主线程启动
print('主线程,start')
#创建线程t1
t1 = Thread(target=a1,args=('t1',))
#守护线程,主线程结束后,线程t1也随之消亡
t1.daemon = True
#启动创建的线程
t1.start()
全局锁GIL问题

在python中,无论你有多少核,在Cpython解释器中永远都是假象。无论你是4核,8核,还是16核.......不好意思,同一时间执行的线程只有一个线程,它就是这个样子的。这个是python的一个开发时候,设计的一个缺陷,所以说python中的线程是“含有水分的线程”。
Python GIL(Global Interpreter Lock)
Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。

⚠️GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行,就没有GIL的问题。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷
线程同步和互斥锁
现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题。
比如:教室里,只有一台电脑,多个人都想使用。天然的解决办法就是,在电脑旁边,大家排队。前一人使用完后,后一人再使用。再比如,上厕所排队。
线程同步
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。

【示例】多线程操作同一个对象(未使用线程同步)
#encoding = utf-8
from threading import Thread
from time import sleep
#账户(包含名称与余额)
class Account:
def __init__(self,name,money):
self.name = name
self.money = money
class Drawing(Thread):
def __init__(self,drawingNum,account): #drawingNum表示取钱的金额
Thread.__init__(self)
self.drawingNum = drawingNum
self.account = account
self.expenseTotal = 0 #表示取钱的总金额累计多少
def run(self):
if self.account.money<self.drawingNum:
return
#模拟线程不同步会出现的问题
sleep(1) #线程1睡眠时,还没有取钱,余额不变,导致线程2判断余额足够
self.account.money-=self.drawingNum
self.expenseTotal+=self.drawingNum
print(f"账户:{self.account.name},总共取了:{self.expenseTotal}")
print(f"账户:{self.account.name},余额是:{self.account.money}")
if __name__ == '__main__':
a1 = Account('张三',100)
draw1 = Drawing(80,a1) # 定义取钱线程对象1;
draw2 = Drawing(80,a1) # 定义取钱线程对象2;
draw1.start() # 第一次取钱
draw2.start() # 第二次取钱
两个线程同时操作同一个账户对象,竟然从只有100元的账户,轻松取出80*2=160元,账户余额竟然成为了-60。这是由于没有线程同步机制,导致的问题。
我们可以通过“锁机制”来实现线程同步问题,锁机制有如下几个要点:
- 必须使用同一个锁对象
- 互斥锁的作用就是保证同一时刻只能有一个线程去操作共享数据,保证共享数据不会出现错误问题
- 使用互斥锁的好处确保某段关键代码只能由一个线程从头到尾完整地去执行
- 使用互斥锁会影响代码的执行效率
- 同时持有多把锁,容易出现死锁的情况
互斥锁
互斥锁: 对共享数据进行锁定,保证同一时刻只能有一个线程去操作。
注意: 互斥锁是多个线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程需要等待,等互斥锁使用完释放后,其它等待的线程再去抢这个锁。
threading模块中定义了Lock变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。
【示例】多线程操作同一个对象(增加互斥锁,使用线程同步)
#encoding = utf-8
from threading import Thread,Lock
from time import sleep
#账户(包含名称与余额)
class Account:
def __init__(self,name,money):
self.name = name
self.money = money
class Drawing(Thread):
def __init__(self,drawingNum,account): #drawingNum表示取钱的金额
Thread.__init__(self)
self.drawingNum = drawingNum
self.account = account
self.expenseTotal = 0 #表示取钱的总金额累计多少
def run(self):
#acquire和release方法之间的代码同一时刻只能有一个线程去操作
lock.acquire()
if self.account.money<self.drawingNum:
return
#模拟线程不同步会出现的问题
sleep(1) #线程1睡眠时,还没有取钱,余额不变,导致线程2判断余额足够
self.account.money-=self.drawingNum
self.expenseTotal+=self.drawingNum
lock.release()
print(f"账户:{self.account.name},总共取了:{self.expenseTotal}")
print(f"账户:{self.account.name},余额是:{self.account.money}")
if __name__ == '__main__':
a1 = Account('张三',100)
#使用互斥锁
lock = Lock()
draw1 = Drawing(80,a1) # 定义取钱线程对象1;
draw2 = Drawing(80,a1) # 定义取钱线程对象2;
draw1.start() # 第一次取钱
draw2.start() # 第二次取钱
acquire和release方法之间的代码同一时刻只能有一个线程去操作- 如果在调用
acquire方法的时候 其他线程已经使用了这个互斥锁,那么此时acquire方法会堵塞,直到这个互斥锁释放后才能再次上锁。
死锁问题和解决方案
死锁
在多线程程序中,死锁问题很大一部分是由于一个线程同时获取多个锁造成的。
举例:有两个人都要做饭,都需要“锅”和“菜刀”才能炒菜。

from threading import Thread, Lock
from time import sleep
def fun1():
lock1.acquire()
print('fun1拿到锁1')
sleep(2)
lock2.acquire()
print('fun1去拿锁2')
lock2.release()
print('fun1释放锅')
lock1.release()
print('fun1释放锁1')
def fun2():
lock2.acquire()
print('fun2拿到锁2')
lock1.acquire()
print('fun2去拿锁1')
lock1.release()
print('fun2释放锁1')
lock2.release()
print('fun2释放锁2')
if __name__ == '__main__':
lock1 = Lock()
lock2 = Lock()
t1 = Thread(target=fun1)
t2 = Thread(target=fun2)
t1.start()
t2.start()
由于方法1拿了锁1,想要拿到锁2再释放锁1,
可是方法2拿了锁2没有释放,导致方法1不能去拿到锁2了
死锁的解决方法
死锁是由于“同步块需要同时持有多个锁造成”的,要解决这个问题,思路很简单,就是:
同一个代码块,不要同时持有两个对象锁。
信号量(Semaphore)

互斥锁使用后,一个资源同时只有一个线程访问。如果某个资源,我们同时想让N个(指定数值)线程访问?这时候,可以使用信号量。
信号量控制同时访问资源的数量。信号量和锁相似,锁同一时间只允许一个对象(进程)通过,信号量同一时间允许多个对象(进程)通过。
应用场景
- 在读写文件的时候,一般只能只有一个线程在写,而读可以有多个线程同时进行,如果需要限制同时读文件的线程个数,这时候就可以用到信号量了(如果用互斥锁,就是限制同一时刻只能有一个线程读取文件)。
- 在做爬虫抓取数据时。
底层原理
信号量底层就是一个内置的计数器。每当资源获取时(调用acquire)计数器-1,资源释放时(调用release)计数器+1。
from threading import Thread, Lock, Semaphore
from time import sleep
def home(name,se):
#拿到锁
se.acquire()
print(f'{name}进入房间')
sleep(2)
print(f'{name}离开房间')
#释放锁
se.release()
if __name__ == '__main__':
#创建信号量为2
se = Semaphore(2)
#创建7个线程
for i in range(7):
t = Thread(target=home,args=(f'tom{i}',se))
t.start()
事件(Event)
事件Event主要用于唤醒正在阻塞等待状态的线程;

原理
Event 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,event 对象中的信号标志被设置假。如果有线程等待一个 event 对象,而这个 event 对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个 event 对象的信号标志设置为真,它将唤醒所有等待个 event 对象的线程。如果一个线程等待一个已经被设置为真的 event 对象,那么它将忽略这个事件,继续执行
Event()可以创建一个事件管理标志,该标志(event)默认为False,event对象主要有四种方法可以调用:
| 方法名 | 说明 |
|---|---|
event.wait(timeout=None) | 调用该方法的线程会被阻塞,如果设置了timeout参数,超时后,线程会停止阻塞继续执行; |
event.set() | 将event的标志设置为True,调用wait方法的所有线程将被唤醒 |
event.clear() | 将event的标志设置为False,调用wait方法的所有线程将被阻塞 |
event.is_set() | 判断event的标志是否为True |
【示例】Event事件对象经典用法
#coding:utf-8
import threading
import time
def chihuoguo(name):
#等待事件,进入等待阻塞状态
print(f'{name}已经启动')
print(f'小伙伴{name}已经进入就餐状态!')
time.sleep(1)
event.wait()
# 收到事件后进入运行状态
print(f'{name}收到通知了.' )
print(f'小伙伴{name}开始吃咯!')
if __name__ == '__main__':
#创建阻塞线程的event对象
event = threading.Event()
# 创建新线程
thread1 = threading.Thread(target=chihuoguo, args=("tom", ))
thread2 = threading.Thread(target=chihuoguo, args=("cherry", ))
# 开启线程
thread1.start()
thread2.start()
time.sleep(5)
# 发送事件通知
print('---->>>主线程通知小伙伴开吃咯!')
event.set()
生产者和消费者模式

多线程环境下,我们经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。
![]()
什么是生产者?
生产者指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
什么是消费者?
消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)
什么是缓冲区?
消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。
生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。
缓冲区是实现并发的核心,缓冲区的设置有3个好处:
- 实现线程的并发协作
有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。
- 解耦了生产者和消费者
生产者不需要和消费者直接打交道
- 解决忙闲不均,提高效率
生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据
缓冲区和queue对象
从一个线程向另一个线程发送数据最安全的方式可能就是使用 queue 库中的队列了。创建一个被多个线程共享的 Queue 对象,这些线程通过使用 put() 和 get()操作来向队列中添加或者删除元素。Queue 对象已经包含了必要的锁,所以你可以通过它在多个线程间多安全地共享数据。
#coding=utf-8
from queue import Queue
from threading import Thread
from time import sleep
def producer(): #生产者
num = 1
while True:
if queue.qsize()<5: #最多只能存放5个馒头
print(f'生产:{num}号馒头')
queue.put(f'{num}号馒头') #将馒头放入框,使用了一次put,queue.qsize()数量+1
num+=1
else:
print('馒头框满了')
sleep(1)
def consumer(): #消费者
while True:
print(f'获取馒头:{queue.get()}') #将馒头取出,使用了一次get,queue.qsize()数量-1
#queue.get()可以被打印,打印的是queue.put括号里面的东西
sleep(1)
if __name__ == '__main__':
#创建queue对象,用来存放馒头
queue = Queue()
t1 = Thread(target=producer) #生产馒头的线程
t1.start()
t2 = Thread(target=consumer) #消费馒头的线程
t2.start()

2537



被折叠的 条评论
为什么被折叠?



