1. 为什么要有锁
先看一个例子:
# Author:AD
# Date:2020/3/7
import threading, time
sum = 0 # 以下是对公共变量的修改
class MyThread(threading.Thread):
def __init__(self, arg):
threading.Thread.__init__(self)
self.arg = arg
def run(self):
start = time.time()
global sum
for i in range(self.arg): #(100000000):
sum += i
end = time.time()
print(self, sum, end-start)
t1 = MyThread(10000000)
t2 = MyThread(10000000)
t1.start()
t2.start()
print('主线程也在跑')
运行结果:发现结果混乱,是因为有可能在某一次执行的时候共同拿到了同一个全局变量
2. 加入Lock()
建立锁对象
mutex = threading.Lock()
加锁
mutex.acquire()
释放锁
mutex.release()
# Author:AD
# Date:2020/3/7
import threading, time
sum = 0 # 以下是对公共变量的修改
class MyThread(threading.Thread):
def __init__(self, arg):
threading.Thread.__init__(self)
self.arg = arg
def run(self):
start = time.time()
global sum
for i in range(self.arg): #(100000000):
# 在对全局变量修改的地方加锁即可
mutex.acquire()
sum += i
mutex.release()
end = time.time()
print(self, sum, end-start)
t1 = MyThread(10000000)
t2 = MyThread(10000000)
mutex = threading.Lock() # 同一把锁在同一时刻只能被一个线程获取,其他会在acquire处阻塞
t1.start()
t2.start()
print('主线程也在跑')
运行结果:
上述方式导致计算密集型式的加锁,内部一直在不停的加锁解锁,耗时十倍,还可以按照如下方式加锁:
'''高效率加锁方式'''
import threading, time
sum = 0 # 以下是对公共变量的修改
class MyThread(threading.Thread):
def __init__(self, arg):
threading.Thread.__init__(self)
self.arg = arg
def run(self):
start = time.time()
global sum
# 在对全局变量修改的地方加锁即可
mutex.acquire()
for i in range(self.arg): #(100000000)
sum += i
mutex.release()
end = time.time()
print(self, sum, end-start)
t1 = MyThread(10000000)
t2 = MyThread(10000000)
mutex = threading.Lock() # 同一把锁在同一时刻只能被一个线程获取,其他会在acquire处阻塞
t1.start()
t2.start()
print('主线程也在跑')
3. 产生死锁:
import threading, time
mutex1 = threading.Lock()
mutex2 = threading.Lock()
def foo1():
mutex1.acquire()
time.sleep(0.5)
mutex2.acquire()
time.sleep(0.5)
mutex2.release()
mutex1.release()
print('foo1 over')
def foo2():
mutex2.acquire()
time.sleep(0.5)
mutex1.acquire()
time.sleep(0.5)
mutex1.release()
mutex2.release()
print('foo2 over')
t1 = threading.Thread(target=foo1, args=())
t2 = threading.Thread(target=foo2, args=())
t1.start()
t2.start()
print('main flow')
会发现两把锁互相锁定,程序阻塞
4. 递归锁RLock解决死锁
两个特点:
- 一个计数器(可多层加锁)
- 一把锁(谁拿谁是释放)
递归锁内部有计数机制,会判断锁了几次,直到某个线程将递归锁技术清零才会释放。
一般可以这样写:
mutex = threading.RLock()
with mutex:pass
上述with代码等同于加锁再解锁,递归锁是最常用的,多见于谁拿谁释放,并未见到解决死锁的例子,
以下利用银行账户实例进行递归锁的理解(其实用一般锁也可以)
# Author:AD
# Date:2020/3/9
import time
import threading
class Account:
def __init__(self, _id, balance):
self.id = _id
self.balance = balance
self.lock = threading.RLock()
def withdraw(self, amount):
with self.lock:
self.balance -= amount
def deposit(self, amount):
with self.lock:
self.balance += amount
def drawcash(self, amount):#lock.acquire中嵌套lock.acquire的场景
with self.lock:
interest=0.05
count=amount+amount*interest
self.withdraw(count)
def transfer(_from, to, amount):
#锁不可以加在这里 因为其他的其它线程执行的其它方法在不加锁的情况下数据同样是不安全的
_from.withdraw(amount)
to.deposit(amount)
#_from.drawcash(amount)
alex = Account('alex',1000)
yuan = Account('yuan',1000)
t1=threading.Thread(target = transfer, args = (alex,yuan, 100))
t1.start()
t2=threading.Thread(target = transfer, args = (yuan,alex, 200))
t2.start()
t1.join()
t2.join()
print('>>>', 'alex', alex.balance)
print('>>>', 'yuan', yuan.balance)
可以发现每个对象都有一把锁,每次操作对象内的字段的时候进行上锁,无论多少个线程进行操作都会保证数据的安全。
如果把函数中的取现放开会发现下面的结果:
而将Rlock换为Lock会发现在取现的时候一般锁锁一次,再遇到第二次就会阻塞。
实验曲线不加两把锁也是正常的,也就是局部变量并无所谓。
5. 信号量 .Semaphore
线程中,信号量主要是用来维持有限的资源,使得在一定时间使用该资源的线程只有指定的数量, 爬虫、数据库等使用
与递归锁的区别:
- 递归锁在某线程进行加锁的过程中层层封闭,知道该线程将所有的锁都释放,其他线程才能进行操作
- 而信号量规定了最大线程数目,可以同时锁定多少线程
- 信号量并不是用来维护数据安全的
.BoundedSemaphore(value=maxconnections) 与 .Semaphore区别:
前者.release次数超过value会抛异常,后者不会。
完全可以用以下方案来解决这个问题:
maxconnections = 3
sema = threading.BoundedSemaphore(value=maxconnections)
with sema:
pass
'''上述代码与下等同'''
sema.acquire()
pass
sema.release()
爬虫控制演示:
import threading
import time
import random
sites = ["https://www.baidu.com/", "https://github.com/Fiz1994", "https://stackoverflow.com/",
"https://www.sogou.com/",
"http://english.sogou.com/?b_o_e=1&ie=utf8&fr=common_index_nav&query="] * 20
sites_index = 0
maxconnections = 3
pool_sema = threading.BoundedSemaphore(value=maxconnections)
class MyThread(threading.Thread):
def run(self):
with pool_sema:
global sites_index, sites
url = str(sites[sites_index])
k = random.randint(1, 10)
print(self, "爬去: " + url + " 需要时间 : " + str(k))
sites_index += 1
time.sleep(k)
print(self, '退出 ', url)
for i in range(10):
MyThread().start()
6. 条件变量同步.Condition()
有一类线程需要满足条件之后才能够继续执行,Python提供了threading.Condition 对象用于条件变量线程的支持,它除了能提供RLock()或Lock()的方法外,还提供了 wait()、notify()、notifyAll()方法。
lock_con=threading.Condition([Lock/Rlock]): 锁是可选选项,不传人锁,对象自动创建一个RLock()。
wait():条件不满足时调用,线程会释放锁并进入等待阻塞;
notify():条件创造后调用,通知等待池激活一个线程,并从acquire处开始执行;
notifyAll():条件创造后调用,通知等待池激活所有线程。
-
acquire(timeout)
调用Condition类关联的Lock/RLock的acquire()方法。 -
release()
调用Condition类关联的Lock/RLock的release()方法。 -
wait(timeout)
1)线程挂起,直到收到一个notify通知或者等待时间超出timeout才会被唤醒;
2)注意:wait()必须在已获得Lock的前提下调用,否则会引起RuntimeError错误。 -
notify(n=1)
1)唤醒在Condition的waiting池中的n(参数n可设置,默认为1)个正在等待的线程并通知它,受到通知的线程将自动调用acquire()方法尝试加锁;
2)如果waiting池中有多个线程,随机选择n个唤醒;
3)必须在已获得Lock的前提下调用,否则将引发错误。 -
notify_all()
唤醒waiting池中的等待的所有线程并通知它们。
import threading
from time import sleep
import random
# 商品
product = 1500
# 条件变量
con = threading.Condition(threading.Lock())
# 生产者类
# 继承Thread类
class Producer(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
global product
while True:
# 如果获得了锁
if con.acquire():
# if 1:
# con.acquire()
# 处理产品大于等于500和小于500的情况
if product >= 500:
# 如果大于等于500,Producer不需要额外操作,于是挂起
print(self.name + " >500")
con.wait()
#con.notify()
else:
product += 50
message = self.name + " produced 50 products."
print(message)
# 处理完成,发出通知告诉Consumer
con.notify()
# 释放锁
con.release()
sleep(5)
# 消费者类
# 继承Thread类
class Consumer(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
global product
while True:
# 如果获得了锁
if con.acquire():
# if 1:
#con.acquire()
# 处理product小于等于100和大于100的两种情况
if product <= 100:
# 如果小于等于100,Consumer不需要额外操作,于是挂起
print(self.name + " <100")
con.wait()
#con.notify()
else:
product -= 10
message = self.name + " consumed 10 products."
print(message)
# 处理完成,发出通知告诉Producer
con.notify()
# 释放锁
n = random.random()
sleep(n)
con.release()
n = random.randint(1,5)
sleep(n)
def main():
# 创建两个Producer
for i in range(2):
p = Producer('【Producer】-%d' % i)
p.start()
# 创建三个Consumer
for i in range(3):
c = Consumer('Consumer-%d' % i)
c.start()
if __name__ == '__main__':
main()
改进了网上的一个例子,实验发现以下结论
- wait释放锁,并进入wait池
- 不管是wait释放的锁还是release释放的锁,对于所有激活的线程,如果运行到acquire语句得话都存在竞争抢锁的关系
- 可能notify的话会激活wait池里面的线程进行抢锁