网络编程七:线程间的同步之lock锁
一、锁lock
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁为资源引入一个状态:锁定/非锁定。
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;
直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。
互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
import threading # 定义锁lock lock = threading.Lock() # 加锁,返回True lock.acquire() # 解锁,返回True lock.release()
lock实例只能调用一次加锁,再次加锁,将阻塞,直到解锁。即加锁 - 解锁 的次数 > 1次的时侯,将阻塞;等于1次或0次,有会阻塞。
因此使用lock锁保护共享资源时,只有一个线程可以修改共享资源;
其它线程修改共享资源时,相当于二次加锁,将阻塞,直到修改共享资源的线程释放锁。
lock.acqure的两个参数:blocking默认为True;timeout超时自动解锁,返回False;release解锁,返回True
lock.acquire(blocking=False) #不会阻塞,但返回False lock.acquire(timeout=3) # 超过3秒,未调用release解锁,将超时解锁,但返回False
二、示例:加锁acquire、解锁release
有个统计类,随机的加减1。
在单线程模式下,将得到正确的结果:加的次数 - 减的次数所得到的结果是确定的。
在多线程模式下,由于共享变量,加法之前使用的数值可能是当前线程的数值,也有可能是任意一个线程修改后的数值,是不确的。对共享资源没有保护,最终导致脏读等。
import threading, random,logging logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(threadName)s -- %(message)s') class Counter: def __init__(self): self._count = 0 @property def count(self): return self._count def inc(self): self._count += 1 def dec(self): self._count -= 1 def random(self): if random.choice([-1, 1]) > 0: logging.info("inc + 1........") self.inc() else: logging.info("dec - 1........") self.dec() count = Counter() for i in range(10): threading.Thread(target=count.random).start() threading.Event().wait(0.2) print(count.count)
2018-11-04 13:44:22,667 INFO Thread-1 -- dec - 1........ 2018-11-04 13:44:22,667 INFO Thread-2 -- inc + 1........ 2018-11-04 13:44:22,667 INFO Thread-3 -- dec - 1........ 2018-11-04 13:44:22,677 INFO Thread-4 -- dec - 1........ 2018-11-04 13:44:22,677 INFO Thread-5 -- inc + 1........ 2018-11-04 13:44:22,677 INFO Thread-6 -- dec - 1........ 2018-11-04 13:44:22,677 INFO Thread-7 -- dec - 1........ 2018-11-04 13:44:22,677 INFO Thread-8 -- dec - 1........ 2018-11-04 13:44:22,677 INFO Thread-9 -- dec - 1........ 2018-11-04 13:44:22,677 INFO Thread-10 -- inc + 1........ -5
加1执行3次,减1执行7次,结果应该是-4。但多线程的实际结果却是不确定的,这里是-5。
造成bug的原因,就是因为共享资源self._count在多个线程内是共享的,没有共享资源写保护,多个线程可以同时修改;
比如,当前线程读取到的修改前的_count值是5,在做加法的时侯,_count值被其它线程修改成了2,加法之后,结果就变成了3。
解决办法:
每个线程的每次修改,都需要对共享资源保护,一次只允许一个线程可以修改,不允许多个线程同时修改;event事件,不适合;lock锁适合。
对写的共享资源self._count加锁,避免幻读。
对读的共享资源self._count加锁,避免脏读。当没有任何线程会修改共享资源时,不需要对读的资源加锁,因为不可能会出现脏读。
import threading, random,logging logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(threadName)s -- %(message)s') class Counter: def __init__(self): self._count = 0 self._lock = threading.Lock() @property def count(self): with self._lock: return self._count def inc(self): try: self._lock.acquire() self._count += 1 finally: self._lock.release() def dec(self): with self._lock: self._count -= 1 def random(self): if random.choice([-1, 1]) > 0: logging.info("inc + 1........") self.inc() else: logging.info("dec - 1........") self.dec() count = Counter() for i in range(10): threading.Thread(target=count.random).start() threading.Event().wait(0.2) print(count.count)
2018-11-04 14:06:14,385 INFO Thread-1 -- inc + 1........ 2018-11-04 14:06:14,386 INFO Thread-2 -- inc + 1........ 2018-11-04 14:06:14,387 INFO Thread-3 -- dec - 1........ 2018-11-04 14:06:14,387 INFO Thread-4 -- inc + 1........ 2018-11-04 14:06:14,388 INFO Thread-5 -- dec - 1........ 2018-11-04 14:06:14,388 INFO Thread-6 -- dec - 1........ 2018-11-04 14:06:14,389 INFO Thread-7 -- dec - 1........ 2018-11-04 14:06:14,390 INFO Thread-8 -- dec - 1........ 2018-11-04 14:06:14,390 INFO Thread-9 -- dec - 1........ 2018-11-04 14:06:14,390 INFO Thread-10 -- dec - 1........ -4
如果在加锁和解锁的过程中,发生异常,将发生异常;因此只有使用锁的地方,都应该使用try.....finnaly,将解锁放在finnaly语句内。也可以用 with lock来代替try..finnaly。
何时需要加锁?多个线程间的资源都是共享的,对于可能会被修改的共享资源,写的时侯就需要加锁,避免幻读;这个可能被修改的共享资源在读的时侯,一般也需要加锁,避免脏读。
三、巧用lock.acquire(False)不阻塞锁
lock.acquire(False)不阻塞锁:对共享资源加锁,但不阻塞。
即当线程1执行任务1时,对任务1加锁,但是不阻塞其它线程;
因为任务1被加锁,线程2不会去执行任务1,但线程2也不会被阻塞(因为acquire(blocking=False)),会跳过任务1,执行其它任务,假设执行的是任务2;
因为任务1和任务2都被加锁,线程3不会去执行任务1和2,但线程3也不会被阻塞(因为acquire(blocking=False)),会跳过任务1和2,执行其它任务。
示例:有个线程池,预先启动5个线程,一起处理10个任务,当其中一个线程处理完任务1时,其它线程跳过任务1,处理其它任务。
import threading, logging logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(threadName)s -- %(message)s') def worker(tasks): for task in tasks: if task.lock.acquire(blocking=False): logging.info(task.name) class Task: def __init__(self, name): self.name = name self.lock = threading.Lock() tasks = [Task(x) for x in range(10)] for i in range(5): threading.Thread(target=worker, args=(tasks,), name="worker-{}".format(i)).start()
2018-11-04 14:33:19,630 INFO worker-0 -- 0
2018-11-04 14:33:19,630 INFO worker-0 -- 1
2018-11-04 14:33:19,630 INFO worker-1 -- 2
2018-11-04 14:33:19,630 INFO worker-0 -- 3
2018-11-04 14:33:19,630 INFO worker-2 -- 4
2018-11-04 14:33:19,640 INFO worker-2 -- 8
2018-11-04 14:33:19,630 INFO worker-1 -- 5
2018-11-04 14:33:19,630 INFO worker-0 -- 7
2018-11-04 14:33:19,630 INFO worker-3 -- 6
2018-11-04 14:33:19,640 INFO worker-2 -- 9
被执行过的任务,其它线程将跳过。
四、超时锁:
五、可重入锁:RLOCK
可重入锁:在同一个线程内,可以多次acquire成功,但是只能有一个线程acquire成功;acquire几次,就需要release几次。
rlock = threading.RLock() rlock.acquire() # True rlock.acquire(0 # True,可以多次加锁 rlock.release() rlock.release() # 加锁几次就要释放几次
六、锁与事件
在上一节中,使用事件同步线程时,说到所有线程中应该使用同一个event实例对象。
但是锁,并不要求所有线程都必须使用同一个锁的实例对象。就像第四点中,每个task有把锁。
加锁是怎么回事,其实并不是给资源加锁, 而是用锁去锁定资源,你可以定义多个锁。
当你需要独占某一资源时,任何一个锁都可以锁这个资源。
就好比你用不同的锁都可以把相同的一个门锁住是一个道理。
七、线程安全
什么是线程安全?
进程的资源,在此进程内的所有线程之间,是共享的,是线程之间的全局资源。
在同一进程内,线程之间的全局资源,是共享的。
线程安全,是指此对象(进程资源或线程之间的全局资源),在同一进程内的,在多线程之间都可以安全使用,内部已经实现锁的机制,不会出现脏读、幻读等现象。
比如,列表、字典、元组、字符串等python所有的内置对象、logging等,都实现锁机制,都是线程安全的;在同一进程内的多个线程之间,任意读取修改,都不会出现数据脏读、幻读等。
posted on 2018-11-03 18:14 myworldworld 阅读(364) 评论(0) 收藏 举报