一、多线程的引入
1、什么是多线程
默认情况下,一个程序只有一个线程,代码是依次线性执行的,而多线程则可以并发执行,一次性多个人做多件事,所以效率会大幅提升。
2、如何创建一个多线程
使用 threading 模块下的 Thread 即可创建线程,关于 threading 的介绍,在并发编程中已总结:
https://blog.csdn.net/qq_35092730/article/details/113840593
3、单线程与多线程运行速度对比
1、单线程运行:
import time
def reader():
for i in range(3):
print(f'{i}号在阅读...')
time.sleep(2)
def run():
for i in range(3):
print(f'{i}号在跑步...')
time.sleep(2)
def single_thread():
reader()
run()
if __name__ == '__main__':
start_time = time.time()
single_thread()
end_time = time.time()
time_difference = end_time - start_time
print(time_difference) # 12.033815860748291
2、多线程运行:
from threading import Thread
import time
def reader():
for i in range(3):
print(f'{i}号在阅读...')
time.sleep(2)
def run():
for i in range(3):
print(f'{i}号在跑步...')
time.sleep(2)
def multi_thread():
t1 = Thread(target=reader)
t2 = Thread(target=run)
t1.start()
t2.start()
t1.join()
t2.join()
if __name__ == '__main__':
start_time = time.time()
multi_thread()
end_time = time.time()
time_difference = end_time - start_time
print(time_difference) # 6.0269551277160645
结论:很明显,多线程完成三次阅读和三次跑步的时间比单线程快了一倍。
二、查看当前线程
1、threading.current_thread():在线程中执行此函数,会返回当前线程对象。
import threading
def run():
thread_infos = threading.current_thread()
print(thread_infos)
print(f'{thread_infos.name} is running')
def multi_thread():
t = threading.Thread(target=run, name='1号线程')
t.start()
if __name__ == '__main__':
multi_thread()
2、threading.enumerate():获取整个程序中的所有线程
import threading
def run(name):
print(f'{name} is running')
def multi_thread():
t = threading.Thread(target=run, name='1号线程', args=('1号线程',))
t.start()
print(threading.enumerate())
if __name__ == '__main__':
multi_thread()
三、多线程共享全局变量
1、多线程同时修改全局变量:
import threading
number = 0
def run():
global number
for i in range(10000000):
number += 1
print('number=',number)
def main():
for i in range(2):
t = threading.Thread(target=run)
t.start()
if __name__ == '__main__':
main()
运行结果:
number= 11048901
number= 11339368
分析:两个线程同时去修改全局变量 number 时,它们是并行的,有可能在同一时间进行修改,造成了数据不安全。就比如线程1和线程2在同一时间拿到了 number 的值,它们同时对 number 修改,造成了最终结果的错误。
2、解决办法(加锁):
import threading
number = 0
gLock = threading.Lock()
def run():
global number
gLock.acquire() # 锁住线程
for i in range(10000000):
number += 1
gLock.release() # 释放线程
print('number =', number)
def main():
for i in range(2):
t = threading.Thread(target=run)
t.start()
if __name__ == '__main__':
main()
知识点:
- 为了解决共享全局变量的问题,threading 提供了一个 Lock 类,这个类可以在某个线程访问某个变量时进行加锁,其他线程必须等待加锁的线程执行完毕后,释放了锁,才能进来执行。
- 使用锁的原则:把尽量少的且不耗时的代码放入锁中执行;代码执行完毕后要释放锁。
四、生产者和消费者模式
1、什么是生产者消费者模式
生产者和消费者模式是多线程开发中一种常见的模式,生产者的线程专门用来生产一些数据,然后存放到一个中间变量中,消费者再从这个变量中获取数据进行消费,通过生产和消费的平衡,可以达到代码高内聚低耦合的效果,程序分工明确,线程更加方便的管理。
2、模拟生产者生产10次包子,每次随机生产x个,消费者每次随机消费y个(Lock版本):
import threading
import random
import time
gRes = 0
gLock = threading.Lock()
gTimes = 0
class Producer(threading.Thread):
def run(self) -> None:
global gRes
global gTimes
while True:
gLock.acquire()
if gTimes >= 10:
gLock.release()
break
res = random.randint(1, 100)
gRes += res
gTimes += 1
print(f'{threading.current_thread().name}生产了{res}个包子')
gLock.release()
time.sleep(2)
class Consumer(threading.Thread):
def run(self) -> None:
global gRes
while True:
gLock.acquire()
res = random.randint(1, 100)
if gRes >= res:
gRes -= res
print(f'{threading.current_thread().name}消费了{res}个包子')
else:
if gTimes >= 10:
gLock.release()
break
print(f'{threading.current_thread().name}需要消费{res}个包子,但是目前只生产了{gRes}个包子')
gLock.release()
time.sleep(2)
def main():
for i in range(5):
producer = Producer(name=f'生产者{i}号')
producer.start()
for j in range(5):
consumer = Consumer(name=f'消费者{j}号')
consumer.start()
if __name__ == '__main__':
main()
分析:此版本的生产者和消费者模式可以正常运行,但是存在一个不足之处,在消费者中,总是通过 while True 死循环的方式去判断包子够不够,如果当前线程判断包子不够消费了,线程还要继续执行到锁的释放,相当于当前线程白跑了一趟,这么做很消耗CPU的资源,为了解决这个问题,引入 threading.Condition 类,可以解决以上资源消耗的问题。
3、模拟生产者生产10次包子,每次随机生产x个,消费者每次随机消费y个(Condition版本):
import threading
import random
import time
gRes = 0
gCondition = threading.Condition()
gTimes = 0
class Producer(threading.Thread):
def run(self) -> None:
global gRes
global gTimes
while True:
gCondition.acquire()
if gTimes >= 10:
gCondition.release()
break
res = random.randint(1, 100)
gRes += res
gTimes += 1
print(f'{threading.current_thread().name}生产了{res}个包子')
gCondition.notify_all() # 生产者生产出了新的包子,唤醒前面所有等待消费的线程来消费。
gCondition.release()
time.sleep(2)
class Consumer(threading.Thread):
def run(self) -> None:
global gRes
while True:
gCondition.acquire()
res = random.randint(1, 100)
while gRes < res:
if gTimes >= 10:
print(f'{threading.current_thread().name}需要消费{res}个包子,但是目前只生产了{gRes}个包子,生产者已经不再生产了!')
gCondition.release()
return
print(f'{threading.current_thread().name}需要消费{res}个包子,但是目前只生产了{gRes}个包子,消费失败!')
gCondition.wait() # 没有消费成功,不解锁,直接等待下次消费即可,这样做可以节省 cpu 的资源。
gRes -= res
print(f'{threading.current_thread().name}消费了{res}个包子')
gCondition.release()
time.sleep(2)
def main():
for i in range(5):
producer = Producer(name=f'生产者{i}号')
producer.start()
for j in range(5):
consumer = Consumer(name=f'消费者{j}号')
consumer.start()
if __name__ == '__main__':
main()
知识点:
1、执行思路:每一个消费者线程启动,如果遇到包子不够的情况,通过使用 gCondition.wait() 的方式处于等待状态,而不是直接运行到解锁,再去重启线程。生产者则每一次生产新的包子,通过 gCondition.notify_all() 的方式唤醒所有等待中的线程来消费。
2、Condition 类中的方法介绍:
- acquire() : 给当前线程上锁
- release() : 给当前线程解锁
- wait() : 将当前线程处于等待状态,并且会释放锁
- notify_all() :唤醒所有等待中的线程,并且不会释放锁
- notify() : 唤醒某个正在等待中的线程,默认唤醒第一个
4、两种生产者消费者模式对比:
五、线程安全队列 Queue
前面运用加锁的方式使线程共享的资源(全局变量)变的安全,但是会造成程序由原来并行变成了串行(因为每一个线程必须等待前一个线程把锁释放了才能进入),并且效率低(共享数据基于文件,而文件是硬盘上的数据),为了解决这个问题,Python 引入了队列,队列是线程安全的,它可以:
- 效率高(多个进程共享一块内存的数据)
- 帮我们处理好锁问题。
1、队列的方法
1、创建队列的类型:
q = queue.Queue(4) # 先进先出
q = queue.LifoQueue(4) # 先进后出
q = queue.PriorityQueue() # 优先级进出
2、常用方法:
import queue
q = queue.Queue(4)
for i in range(5):
try:
q.put(i, block=False) # 将 block 设置为 False 表示线程不会阻塞
except:
break
print(q.full()) # 判断线程队列是否处于满的状态
print(f'目前容量:{q.qsize()}') # 判断线程目前容量
for i in range(5):
try:
print(f'第{i}个取出值:{q.get(block=False)}')
except:
break
print(q.empty()) # 判断线程队列是否处于空的状态
3、并发实例:
from queue import Queue
from threading import Thread
import random
import time
def add_value(q):
while True:
q.put(random.randint(1,10))
time.sleep(1)
def get_value(q):
while True:
print(f'获取到的数据:{q.get()}')
def main():
q = Queue(10)
t1 = Thread(target=add_value, args=[q])
t2 = Thread(target=get_value, args=[q])
t1.start()
t2.start()
if __name__ == '__main__':
main()