网络编程七:线程间的同步之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)    收藏  举报

导航