这个是CSDN一个课程的学习笔记,名称为150讲轻松搞定Python网络爬虫。
使用
threading
模块下的Thread
类可创建一个线程。其有一个target
参数,需要指定一个函数,以后这个线程执行的时候,就会执行这个函数的代码。
首先通过下面的代码比较一下单线程和多线程。
import time
import threading
def thread1():
for x in range(3):
print("线程1-%d"%x)
time.sleep(1)
def thread2():
for x in range(3):
print("线程2-%d"%x)
time.sleep(1)
def single_thread():
thread1()
thread2()
def multi_thread():
th1 = threading.Thread(target=thread1)
th2 = threading.Thread(target=thread2)
th1.start()
th2.start()
if __name__ == '__main__':
single_thread()
multi_thread()
执行程序可以看出,定义了两个线程之后,这两个线程几乎同时运行,可以节省一半的时间。
threading.current_thread
:返回当前线程的对象。threading.enumerate
:获取整个程序中所有的线程。
import time
import threading
def thread1():
the_thread = threading.current_thread()
print(the_thread.name)
for x in range(3):
# print("线程1-%d"%x)
time.sleep(1)
def thread2():
the_thread = threading.current_thread()
print(the_thread.name)
for x in range(3):
# print("线程2-%d"%x)
time.sleep(1)
def multi_thread():
th1 = threading.Thread(target=thread1,name='线程1')
th2 = threading.Thread(target=thread2,name='线程2')
th1.start()
th2.start()
print("="*30)
print(threading.enumerate())
if __name__ == '__main__':
multi_thread()
执行结果:
使用类可以更加方便的管理我们的代码,可以让我们使用面向对象的方式进行编程。
- 我们自己定义的类必须继承自
threading.Thread
类。- 线程代码需要放在run方法中执行。
import time
import threading
class thread1(threading.Thread):
def run(self):
the_thread = threading.current_thread()
for x in range(3):
print("线程1-%d"%x)
time.sleep(1)
class thread2(threading.Thread):
def run(self):
the_thread = threading.current_thread()
for x in range(3):
print("线程2-%d"%x)
time.sleep(1)
def multi_thread():
th1 = thread1()
th2 = thread2()
th1.start()
th2.start()
if __name__ == '__main__':
multi_thread()
看下面的代码:
import threading
value = 0
def add_value():
global value
for x in range(100):
value += 1
print('value的值为:',value)
def thread():
for i in range(2):
th = threading.Thread(target=add_value)
th.start()
if __name__ == '__main__':
thread()
# 运行结果
# value的值为: 100
# value的值为: 200
我们设置100次循环,貌似运行结果没有什么异常。那么将循环改成1000000次呢?
import threading
value = 0
def add_value():
global value
for x in range(1000000):
value += 1
print('value的值为:',value)
def thread():
for i in range(2):
th = threading.Thread(target=add_value)
th.start()
if __name__ == '__main__':
thread()
# 运行结果
# value的值为: 1428494
# value的值为: 1748155
由于多线程之间是同时进行的,第一个值超过1000000很容易理解,但是为什么第二个值达不到2000000呢?原来当两个线程同时达到加1操作时,它们并不会互相“谦让”,让对方先行,而是同时对同一个value加1,这样就相当于“浪费”了一次加一的机会,导致最终结果偏小。那么有什么方法能解决这个问题呢
threading.Lock
类提供了解决办法,这个类可以在某个线程访问某个变量时加锁,防止此时其他线程再进来,直至当前线程处理结束后,才释放锁,其他线程才能进来处理。
在多线程中,如果需要修改全局变量,那么需要在修改全局变量的地方使用锁锁起来,执行完成后再把锁释放掉。
使用Lock的原则:
- 把尽量少的和不耗时的代码放到锁中执行。
- 代码执行完成后要记得释放锁。(非常重要)
在Python中,可以使用threading.Lock
来创建锁,lock.acquire()
是上锁操作,lock.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的值为:',value)
def thread():
for i in range(2):
th = threading.Thread(target=add_value)
th.start()
if __name__ == '__main__':
thread()
# 运行结果
# value的值为: 10000000
# value的值为: 20000000
生产者和消费者模式是多线程开发中经常见到的一种模式。生产者的线程专门用来生产一些数 据,然后存放到一个中间的变量中。消费者再从这个中间的变量中取出数据进行消费。通过生产 者和消费者模式,可以让代码达到高内聚低耦合的目标,程序分工更加明确,线程更加方便管理。
import threading
import random
import time
gMoney = 0
gLock = threading.Lock()
gTimes = 0
class Producer(threading.Thread):
def run(self):
global gMoney
global gTimes
while True:
gLock.acquire()
if gTimes >= 10:
gLock.release()
break
money = random.randint(0, 100)
gMoney += money
gTimes += 1
print("%s生产了%d元钱"%(threading.current_thread().name,money))
time.sleep(1)
gLock.release()
class Consumer(threading.Thread):
def run(self) -> None:
global gMoney
while True:
gLock.acquire()
money = random.randint(0,100)
if gMoney >= money:
gMoney -= money
print("%s消费了%d元钱"%(threading.current_thread().name,money))
else:
if gTimes >= 10:
gLock.release()
break
print("%s想消费%d元钱,但是余额只有%d"%(threading.current_thread().name,money,gMoney))
time.sleep(1)
gLock.release()
def main():
for x in range(5):
th = Producer(name="生产者%d号"%x)
th.start()
for x in range(5):
th = Consumer(name="消费者%d号"%x)
th.start()
if __name__ == '__main__':
main()
# 运行结果
# 生产者0号生产了80元钱
# 生产者1号生产了45元钱
# 生产者1号生产了40元钱
# 生产者1号生产了10元钱
# 生产者1号生产了61元钱
# 生产者1号生产了93元钱
# 生产者1号生产了66元钱
# 生产者1号生产了44元钱
# 生产者1号生产了8元钱
# 生产者1号生产了75元钱
# 消费者0号消费了8元钱
# 消费者0号消费了81元钱
# 消费者2号消费了96元钱
# 消费者2号消费了27元钱
# 消费者2号消费了19元钱
# 消费者2号消费了30元钱
# 消费者2号消费了61元钱
# 消费者2号消费了81元钱
# 消费者2号消费了62元钱
# 消费者2号消费了45元钱
# 消费者0号消费了3元钱
Lock版本的生产者与消费者模式可以正常的运行。但是存在一个不足,在消费者中,总是通过while True死循环并且上锁的方式去判断钱够不够。上锁是一个很耗费CPU资源的行为。因此这种方式不是最好的。还有一种更好的方式便是使用threading.Condition
来实现。threading.Condition
可以在没有数据的时候处于阻塞等待状态。一旦有合适的数据了,还可以使用notify相关的函数来通知其他处于等待状态的线程。这样就可以不用做一些无用的上锁和解锁的操作。可以提高程序的性能。首先对threading.Condition
相关的函数做个介绍,threading.Condition
类似threading.Lock
,可以在修改全局数据的时候进行上锁,也可以在修改完毕后进行解锁。以下将一些常用的函数做个简单的介绍:
import threading
import random
import time
gMoney = 0
gCondition = threading.Condition()
gTimes = 0
class Producer(threading.Thread):
def run(self):
global gMoney
global gTimes
while True:
gCondition.acquire()
if gTimes >= 10:
gCondition.release()
break
money = random.randint(0, 100)
gMoney += money
gTimes += 1
print("%s生产了%d元钱,剩余%d元钱"%(threading.current_thread().name,money,gMoney))
gCondition.notify_all()
gCondition.release()
time.sleep(1)
class Consumer(threading.Thread):
def run(self) -> None:
global gMoney
while True:
gCondition.acquire()
money = random.randint(0,100)
while gMoney < money:
if gTimes >= 10:
print("%s想消费%d元钱,但是余额只有%d元钱了,并且生产者已经不再生产了!"%(threading.current_thread().name,money,gMoney))
gCondition.release()
return
print("%s想消费%d元钱,但是余额只有%d元钱了,消费失败!"%(threading.current_thread().name,money,gMoney))
gCondition.wait()
gMoney -= money
print("%s消费了%d元钱,剩余%d元钱"%(threading.current_thread().name,money,gMoney))
gCondition.release()
time.sleep(1)
def main():
for x in range(5):
th = Producer(name="生产者%d号"%x)
th.start()
for x in range(5):
th = Consumer(name="消费者%d号"%x)
th.start()
if __name__ == '__main__':
main()
# 运行结果
# 生产者0号生产了92元钱,剩余92元钱
# 生产者1号生产了97元钱,剩余189元钱
# 生产者2号生产了38元钱,剩余227元钱
# 生产者3号生产了18元钱,剩余245元钱
# 生产者4号生产了94元钱,剩余339元钱
# 消费者0号消费了18元钱,剩余321元钱
# 消费者1号消费了9元钱,剩余312元钱
# 消费者2号消费了58元钱,剩余254元钱
# 消费者3号消费了83元钱,剩余171元钱
# 消费者4号消费了41元钱,剩余130元钱
# 生产者2号生产了49元钱,剩余179元钱
# 生产者1号生产了6元钱,剩余185元钱
# 生产者3号生产了22元钱,剩余207元钱
# 生产者0号生产了99元钱,剩余306元钱
# 消费者1号消费了22元钱,剩余284元钱
# 生产者4号生产了96元钱,剩余380元钱
# 消费者2号消费了86元钱,剩余294元钱
# 消费者0号消费了8元钱,剩余286元钱
# 消费者3号消费了30元钱,剩余256元钱
# 消费者4号消费了33元钱,剩余223元钱
# 消费者2号消费了91元钱,剩余132元钱
# 消费者0号消费了72元钱,剩余60元钱
# 消费者1号想消费68元钱,但是余额只有60元钱了,并且生产者已经不再生产了!
# 消费者3号想消费93元钱,但是余额只有60元钱了,并且生产者已经不再生产了!
# 消费者4号想消费71元钱,但是余额只有60元钱了,并且生产者已经不再生产了!
# 消费者2号消费了31元钱,剩余29元钱
# 消费者0号消费了18元钱,剩余11元钱
# 消费者0号想消费19元钱,但是余额只有11元钱了,并且生产者已经不再生产了!
# 消费者2号想消费91元钱,但是余额只有11元钱了,并且生产者已经不再生产了!
在线程中,访问一些全局变量,加锁是一个经常的过程。如果你是想把一些数据存储到某个队列中,那么Python内置了一个线程安全的模块叫做queue模块。Python中的queue模块中提供了同步的、线程安全的队列类,包括FIFO(先进先出)队列Queue,LIFO(后入先出)队列LifoQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么都做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。相关的函数如下:
初始化Queue(maxsize):创建一个先进先出的队列。
block=False
,来关掉阻塞。如果关掉了阻塞,在队列为空的情况获取就会抛出异常。from queue import Queue
q = Queue(4)
for i in range(4):
q.put(i)
print(q.qsize())
print('完成')
# 运行结果
# 4
# 完成
from queue import Queue
q = Queue(4)
for i in range(5):
q.put(i)
print(q.qsize())
print('完成')
# 如果将range(4)改成range(5),由于q中最多只能存放4个元素,所以会被阻塞在循环内,不会执行下面的代码
from queue import Queue
q = Queue(4)
for i in range(5):
q.put(i,block=False) # block=False代表非阻塞,阻塞时会报错
print(q.qsize())
print('完成')
# 运行结果
'''
Traceback (most recent call last):
File "c:/Users/woniu/Desktop/python/CSDN学院/爬虫/.vscode/03.py", line 6, in
q.put(i,block=False) # block=False代表非阻塞,阻塞时会报错
File "D:\ANACANDA\lib\queue.py", line 136, in put
raise Full
queue.Full
'''
from queue import Queue
q = Queue(4)
for i in range(5):
try:
q.put(i,block=False) # 可以利用这个报错来进行异常处理
except:
break
print(q.qsize())
print('完成')
# 运行结果
# 4
# 完成
from queue import Queue
q = Queue(4)
for i in range(5):
try:
q.put(i,block=False)
except:
break
# print(q.qsize())
for j in range(4):
value = q.get() # get获取数据
print(value)
print('完成')
# 运行结果
# 0
# 1
# 2
# 3
# 完成
from queue import Queue
q = Queue(4)
for i in range(5):
try:
q.put(i,block=False) # 可以利用这个报错来进行异常处理
except:
break
# print(q.qsize())
for j in range(5):
try:
# get和put类似,在queue为空时也会阻塞,可以利用block=False报错,进行异常处理
value = q.get(block=False)
except:
break
print(value)
print('完成')
# 运行结果
# 0
# 1
# 2
# 3
# 完成
from queue import Queue
q = Queue(4)
for i in range(5):
try:
q.put(i,block=False)
except:
break
print(q.full())# 判断队列满
for j in range(5):
try:
value = q.get(block=False)
except:
break
print(value)
print(q.empty())# 判断队列空
print('完成')
# True
# 运行结果
# 0
# 1
# 2
# 3
# True
# 完成