如果我们把CPU比作一个工厂,这个工厂里面有多个车间,每一个车间就是一个进程;每一个车间有多个工人,每一个工人就是一个线程。我们之所以要学习多线程就是为了在同一时间里完成多项任务。
python中的多线程使用的是threading模块。
我们来看下面的一个例子,
import time
def drinking():
for x in range(3):
print("正在喝啤酒:%s" % x)
time.sleep(1)
def eating():
for x in range(3):
print("正在吃小龙虾:%s" % x)
time.sleep(1)
def main():
drinking()
eating()
if __name__ == '__main__':
main()
执行结果如下,
正在喝啤酒:0
正在喝啤酒:1
正在喝啤酒:2
正在吃小龙虾:0
正在吃小龙虾:1
正在吃小龙虾:2
我们可以发现系统会在drinking()执行完毕后再执行eating(),如果我们要同时执行drinking()和eating()就需要使用多线程的方式进行了。例如,
import threading
import time
def drinking():
for x in range(3):
print("正在喝啤酒:%s" % x)
time.sleep(1)
def eating():
for x in range(3):
print("正在吃小龙虾:%s" % x)
time.sleep(1)
def main():
t1 = threading.Thread(target=drinking) # 创建线程并且指定目标
t2 = threading.Thread(target=eating)
t1.start() # 开启线程
t2.start()
if __name__ == '__main__':
main()
执行结果如下,
正在喝啤酒:0
正在吃小龙虾:0
正在吃小龙虾:1
正在喝啤酒:1
正在喝啤酒:2
正在吃小龙虾:2
在上例中,我们创建了两个Thread对象,Thread对象执行target参数中的方法为执行方法,分别用于执行drinking()和eating(),使用start()方法让线程开始任务。
我们可以看到drinking()和eating()同时执行。
我们可以自定义一个类继承于threading.Thread类。threading.Thread类中的run()方法是当创建Thread对象的时候,如果没有传入target参数,则会执行run()方法中的代码。因此,我们需要对run()方法进行重写。
我们使用自定义线程类的方式重写上例,
import threading
import time
class DrinkingThread(threading.Thread):
def run(self):
for x in range(3):
print("%s正在喝啤酒" % threading.current_thread())
# threading.current_thread()方法会显示当前线程的名称
time.sleep(1)
class EatingThread(threading.Thread):
def run(self):
for x in range(3):
print("%s正在撸串" % threading.current_thread())
time.sleep(1)
def main():
t1 = DrinkingThread()
t2 = EatingThread()
t1.start()
t2.start()
if __name__ == '__main__':
main()
执行结果同上例。
有时我们可能需要多个线程对同一个变量进行操作,这时会出现线程冲突的问题,例如,我们使用多个线程进行自增操作,
import threading
VALUE = 0
def add_value():
global VALUE
for x in range(1000000):
VALUE += 1
print("VALUE:{}".format(VALUE))
def main():
for x in range(2):
t = threading.Thread(target=add_value)
t.start()
if __name__ == '__main__':
main()
执行结果如下,
VALUE:923582
VALUE:1135998
我们需要的执行结果为1000000和2000000。但是输出结果为923582和1135998,这是由于线程同时操作VALUE变量导致结果出错。
要解决线程冲突问题,我们需要对线程上锁。
首先,创建一个Lock对象,在对共享变量操作的代码前使用acquire()方法上锁,在代码后使用release()方法解锁。例如,
import threading
VALUE = 0
gLock = threading.Lock()
def add_value():
global VALUE
gLock.acquire() # 上锁
for x in range(1000000):
VALUE += 1
gLock.release() # 释放锁
print("VALUE:{}".format(VALUE))
def main():
for x in range(2):
t = threading.Thread(target=add_value)
t.start()
if __name__ == '__main__':
main()
执行结果如下,
VALUE:1000000
VALUE:2000000
注意,上锁操作是非常占用CPU的。
生产者和消费者模式是我们使用爬虫的一种模式,我们使用生产者来生成url链接,使用消费者来下载。
我们这里根据操作共享变量的模式分为Lock版和Condition版。
例如,我们使用生产者给gMoney共享变量增加值,使用消费者给gMoney共享变量减少值。
import threading
import random
import time
gMoney = 666
gLock = threading.Lock()
gTotalTimes = 10
gTimes = 0
class Producer(threading.Thread):
def run(self):
global gMoney, gTotalTimes, gTimes
while True:
money = random.randint(666, 6666)
gLock.acquire() # 上锁
if gTimes >= gTotalTimes:
gLock.release()
break
gMoney += money
gTimes += 1
print("{}挣了{}元钱,余额{}元".format(threading.current_thread(), money, gMoney))
gLock.release()
time.sleep(0.5)
class Consumer(threading.Thread):
def run(self):
global gMoney, gTimes, gTotalTimes, gLock
while True:
money = random.randint(666, 6666)
gLock.acquire()
if gMoney >= money:
gMoney -= money
print("{}消费了{}元钱,余额{}元".format(threading.current_thread(), money, gMoney))
else:
if gTimes >= gTotalTimes:
gLock.release() # 如果不释放,容易卡死
break
print("{}准备消费{}元钱,余额{}元,余额不足".format(threading.current_thread(), money, gMoney))
gLock.release()
time.sleep(1)
def main():
# 五个生产者
for x in range(5):
t = Producer(name="生产者线程{}".format(x + 1))
t.start()
# 三个消费者
for x in range(3):
t = Consumer(name="消费者线程{}".format(x))
t.start()
if __name__ == '__main__':
main()
因为频繁上锁消耗CPU,因此,这种方式不是最优方式。
我们可以使用threading.Condition让线程在没有数据时,进入等待状态。
我们同样需要创建一个Condition对象,在操作共享变量的代码前使用acquire()方法上锁,在使用release()方法解锁前,需要使用notify()或者notify_all()方法通知第一个或这所有线程。
注意,notify()、notify_all()方法不会释放锁,必须在release()方法之前调用。
import threading
import random
import time
gMoney = 666
gCondition = threading.Condition()
gTotalTimes = 10
gTimes = 0
class Producer(threading.Thread):
def run(self):
global gMoney, gTotalTimes, gTimes, gCondition
while True:
money = random.randint(666, 6666)
gCondition.acquire() # 上锁
if gTimes >= gTotalTimes:
gCondition.release()
print("当前生产者总共生产了{}次".format(gTimes))
break
gMoney += money
gTimes += 1
print("{}挣了{}元钱,余额{}元".format(threading.current_thread(), money, gMoney))
gCondition.notify_all() # 通知所有等待的线程
gCondition.release() # 释放之前,通知正在等待的消费者线程
time.sleep(0.5)
class Consumer(threading.Thread):
def run(self):
global gMoney, gTimes, gTotalTimes, gCondition
while True:
money = random.randint(666, 6666)
gCondition.acquire()
while gMoney < money:
print("{}准备消费{}元钱,余额{}元,余额不足".format(threading.current_thread(), money, gMoney))
if gTimes >= gTotalTimes:
gCondition.release()
return
gCondition.wait()
gMoney -= money
print("{}准备消费{}元钱,余额{}元".format(threading.current_thread(), money, gMoney))
gCondition.release()
time.sleep(1)
def main():
# 五个生产者
for x in range(5):
t = Producer(name="生产者线程{}".format(x + 1))
t.start()
# 三个消费者
for x in range(3):
t = Consumer(name="消费者线程{}".format(x))
t.start()
if __name__ == '__main__':
main()
为了防止线程冲突,我们也可以使用队列的方式存取数据。在python中我们使用queue模块。
我们可以创建两种队列,
上面的两种队列都实现了锁,即要么不操作,要么操作完,我们可以直接拿来使用。
下面是队列的一些方法,