python爬虫之多线程介绍(二)
1 线程间资源的竞争
1.1 线程间资源竞争产生的原因
'''
线程间的资源竞争产生原因:
1)了解线程间的通信:多线程共享全局变量
2)多线程操作:在对一个线程的文件读写过程时,另一个线程就在执行
3)⼀个线程写⼊,⼀个线程读取,没问题,如果两个线程都写⼊,就会出现资源间的竞争
'''
# 1. 多线程共享全局变量
# 在demo1--> t1.start()开始之后设置time.sleep(10)保证demo1先执行完,再执行demo2 ,不会导致全局变量num的争夺
import threading
import time
num = 0
def demo1(nums):
global num
for i in range(nums): # 执行nums次
num += 1
print(num)
def demo2(nums):
for i in range(nums): # 执行nums次
num += 1
print(num)
def main():
print(num)
if __name__ == '__main__':
t1 = threading.Thread(target=demo1,args=(100000,))
t2 = threading.Thread(target=demo1, args=(100000,))
t1.start()
time.sleep(10) # 保证demo1先执行
t2.start()
time.sleep(3)
main()
# 结果为
# 100000
# 200000
# 300000
# 2.多线程操作:在对一个线程的文件读写过程时,另一个线程就在执行,但如果两个线程都写⼊,就会出现资源间的竞争
# 此处没有设置time.sleep()保证demo1先执行,这会导致demo1和demo2都在抢夺num这个全局变量,导致demo1、demo2轮换进行
import threading
import time
num = 0
def demo1(nums):
global num
for i in range(nums): # 执行nums次
num += 1
print(num)
def demo2(nums):
for i in range(nums): # 执行nums次
num += 1
print(num)
def main():
print(num)
if __name__ == '__main__':
t1 = threading.Thread(target=demo1,args=(1000000,))
t2 = threading.Thread(target=demo1, args=(1000000,))
t1.start()
t2.start()
time.sleep(3)
main()
# 结果为
# 126348
# 1178282
# 1294649
1.2 线程间资源竞争的解决
1.2.1 互斥锁
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制某个线程要更改共享数据时,先将其锁定,此时资源的状态为**“锁定”,其他线程不能改变**,只到该线程释放资源,将资源的状态变成"非锁定",其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
'''
解决线程间资源竞争 —— 将线程加一把锁,等他执行完,再执行另外一个线程
创建一个互斥锁
语法: lock1 = threading.Lock() # Lock 只能上一把锁
lock2 = threading.RLock() # 可以上多把锁,但是加锁和解锁的数量要一一对应
'''
import threading
import time
lock1 = threading.Lock()
lock2= threading.RLock()
num = 0
def demo1(nums):
global num
# 上锁
lock1.acquire()
for i in range(nums): # 执行nums次
num += 1
# 解锁
lock1.release()
print(num)
def demo2(nums):
# 上锁
lock2.acquire()
lock2.acquire()
for i in range(nums): # 执行nums次
num += 1
# 解锁
lock2.release()
lock2.release()
print(num)
def main():
print(num)
if __name__ == '__main__':
t1 = threading.Thread(target=demo1,args=(1000000,))
t2 = threading.Thread(target=demo1, args=(1000000,))
t1.start()
t2.start()
time.sleep(3)
main()
# 结果为
# 1000000
# 2000000
# 2000000
1.2.2 死锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
'''
死锁
'''
import threading
import time
class MyThread1(threading.Thread):
def run(self):
# 对mutexA上锁
mutexA.acquire()
# mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
print(self.name+'----do1---up----')
time.sleep(1)
# 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
mutexB.acquire()
print(self.name+'----do1---down----')
mutexB.release()
# 对mutexA解锁
mutexA.release()
class MyThread2(threading.Thread):
def run(self):
# 对mutexB上锁
mutexB.acquire()
# mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
print(self.name+'----do2---up----')
time.sleep(1)
# 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
mutexA.acquire()
print(self.name+'----do2---down----')
mutexA.release()
# 对mutexB解锁
mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
2 线程锁
前面我们讲了用互斥锁解决线程间资源的竞争问题,现在让我们进一步了解线程锁
2.1 Semaphore
详细解释见博客:多线程并发之Semaphore(信号量)使用详解
'''
线程锁之信号Semaphore:
这种锁允许一定数量的线程同时更改数据,它不是互斥锁。比如地铁安检,排队人很多,工作人员只允许一定数量的人进入安检区,其它的人继续排队。
'''
import time
import threading
n=0
def demo(num, se):
global n
se.acquire()
for i in range(num):
n += 1
print("run the thread: %s" % n)
time.sleep(1)
se.release()
# 设置允许5个线程同时运行
semaphore = threading.BoundedSemaphore(5)
for i in range(20):
t = threading.Thread(target=demo, args=(10000,semaphore))
t.start()
2.2 Condition
Condition称作条件锁,依然是通过acquire()/release()加锁解锁。
语法
con = threading.Condition()
con.acquire() 上锁
con.wait() 线程挂起
con.notify() 被挂起的线程被唤醒
con.release() 解锁
wait([timeout])方法将使线程进入Condition的等待池。等待通知(notify()方法),并释放锁。使用前线程必须已获得锁定,否则将抛出异常。
notify()方法将从等待池挑选一个线程并通知(notify()方法),收到通知的线程将自动调用acquire()尝试获得锁定(进入锁定池),其他线程仍然在等待池中。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。
'''
线程锁之条件Condition
'''
import threading
def demo1():
con.acquire() # 上锁
print('第1步-打印1')
con.notify() # 唤醒demo2的con.wait(),执行demo2
con.wait() # 进入等待池,等待唤醒
print('第3步-打印3')
con.notify() # 唤醒demo2的con.wait(),执行demo2
con.release() # 解锁
def demo2():
con.acquire() # 上锁
con.wait() # 进入等待池,所以先执行demo1
print('第2步-打印2')
con.notify() # 唤醒demo1的con.wait(),执行demo1
con.wait() # 进入等待池,等待唤醒
print('第4步-打印4')
con.release() # 解锁
if __name__ == '__main__':
con = threading.Condition()
t1 = threading.Thread(target=demo1)
t2 = threading.Thread(target=demo2)
t2.start()
t1.start()
2.3 Event
全局定义了一个Flag,如果Flag的值为False,那么当程序执行wait()方法时就会阻塞,如果Flag值为True,线程不再阻塞。这种锁,类似交通红绿灯(默认是红灯),它属于在红灯的时候一次性阻挡所有线程,在绿灯的时候,一次性放行所有排队中的线程。
事件主要提供了四个方法set()、wait()、clear()和is_set()。
调用clear()方法会将事件的Flag设置为False。
调用set()方法会将Flag设置为True。
调用wait()方法将等待“红绿灯”信号。
is_set():判断当前是否"绿灯放行"状态
学习和参考:Python threading 多线程模块
'''
调用clear()方法会将事件的Flag设置为False。
调用set()方法会将事件的Flag设置为True。
调用wait()方法将等待“红绿灯”信号。
is_set():判断当前是否"绿灯放行"状态
'''
import threading
import time
event = threading.Event()
def lighter():
green_time = 5 # 绿灯时间
red_time = 5 # 红灯时间
event.set()
# 初始设为绿灯:
while True:
print('绿灯亮')
time.sleep(green_time)
event.clear() # 将事件的Flag设置为False
print('红灯亮')
time.sleep(red_time)
event.set() # 将事件的Flag设置为True
def run(name):
while True:
if event.is_set(): # 判断是否可以通行
print("一辆[%s] 呼啸开过..." % name)
time.sleep(1)
else:
print("一辆[%s]开来,看到红灯,无奈的停下了..." % name)
event.wait() # 将等待“红绿灯”信号
print("[%s] 看到绿灯亮了,瞬间飞起....." % name)
if __name__ == '__main__':
light = threading.Thread(target=lighter)
light.start()
for name in ['奔驰', '宝马', '奥迪']:
car = threading.Thread(target=run, args=(name,))
car.start()
2.4 with
with也相当于线程锁,语法如下:
with some_lock:
# 执行任务…
'''
with
'''
import threading
import time
lock = threading.Lock()
num = 0
def demo1(nums):
global num
with lock: # with 上锁
for i in range(nums): # 执行nums次
num += 1
print(num)
def demo2(nums): # with 上锁
with lock:
for i in range(nums): # 执行nums次
num += 1
print(num)
def main():
print(num)
if __name__ == '__main__':
t1 = threading.Thread(target=demo1,args=(1000000,))
t2 = threading.Thread(target=demo1, args=(1000000,))
t1.start()
t2.start()
time.sleep(3)
main()
3 Queue线程和线程池
3.1 Queue线程
在线程中,访问一些全局变量,加锁是一个经常的过程。如果你是想把一些数据存储到某个队列中,那么Python内置了-个线程安全的模块叫做queue模块。
Python中的queue模块中提供了同步的、线程安全的队列类,包括 FIFO (先进先出)队列Queue, LIFO (后入先出)队列ifoQueue。
这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么都做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。
1初始化Queue(maxsize): 创建一个先进先出的队列。
2 qsize():返回队列的大小。
3 empty():判断队列是否为空。
4 full():判断队列是否满了。
5 get():从队列中取最后一个数据。
6 put():将一个数据放到队列中。
'''
# Queue 就是对数据结构中的栈和队列这种数据结构的封装
# 初始化Queue(maxsize):创建一个先进先出的队列。
# qsize():返回队列的大小。
# empty():判断队列是否为空。
# full():判断队列是否满了。
# get():从队列中取最后一个数据。
# put():将一个数据放到队列中。
'''
from queue import Queue
q = Queue(3) # 队列内部设置最多3个
print(q.empty()) # True 队列是空的
print(q.full()) # False 队列没满
q.put(1) # 存入一个
q.put(2) # 存入第二个
q.put(3) # 存入第三个
print('----------------------------------')
print(q.empty()) # True 队列是空的 False
print(q.full()) # 队列满 True
# q.put(4,timeout=2) # 堵塞状态 2秒后报错,不设置timeout将一直处于堵塞状态
q.put_nowait(4) # queue.Full 等同于timeout
print(q.get()) # 取出最外面一个,即放入的第三个
print(q.get()) # 取出放入的第二个
print(q.get()) # 取出放入的第一个
print(q.get(timeout=2)) # queue.Empty 两秒后报错
print(q.get_nowait()) # 等同于timeout
3.2 线程池
定义:预先创建好一个数量较为优化的线程组,在需要的时候立刻能够使用,就形成了线程池。
线程池的整体构造需要自己精心设计,比如某个函数定义存在多少个线程,某个函数定义什么时候运行这个线程,某个函数定义去获取线程获取任务,某个线程设置线程守护(线程锁之类的)。
这里就需要应用到queue线程的知识,把线程类当做元素添加到队列内,从而实现线程池。
学习和参考:Python threading 多线程模块
'''
分析:
1.实例化一个MyThreadPool的对象,在其内部建立了一个最多包含5个元素的阻塞队列,并一次性将5个Thread类型添加进去。
2.循环20次,每次从pool中获取一个thread类,利用该类,传递参数,实例化线程对象。
3.在run()方法中,每当任务完成后,又为pool添加一个thread类,保持队列中始终有5个thread类。
4.一定要分清楚,代码里各个变量表示的内容。t表示的是一个线程类,也就是threading.Thread,而obj才是正真的线程对象。
'''
import queue
import time
import threading
class MyThreadPool:
def __init__(self, maxsize=5):
self.maxsize = maxsize
self._pool = queue.Queue(maxsize) # 使用queue队列,创建一个线程池
for _ in range(maxsize):
self._pool.put(threading.Thread)
def get_thread(self):
return self._pool.get()
def add_thread(self):
self._pool.put(threading.Thread)
def run(i, pool):
print('执行任务', i)
time.sleep(1)
pool.add_thread() # 执行完毕后,再向线程池中添加一个线程类
if __name__ == '__main__':
pool = MyThreadPool(5) # 设定线程池中最多只能有5个线程类
for i in range(20):
t = pool.get_thread() # 每个t都是一个线程类
obj = t(target=run, args=(i, pool)) # 这里的obj才是正真的线程对象
obj.start()
print("活动的子线程数: ", threading.active_count()-1)
4 线程同步
线程的同步就是A线程和B线程的交替对话,可以通过condition实现,这个模块只是为了巩固上面的知识
实现如下对话:
天猫精灵:小爱同学
小爱同学:在
天猫精灵:现在几点了?
小爱同学:你猜猜现在几点了
import threading
class XiaoAi(threading.Thread):
def __init__(self,cond):
# super() 可以用来获取当前类的父类
super().__init__(name='小爱')
self.cond = cond
def run(self):
# self.cond.acquire()
with self.cond: # 用with实现上锁和解锁
self.cond.wait()
print('{}: 在'.format(self.name))
self.cond.notify()
self.cond.wait()
print('{}: 你猜猜几点了'.format(self.name))
self.cond.notify()
# self.cond.release()
class TianMao(threading.Thread):
def __init__(self, cond):
# super() 可以用来获取当前类的父类
super().__init__(name='天猫')
self.cond = cond
def run(self):
self.cond.acquire() # 用cond.require 实现上锁
print('{}: 小爱同学'.format(self.name))
# print(1)
self.cond.notify()
# print(2)
self.cond.wait()
# print(3)
print('{}: 现在几点了?'.format(self.name))
self.cond.notify()
self.cond.release() # 用cond.require 实现解锁
if __name__ == '__main__':
# 定义一把条件锁
cond = threading.Condition()
xiaoai = XiaoAi(cond)
tianmao = TianMao(cond)
xiaoai.start()
tianmao.start()