33 - 并发编程-线程同步-Event-lock
1 线程同步
线程同步,线程间协同,通过某种技术,让一个线程访问某些数据时,其他线程不能访问这些数据,直到该线程完成对数据的操作后。不同的操作系统有多种实现方式。比如临界区(Critical Section)、互斥锁(Mutex)、信号量(Semaphore)、事件(Event)等。
1.1 Event
Event是线程间通讯机制最简单的实现,使用一个内部标记flag,主要提供了三个方法wait、clear、set,通过操作flag来控制线程的执行。
- clear():将'Flag'设置为False。
- set():将'Flag'设置为True。
- wait(timeout=None):等待'Flag'为True后,继续执行(timeout为超时时间,否则永远等待)。
- is_set(): 判断'Flag'是否为
1.1.1 什么是Flag?
Event对象在全局定义了一个'Flag',如果'Flag'值为 False,那么当程序执行 Event对象的wait方法时就会阻塞,如果'Flag'值为True,那已经阻塞的wait方法会继续执行。
1.1.2 Event原理
在使用threading.Event 实现线程间通信时:使用threading.Event可以使一个线程等待其他线程的通知,我们把这个Event传递到线程对象中,Event默认内置了一个标志,初始值为False。一旦该线程通过wait()方法进入等待状态,直到另一个线程调用该Event的set()方法将内置标志设置为True时,该Event会通知所有等待状态的线程恢复运行。
1.1.3 吃包子
有下面代码,大欣负责吃包子,厨师负责做包子,只有厨师做好了,大欣才能开始吃。
import time
import random
event = threading.Event()
def consumer():
current = threading.current_thread()
print('{} 等着吃包子...'.format(current.name))
event.wait()
print('包子来了,我正在吃')
for i in range(1,5):
time.sleep(random.randrange(1,3))
print('{} 吃 包子-{}。'.format(current.name,i))
def chef():
current = threading.current_thread()
print('{} 正在做包子'.format(current.name))
time.sleep(3)
print('{} 包子做好了,下班'.format(current.name))
event.set()
chef = threading.Thread(target=chef,name='大厨师')
chef.start()
consumer = threading.Thread(target=consumer,name='大欣')
consumer.start()
运行consumer时,包子还没做,所以只能等着,等chef做完了以后,设置了event为True,这时consumer就开始吃了。wait还可以指定等待时间,比如chef做的太慢了,consumer不吃了。
import time
import random
event = threading.Event()
def consumer():
current = threading.current_thread()
print('{} 等着吃包子...'.format(current.name))
if not event.wait(2):
print('太慢了,不吃了')
else:
print('包子来了,我正在吃')
for i in range(1,5):
time.sleep(random.randrange(1,3))
print('{} 吃 包子-{}。'.format(current.name,i))
def chef():
current = threading.current_thread()
print('{} 正在做包子'.format(current.name))
time.sleep(8)
print('{} 包子做好了,下班'.format(current.name))
event.set()
chef = threading.Thread(target=chef,name='大厨师')
chef.start()
consumer = threading.Thread(target=consumer,name='大欣')
consumer.start()
当Event被set后,wait的返回值就是True,如果wait(2),在2秒内,Event没有被set,那么返回值是False。
1.2 Lock
由于线程间的数据是共享的,当我们多个线程操作一个相同的用户的数据时,有可能造成混乱,如下例子:
import threading
import time
n = 10
def work():
global n
while n > 0:
time.sleep(0.01)
n -= 1
if __name__ == '__main__':
t_l = []
for i in range(10):
t = threading.Thread(target=work)
t_l.append(t)
t.start()
for t in t_l:
t.join()
print(n)
我们认为结果应该是0,但是结果可能不如人意,因为进程间共享数据的问题,多个进程同时修改共享数据时,由于GIL的存在同一时刻只有1个线程在运行。当n的值为1的时候,10个子线程很有可能同时判断成功,再要修改的时候被挂起(时间片用完),等到真正回来修改的时候,n已经被其他线程改过来!所以如果要保持数据的正确性,那么就需要牺牲性能,即使用锁机制。
- 创建一个锁,由于进程内的线程共享进程数据,那么不需要传递,就可以直接调用
import threading
mutex = threading.Lock()
- 加锁解锁
Lock内部实现了__enter__和__exit__的方法,所以我们可以使用两种方式来加锁或者解锁。
# 调用方式一
mutex.acquire() # 加锁
'''code'''
mutex.release() # 解锁
# 调用方式二
with mutex:
'''code''' # 离开wit代码段,自动解锁
1.2.1 lock方法
名称 | 含义 |
---|---|
acquire(blocking=True,timeout=-1) | 获取锁并加锁 blocking:表示是否阻塞,默认为True表示阻塞。 timeout:表示阻塞超时时间。 当blocking为非阻塞时,timeout不能进行设置 |
release() | 释放锁,可以从任何线程上调用释放. 已上锁,调用时会释放锁,即重置为unlocked。 未上锁,调用时会抛出RuntimeError异常 |
所以,上面的例子可以有如下修改:
import threading
import time
mutex = threading.Lock()
n = 10
def work():
global n
while True:
mutex.acquire()
if n > 0:
time.sleep(1)
n -= 1
mutex.release()
else:
mutex.release()
break
if __name__ == '__main__':
t_l = []
for i in range(10):
t = threading.Thread(target=work)
t_l.append(t)
t.start()
for t in t_l:
t.join()
print(n)
在判断的时候就开始加锁,在修改完毕的时候解锁。这样加锁的情况下我们发现运行时间变长了,那是因为只有抢到锁的线程才可以工作(穿行执行),
1.2.2 计数器
有下面一个计数器类,来看如何加锁
import threading
class Counter:
def __init__(self):
self._value = 0
@property
def value(self):
return self._value
def inc(self):
self._value += 1
def dec(self):
self._value -= 1
def calc(c:Counter):
for _ in range(1000):
for i in range(-50,50):
if i < 0:
c.dec()
else:
c.inc()
c = Counter()
lst = []
for i in range(10):
t = threading.Thread(target=calc, args=(c,))
lst.append(t)
t.start()
for t in lst:
t.join()
print(c.value)
在需要调用和修改的地方加锁,修改完毕后解锁,是锁使用的基本原则,一般来说,加锁就要解锁,但是加锁和解锁之间会有一些代码要执行,如果出现异常,那么锁是无法释放的,但是当前线程已经终止了,这种情况一般称为死锁,可以添加异常处理,来确保锁一定被释放。
import threading
mutex = threading.Lock()
class Counter:
def __init__(self):
self._value = 0
@property
def value(self):
return self._value
def inc(self):
try: # 添加异常处理,即便时崩溃也可以释放锁
mutex.acquire()
self._value += 1
finally:
mutex.release()
def dec(self):
with mutex: # 上下文管理写法
self._value -= 1
def calc(c:Counter):
for _ in range(1000):
for i in range(-50,50):
if i < 0:
c.dec()
else:
c.inc()
c = Counter()
lst = []
for i in range(10):
t = threading.Thread(target=calc, args=(c,))
lst.append(t)
t.start()
for t in lst:
t.join()
当然这里也可以为每一个计数器实例对象初始化一个自己的锁,如果用全局锁,那么不同的计数器实例,会相互影响(因为多个实例,共享一把锁),因为不同实例的结果是不同的,所以建议为每个实例构建一个自己的锁。
class Counter:
def __init__(self):
self._value = 0
self._lock = threading.Lock()
@property
def value(self):
return self._value
def inc(self):
try:
self._lock.acquire()
self._value += 1
finally:
self._lock.release()
def dec(self):
with self._lock:
self._value -= 1
1.2.3 非阻塞锁
当lock.acquire(False)时,该锁就是非阻塞锁了,调用时,仅获取一次,如果获取到那么返回True,否则返回False。
import time
import threading
import random
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(threadName)s %(thread)s %(message)s')
def worker(tasks:list):
for task in tasks:
if task.lock.acquire(False): # 抢到任务
time.sleep(random.randrange(1,3)) # 模拟执行任务需要的时间
logging.info('{} I am working for {}'.format(threading.current_thread().name, task))
else: # 没有抢到任务
logging.info('{} not get {}'.format(threading.current_thread().name,task))
class Task:
def __init__(self,name):
self.task = name
self.lock = threading.Lock()
def __repr__(self):
return self.task
__str__ = __repr__
if __name__ == '__main__':
task_list = [Task('task{}'.format(x)) for x in range(1,11)] # 构建任务列表
for _ in range(10):
threading.Thread(target=worker,args=(task_list,)).start() # 开启多线程执行任务
10个任务,交给10个线程运行,谁抢到哪个线程,谁就运行。
1.2.4 锁应用场景
锁适用于访问和修改同一个共享资源的时候,即读写同一个资源的时候。但如果都是读取的话,就不需要了。使用锁的时候有一下几点需要特别注意:
- 少于锁,必要时用锁,使用了锁,多线程访问被被锁资源时,就成了穿行的了,要么排队,要么争抢执行。
- 加锁时间越短越好,不需要时立即释放锁。
- 一定要避免死锁。
33 - 并发编程-线程同步-Event-lock的更多相关文章
- Python并发编程-线程同步(线程安全)
Python并发编程-线程同步(线程安全) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 线程同步,线程间协调,通过某种技术,让一个线程访问某些数据时,其它线程不能访问这些数据,直 ...
- 11 并发编程-(线程)-信号量&Event&定时器
1.信号量(本质也是一把锁)Semaphore模块 信号量也是一把锁,可以指定信号量为5,对比互斥锁同一时间只能有一个任务抢到锁去执行, 信号量同一时间可以有5个任务拿到锁去执行, 如果说互斥锁是合租 ...
- C#并行编程-线程同步原语
菜鸟学习并行编程,参考<C#并行编程高级教程.PDF>,如有错误,欢迎指正. 目录 C#并行编程-相关概念 C#并行编程-Parallel C#并行编程-Task C#并行编程-并发集合 ...
- Java并发编程:同步容器
Java并发编程:同步容器 为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch). ...
- 【转】Java并发编程:同步容器
为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch).今天我们就来讨论下同步容器. ...
- 8、Java并发编程:同步容器
Java并发编程:同步容器 为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch). ...
- Java 并发编程 | 线程池详解
原文: https://chenmingyu.top/concurrent-threadpool/ 线程池 线程池用来处理异步任务或者并发执行的任务 优点: 重复利用已创建的线程,减少创建和销毁线程造 ...
- java并发编程 线程基础
java并发编程 线程基础 1. java中的多线程 java是天生多线程的,可以通过启动一个main方法,查看main方法启动的同时有多少线程同时启动 public class OnlyMain { ...
- java并发:线程同步机制之Lock
一.初识Lock Lock是一个接口,提供了无条件的.可轮询的.定时的.可中断的锁获取操作,所有加锁和解锁的方法都是显式的,其包路径是:java.util.concurrent.locks.Lock, ...
随机推荐
- oracle锁与死锁概念,阻塞产生的原因以及解决方案
锁是一种机制,一直存在:死锁是一种错误,尽量避免. 首先,要理解锁和死锁的概念: 1.锁: 定义:简单的说,锁是数据库为了保证数据的一致性而存在的一种机制,其他数据库一样有,只不过实现机制上可能大 ...
- Linux下启用MySQL慢查询
MySQL在linux系统中的配置文件一般是my.cnf找到[mysqld]下面加上log-slow-queries=/data/mysqldata/slowquery.loglong_query_t ...
- 第88天:HTML5中使用classList操作css类
在HTML5 API里,页面DOM里的每个节点上都有一个classList对象,程序员可以使用里面的方法新增.删除.修改节点上的CSS类.使用classList,程序员还可以用它来判断某个节点是否被赋 ...
- Guardian of Decency UVALive - 3415(最大独立集板题)
老师在选择一些学生做活动时,为避免学生发生暧昧关系,就提出了四个要求.在他眼中,只要任意两个人符合这四个要求之一,就不可能发生暧昧.现在给出n个学生关于这四个要求的信息,求老师可以挑选出的最大学生数量 ...
- Ants UVA - 1411(km板题竟然让我换了个板子)
题意: 给出n个白点和n个黑点的坐标,要求用n条不相交的线段把它们连接起来,其中每条线段恰好连接一个白点和一个黑点,每个点恰好连接到一条线段 解析: 带入负的欧几里得距离求就好了 假设a1-b1 与 ...
- 洛谷 P2659 美丽的序列 解题报告
P2659 美丽的序列 题目背景 GD是一个热衷于寻求美好事物的人,一天他拿到了一个美丽的序列. 题目描述 为了研究这个序列的美丽程度,GD定义了一个序列的"美丽度"和" ...
- Linux内核分析第三周学习博客——跟踪分析Linux内核的启动过程
Linux内核分析第三周学习博客--跟踪分析Linux内核的启动过程 实验过程截图: 过程分析: 在Linux内核的启动过程中,一共经历了start_kernel,rest_init,kernel_t ...
- oAuth2.0理解
转自http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html 理解OAuth 2.0 作者: 阮一峰 日期: 2014年5月12日 OAuth是一个关 ...
- bzoj1969: [Ahoi2005]LANE 航线规划(树链剖分)
只有删边,想到时间倒流. 倒着加边,因为保证图连通,所以一开始一定至少是一棵树.我们先建一棵树出来,对于每一条非树边,两个端点在树上这段路径的边就不会变成关键边了,所以将它们对答案的贡献删去,那么直接 ...
- bzoj5055: 膜法师(BIT)
大水题WA了两发T T 记录一下a[i]的前缀和,a[i]*a[j]就是sigma(a[j]*sumi[j-1]) 记录一下a[i]*a[j]的前缀和,a[i]*a[j]*a[k]就是sigma(a[ ...