自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm=1001.2014.3001.5501
我们已经知道进程之间不能直接共享信息,那么线程之间可以共享信息吗?我们通过一个例子来验证一下。定义一个全局变量g_num,分别创建2个子线程对g_num执行不同的操作,并输出操作后的结果。代码如下:
# -*- coding:utf-8 -*-
from threading import Thread # 导入线程
import time
def plus(): # 第一个线程函数
print('-------子线程1开始------')
global g_num # 定义全局变量
g_num += 50 # 全局变量值加50
print('g_num is %d'%g_num)
print('-------子线程1结束------')
def minus(): # 第二个线程函数
time.sleep(1) # 休眠1毫秒
print('-------子线程2开始------')
global g_num # 定义全局变量
g_num -= 50 # 全局变量值减50
print('g_num is %d'%g_num)
print('-------子线程2结束------')
g_num = 100 # 定义一个全局变量
if __name__ == '__main__':
print('-------主线程开始------')
print('g_num is %d'%g_num)
t1 = Thread(target=plus) # 实例化线程t1
t2 = Thread(target=minus) # 实例化线程t2
t1.start() # 开启线程t1
t2.start() # 开启线程t2
t1.join() # 等待t1线程结束
t2.join() # 等待t2线程结束
print('-------主线程结束------')
上述代码中,定义一个全局变量g_num,赋值为100,然后创建2个线程。一个线程将g_num增加50,一个线程将g_num减少50。如果g_num的最终结果为100,则说明线程之间可以共享数据。运行结果如图17所示。
图17 检测线程数据是否共享
从上面的例子可以得出,在一个进程内的所有线程共享全局变量,能够在不使用其他方式的前提下完成多线程之间的数据共享。
1 什么是互斥锁
由于线程可以对全局变量随意修改,这就可能造成多线程之间对全局变量的混乱操作。依然以房子为例,当房子内只有一个居住者时(单线程),他可以任意时刻使用任意一个房间,如厨房、卧室和卫生间等。但是,当这个房子有多个居住者时(多线程),他就不能在任意时刻使用某些房间,如卫生间,否则就会造成混乱。
如何解决这个问题呢?一个防止他人进入的简单方法,就是门上加一把锁。先到的人锁上门,后到的人就在门口排队,等锁打开再进去。如图18所示。
图18 互斥锁示意图
这就是“互斥锁”(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。互斥锁为资源引入一个状态:锁定和非锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”时,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
2 使用互斥锁
在threading模块中使用Lock类可以方便地处理锁定。Lock类有2个方法:acquire()锁定和release()释放锁。示例用法如下:
mutex = threading.Lock() # 创建锁
mutex.acquire([blocking]) # 锁定
mutex.release() # 释放锁
参数说明如下:
l acquire([blocking]):获取锁定,如果有必要,需要阻塞到锁定释放为止。如果提供blocking参数并将它设置为False,当无法获取锁定时将立即返回False;如果成功获取锁定则返回True。
l release():释放一个锁定。当锁定处于未锁定状态时,或者从与原本调用acquire()方法的不同线程调用此方法,将出现错误。
实例06 使用互斥锁实现多人同时订购电影票
下面,通过一个示例学习一下如何使用互斥锁。这里使用多线程和互斥锁模拟实现多人同时订购电影票的功能,假设电影院某个场次只有100张电影票,10个用户同时抢购该电影票。每售出一张,显示一次剩余的电影票张数。代码如下:
from threading import Thread,Lock
import time
n=100 # 共100张票
def task():
global n
mutex.acquire() # 上锁
temp=n # 赋值给临时变量
time.sleep(0.1) # 休眠0.1秒
n=temp-1 # 数量减1
print('购买成功,剩余%d张电影票'%n)
mutex.release() # 释放锁
if __name__ == '__main__':
mutex=Lock() # 实例化Lock类
t_l=[] # 初始化一个列表
for i in range(10):
t=Thread(target=task) # 实例化线程类
t_l.append(t) # 将线程实例存入列表中
t.start() # 创建线程
for t in t_l:
t.join() # 等待子线程结束
上述代码中,创建了10个线程,全部执行task()函数。为解决资源竞争问题,使用mutex.acquire()函数实现资源锁定,第一个获取资源的线程锁定后,其他线程等待mutex.release()解锁。所以每次只有一个线程执行task()函数。运行结果如图19所示。
图19 模拟购票功能
注意:使用互斥锁时,要避免死锁。在多任务系统下,当一个或多个线程等待系统资源,而资源又被线程本身或其他线程占用时,就形成了死锁,如图20所示。
图20 死锁示意图
3 使用队列在线程间通信
我们知道multiprocessing模块的Queue队列可以实现进程间通信,同样在线程间,也可以使用Queue队列实现线程间通信。不同之处在于我们需要使用queue模块的Queue队列,而不是multiprocessing
模块的Queue队列,但Queue的使用方法相同。
使用Queue在线程间通信通常应用于生产者消费者模式。产生数据的模块称为生产者,而处理数据的模块称为消费者。在生产者与消费者之间的缓冲区称之为仓库。生产者负责往仓库运输商品,而消费者负责从仓库里取出商品,这就构成了生产者消费者模式。下面通过一个示例学习一下使用Queue在线程间通信。
实例07 使用队列模拟生产者消费者模式
定义一个生产者类Producer,定义一个消费者类Consumer。生成者生成5件产品,依次写入队列,而消费者依次从队列中取出产品,代码如下:
from queue import Queue
import random,threading,time
# 生产者类
class Producer(threading.Thread):
def __init__(self, name,queue):
threading.Thread.__init__(self, name=name)
self.data=queue
def run(self):
for i in range(5):
print("生成者%s将产品%d加入队列!" % (self.getName(), i))
self.data.put(i)
time.sleep(random.random())
print("生成者%s完成!" % self.getName())
# 消费者类
class Consumer(threading.Thread):
def __init__(self,name,queue):
threading.Thread.__init__(self,name=name)
self.data=queue
def run(self):
for i in range(5):
val = self.data.get()
print("消费者%s将产品%d从队列中取出!" % (self.getName(),val))
time.sleep(random.random())
print("消费者%s完成!" % self.getName())
if __name__ == '__main__':
print('-----主线程开始-----')
queue = Queue() # 实例化队列
producer = Producer('Producer',queue) # 实例化线程Producer,并传入队列作为参数
consumer = Consumer('Consumer',queue) # 实例化线程Consumer,并传入队列作为参数
producer.start() # 启动线程Producer
consumer.start() # 启动线程Consumer
producer.join() # 等待线程producer结束
consumer.join() # 等待线程consumer结束
print('-----主线程结束-----')
运行结果如图21所示。
图21 使用Queue在线程间通信
注意:由于程序中使用了random.random()函数生成0-1之间的随机数,所以读者的运行结果可能与图21不同。