1、多线程的概念
默认情况下,一个程序只有一个进程和一个线程,代码是一次线性执行的。而多线程则可以并发执行,效率要高。
1、使用threading.current_thread()可以看到当前线程的信息
2、使用threading.enumerate()函数可以看到当前的线程
代码范例:
import time
import threading
# 使用threading.current_thread()可以查看当前线程信息
# 使用threading.enumerate()可以看到当前线程
def coding():
the_thread = threading.current_thread()
for i in range(3):
print('%s正在写第%d行代码'%(the_thread.name, i+1))
time.sleep(1)
def drawing():
the_thread = threading.current_thread()
for i in range(3):
print('%s正在画画'%(the_thread.name))
time.sleep(1)
def multi_thread():
# 创建线程
th1 = threading.Thread(target=coding, name = '小明')
th2 = threading.Thread(target=drawing, name = '小刚')
# 线程开始
th1.start()
th2.start()
print(threading.enumerate())
if __name__ == '__main__':
multi_thread()
运行结果
为了让线程代码更好的封装,可以使用threading模块的Thread类,重写run方法,线程要执行的操作放在run函数体中,创建出线程对象
代码样例:
import time
import threading
from threading import Thread
# 创建写代码的类,做继承,重写run方法
class CodingThread(Thread):
def run(self):
thread1 = threading.current_thread()
for i in range(3):
print('%s正在写第%s行代码'%(thread1.name, i+1))
time.sleep(1)
class DrawThread(Thread):
def run(self):
thread2 = threading.current_thread()
for i in range(3):
print('%s正在画第%s幅画'%(thread2.name, i+1))
time.sleep(1)
def multi_thread():
# 创建线程方法
t1 = CodingThread()
t2 = DrawThread()
# 调用start()方法,开始线程
t1.start()
t2.start()
if __name__ == '__main__':
multi_thread()
运行结果:
2、多线程共享全局变量的问题
多线程都是在同一个进程里面运行的,因此,进程中的全局变量所有线程皆可以共享。那么会有一个问题,线程执行的顺序是无序的,可能会造成数据错误
会出错的情况是什么样子呢,下面给一个例子
准备一个函数,这个函数的作用是将变量的值不断增加,
通过另外一个函数对这一个进程创建两个 线程,同时对num变量进行操作,当range()的范围是100的时候,结果是和代码预期的一致,分别是100和200,但是当这个数字变大之后,比如到达一千万,结果就不一样了,第一个循环是超出了一千万的结果,第二个循环是不够两千万
import threading
num = 0
def add_global_var():
global num
for i in range(100):
num += 1
print('num的值是%s'%num)
def multi_thread():
for i in range(2):
thread = threading.Thread(target = add_global_var)
thread.start()
if __name__ == '__main__':
multi_thread()
导致这样的情况出现的原因是什么呢,由于多个线程的顺序是不一样的,而且执行的时间不同,那么两个线程在处理num的值的时候,时间可能会先后,也有可能会同时到达,这样num的值就只会改变一次。
如何解决呢?那就需要在线程里面加入锁的机制了
threading提供了一个Lock类,这个类可以在某个线程访问某个变量的时候加锁,其他的线程就进不来了,直到当前线程处理完成之后,将锁释放,下一个线程才能处理
使用锁的原则:
1、把尽量少的和不耗时的代码放到锁中执行
2、代码执行完成之后要记得释放锁
加入之后的效果如下:
import threading
num = 0
gLock = threading.Lock()
def sub_global_var():
global num
# 在修改全局变量的操作上,加入锁的机制
# 用完之后记得释放锁
gLock.acquire()
for i in range(10000000):
num -= 1
gLock.release()
print('num的值是:', num)
def multi_thread():
for i in range(3):
t = threading.Thread(target = sub_global_var)
t.start()
if __name__ == '__main__':
multi_thread()
刚才的问题就得到了解决
3、生产者和消费者模型
生产者和消费者模型是多线程开发中经常见到的一种模式。生产者的线程专门用来生产一些数据,将数据保存到一个中间变量中。消费者再从中间变量中取出数据进行消费。通过生产者和消费者模式,可以让代码实现高内聚低耦合的目标,分工更加明确,线程方便管理
举个例子,生产者负责生产苹果,消费者负责吃苹果,全局变量用来存储苹果的数量
代码如下
import threading, random, time
from threading import Thread
# 面向对象
gApple = 0
# 创建锁
gLock = threading.Lock()
# 创建生产者
class Producer(Thread):
def run(self) -> None:
global gApple
while True:
# 上锁
gLock.acquire()
apple = random.randint(1, 100)
gApple += apple
print('%s生产了%s个苹果,目前一共有%s个苹果'%(threading.current_thread().name, apple, gApple))
# 释放锁
gLock.release()
time.sleep(1)
# 创建消费者
class Consumer(Thread):
def run(self) -> None:
global gApple
while True:
# 上锁
gLock.acquire()
apple = random.randint(1, 100)
if gApple >= apple:
gApple -= apple
print('%s吃掉了%s个苹果,还剩下%s个苹果' % (threading.current_thread().name, apple , gApple))
else:
print('%s需要吃掉的苹果数量是%s,但是库存只有%s个了,消费失败'%(threading.current_thread().name, apple, gApple))
# 释放锁
gLock.release()
time.sleep(1)
def multi_thread_ProducerAndConsumer():
for i in range(5):
# 创建生产者和消费者对象
p = Producer()
# 开始线程运行
p.start()
for i in range(5):
c = Consumer()
c.start()
if __name__ == '__main__':
multi_thread_ProducerAndConsumer()
截取部分效果图:
上面的代码虽然实现了生产者和消费者模型,但是在上述条件中,程序并不能结束,一般情况是要有一个结束条件的,那么在这我们加入一些条件,让程序主动结束
import threading, random, time
from threading import Thread
# 面向对象
gApple = 0
# 创建锁
gLock = threading.Lock()
# 加入次数限制
gT = 0
# 创建生产者
class Producer(Thread):
def run(self) -> None:
global gApple, gT
while True:
# 上锁
gLock.acquire()
if gT >= 20:
gLock.release()
break
apple = random.randint(1, 100)
gApple += apple
gT += 1
print('%s生产了%s个苹果,目前一共有%s个苹果'%(threading.current_thread().name, apple, gApple))
# 释放锁
gLock.release()
time.sleep(1)
# 创建消费者
class Consumer(Thread):
def run(self) -> None:
global gApple
while True:
# 上锁
gLock.acquire()
apple = random.randint(1, 100)
if gApple >= apple:
gApple -= apple
print('%s吃掉了%s个苹果,还剩下%s个苹果' % (threading.current_thread().name, apple , gApple))
else:
if gT >= 20:
gLock.release()
break
print('%s需要吃掉的苹果数量是%s,但是库存只有%s个了,消费失败'%(threading.current_thread().name, apple, gApple))
# 释放锁
gLock.release()
time.sleep(1)
def multi_thread_ProducerAndConsumer():
for i in range(5):
# 创建生产者和消费者对象
p = Producer(name = '生产者%d号'%(i+1))
# 开始线程运行
p.start()
for i in range(5):
c = Consumer(name = '消费者%d号'%(i+1))
c.start()
if __name__ == '__main__':
multi_thread_ProducerAndConsumer()
4、Condition版本的生产者消费者模型
Lock版本的生产者和消费者模式可以正常运行,但是存在一个不足,在消费者中,总是通过while循环并上锁的方式去判断条件是否正确。上锁也是一个很消耗CPU资源的行为,因此这种方式不是最好的,还有一种可以通过threading.Condition来实现。
Condition的具体函数
acquire | 上锁 |
release | 解锁 |
wait | 将当前线程处于等待状态,并且会释放锁。可以被其他线程使用notify和notify_all函数唤醒。被唤醒后会继续等待上锁,上锁后继续执行下面的代码 |
notify | 通知某个正在等待的线程,默认是第1个等待的线程 |
notify_all | 通知所有正在等待的线程。notify和notify_all不会释放锁。并且需要在release之前调用 |
在用Condition进行代码补充过程中,需要注意这几点。首先,在生产者类中规定了生产的次数,在进行生产的过程中,消费者消费的苹果数量可能会大于现有的苹果数量,那么此时消费者中的线程需要等待,等什么时候条件满足了,线程才会继续。因此,生产者中在生产了苹果之后,需要加入notify_all方法去唤醒等待的线程。
其次,在消费者消费的时候,要消费的苹果数量会有可能大于库存的苹果数量。因此,在这里需要一个条件的考虑。如果库存的数量小于要消费的数量时,需要做一个消费失败的提示,并让该线程等待,如果在该条件下,生产的次数已经达上限了,那也需要做具体相应的提示。之后满足库存苹果数量大于消费数量时,才能进行减法操作
import threading, random, time
from threading import Thread
# 面向对象
gApple = 0
# 创建锁
gCon = threading.Condition()
# 加入次数限制
gT = 0
# 创建生产者
class Producer(Thread):
def run(self) -> None:
global gApple, gT
while True:
# 上锁
gCon.acquire()
if gT >= 20:
gCon.release()
break
apple = random.randint(1, 100)
gApple += apple
gT += 1
print('%s生产了%s个苹果,目前一共有%s个苹果'%(threading.current_thread().name, apple, gApple))
# 唤醒等待线程
gCon.notify_all()
# 释放锁
gCon.release()
time.sleep(1)
# 创建消费者
class Consumer(Thread):
def run(self) -> None:
global gApple
while True:
# 上锁
gCon.acquire()
apple = random.randint(1, 100)
while gApple < apple:
if gT >= 20: # 如果生产者已经不生产,那么直接return返回
print('%s想吃%s个苹果,但是只有%s个了,而且生产者不再生产'%(threading.current_thread().name, apple, gApple))
return
print('%s想吃%s苹果,但是只有%s个苹果了,失败!'%(threading.current_thread().name, apple, gApple))
gCon.wait()
gApple -= apple
# 吃掉之后,打印信息
print('%s吃了%s苹果,还有%s个苹果'%(threading.current_thread().name, apple, gApple))
# 释放锁
gCon.release()
time.sleep(1)
def multi_thread_ProducerAndConsumer():
for i in range(5):
# 创建生产者和消费者对象
p = Producer(name = '生产者%d号'%(i+1))
# 开始线程运行
p.start()
for i in range(5):
c = Consumer(name = '消费者%d号'%(i+1))
c.start()
if __name__ == '__main__':
multi_thread_ProducerAndConsumer()
运行结果
5、线程安全队列——Queue
在线程中,访问一些全局变量,加锁是一个经常的过程。如果要把一些数据存储到某个队列中,那python内置的线程安全模块:queue就能满足我们的需求。这个模块中提供了同步的、线程安全的队列类,包括FIFO(先进先出)队列,LIFO(后入先出)队列。这些队列都实现了锁原语(可以理解为原子操作,要么不做,要么就都做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。
相关函数如下:
Queue(maxsize) | 创建一个先进先出的队列 |
qsize() | 返回队列的大小 |
empty() | 判断队列是否为空 |
full() | 判断队列是否满了 |
get() | 从队列中取最后一个数据。默认情况下是阻塞的。也就是说如果队列已经空了,那么再调用就会一直阻塞,直到有新的数据添加进来。也可以使用'block = False',来关掉阻塞。如果关掉阻塞,在队列为空的情况下获取,就会抛出异常 |
put() | 将一个数据放到队列中。如果在队列满的情况下,再调用put()方法,也会阻塞,同样可以使用'block = False'关闭阻塞,来抛出异常 |
先了解相关方法:
import queue
from queue import Queue
# 创建队列并指定队列中的元素个数
q = Queue(5)
# put() 放置数据
for i in range(6):
'''
如果向队列中加入的数据,要多于创建队列时指定的元素个数
调用put()方法时会报错,我们可以捕获这个异常,出现异常
的时候就break,那么程序是可以正常放置数据的,只不过最
多放置5个,在放置第6个的时候就捕捉到异常,直接break了
block参数为False用于抛出异常
'''
try:
q.put(i, block=False)
except:
break
print('队列大小', q.qsize())
# full() 判断队列是否为满
res = q.full()
print('队列是否满', res)
# get() 获取队列中的数据
for i in range(5):
try:
value = q.get(block=False)
except:
break
print('队列中取数据', value)
# empty() 判断是否为空
# 上述代码已经取出过元素了,所以此时队列为空
res = q.empty()
print('判断队列是否为空', res)
结合Queue实现多线程
def add_num(q):
'''
:param q: 创建的队列
:return:
'''
while True:
n = random.randint(1, 100)
q.put(n)
print('向队列添加数字:', n)
time.sleep(1)
def get_num(q):
while True:
print('从队列中取数字:', q.get())
def thread_queue():
q = Queue(4)
t1 = threading.Thread(target = add_num, args = [q])
t2 = threading.Thread(target = get_num, args = [q])
t1.start()
t2.start()
if __name__ == '__main__':
thread_queue()
这样的话就不需要上锁了,会依次执行