概念梳理:
临界区:
临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段
,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待,有一些同步的机制必须在临界区段的进入点与离开点实现,以确保这些共用资源是被互斥获得使用。
目的:
我们在多线程
处理是,经常会涉及到对于一块共享资源的访问,这里就需要线程可以互斥去避免出现竞态条件(race condition).
方法:
要想让可变对象安全地在多线程环境中进行访问,就要使用锁变量(threading中的LockRLock),锁变量也是同步原语
中的最常见一种之一。
示例:
import threading
class ShareCounter:
'''
一个可以在多线程中分享的类
'''
def __init__(self, initial_value=0):
self._value = initial_value
self._value_lock = threading.Lock()
def add_one(self, delta=1):
with self._value_lock:# with 上下文管理器,是的锁的申请和释放更加方便(自动释放),更好地保证了互斥
# 同样很好地避免了死锁
self._value += delta#执行缩进语句时获得锁,缩进语句结束后自动释放锁
def sub_one(self):
with self._value_lock:
self._value -= 1
#with管理器完全等同于如下操作(老版本的Python/ target=_blank class=infotextkey>Python中):
# self._value_lock.acquire()
# self._value -= 1
# self._value_lock.release()
线程的调度从本质上来说是非确定性的(只能保证独一访问,但保证不了谁先谁后)。只要共享的可变状态需要被多个线程访问,就要使用锁机制,保证数据的安全。
在threading库中我们也发现了其他的同步原语例如:RLock(可重入锁)、Semaphore对象(信号量)。
RLock:可以被同一个线程多次获取,主要用来编写基于锁的代码,或者基于‘监听器’的同步处理。
当某个类持有这种类型锁时,只有一个线程可以使用类中全部函数或者方法,例如:
import threading
class ShareCounter:
'''
一个可以在多线程中分享的类
'''
_lock = threading.RLock()
def __init__(self, initial_value=0):
self._value = initial_value
def add_one(self, delta=1):
with ShareCounter._lock:
self._value += delta
def sub_one(self, delta=1):
with ShareCounter._lock:
self.add_one(-delta)
这份代码中只有一个作用于整个类的锁,它被所有的类实例所共享,不再将所绑定在某个实例的可变状态上,现在这个锁是用来同步类中的方法的。对于其他标准锁不同的是,对于已经持有了该锁的方法可以调用同样使用了这个锁的其他方法(参考sub_one())。
这个实现的特点是,无论创建了多少counter实例,这些实例共有同一把锁。因此,当有大量counter出现时,这种方法堆内存的使用效率要高很多。但是可能存在的缺点是在使用了大量线程且需要频繁更新counter中的数据时,这么做会出现锁争用的情况。
另外一种同步原语semaphore,是一种基于共享计数器的同步原语。如果计数器非0,那么with语句会递减计数器并且允许线程继续执行。当with语句块结束后,会将计数器递增。如果计数器为0,那么执行过程会被阻塞,直到由另外一个线程来递增计数器为止。由于信号量
的实现更为复杂,这会对程序带来一定的负面影响。除了简单地加锁功能外,信号量对象对于那些设计在线程间发送信号或者需要实现节流处理的应用中更加有用,例如限制并发总数:
from threading import Semaphore
import urllib.request
_fetch_url_sema = Semaphore(5)
def fetch_url(url):
with _fetch_url_sema:
return urllib.request.urlopen(url)