分析Python感知线程状态的解决方案之Event与信号量


Posted in Python onJune 16, 2021

一、停止线程

利用Threading库我们可以很方便地创建线程,让它按照我们的想法执行我们想让它执行的事情,从而加快程序运行的效率。然而有一点坑爹的是,线程创建之后,就交给了操作系统执行,我们无法直接结束一个线程,也无法给它发送信号,无法调整它的调度,也没有其他高级操作。如果想要相关的功能,只能自己开发。

怎么开发呢?

我们创建线程的时候指定了target等于一个我们想让它执行的函数,这个函数并不一定是全局函数,实际上也可以是一个对象中的函数。如果是对象中的函数,那么我们就可以在这个函数当中获取到对象中的其他信息,我们可以利用这一点来实现手动控制线程的停止。

说起来好像不太好理解,但是看下代码真的非常简单:

import time
from threading import Thread

class TaskWithSwitch:
    def __init__(self):
        self._running = True
        
    def terminate(self):
        self._running = False

    def run(self, n):
        while self._running and n > 0:
            print('Running {}'.format(n))
            n -= 1
            time.sleep(1)

c = TaskWithSwitch()
t = Thread(target=c.run, args=(10, ))
t.start()
c.terminate()
t.join()

如果你运行这段代码,会发现屏幕上只输出了10,因为我们将_running这个字段置为False之后,下次循环的时候不再满足循环条件,它就会自己退出了。

分析Python感知线程状态的解决方案之Event与信号量

如果我们想要用多线程来读取IO,由于IO可能存在堵塞,所以可能会出现线程一直无法返回的情况。也就是说我们在循环内部卡死了,这个时候单纯用_running来判断还是不够的,我们需要在线程内部设置计时器,防止循环内部的卡死。

class IOTask:
    def __init__(self):
        self._running = True
        
    def terminate(self):
        self._running = False

    def run(self, sock):
        # 在socket中设置计时器
        sock.settimeout(10)
        while self._running:
            try:
                # 由于设置了计时器,所以这里不会永久等待
                data = sock.recv(1024)
                break
            except socket.timeout:
                continue
        return

二、线程信号的传递

我们之所以如此费劲才能控制线程的运行,主要原因是线程的状态是不可知的,并且我们无法直接操作它,因为它是被操作系统管理的。我们运行的主线程和创建出来的线程是独立的,两者之间并没有从属关系,所以想要实现对线程的状态进行控制,往往需要我们通过其他手段来实现。

我们来思考一个场景,假设我们有一个任务,需要在另外一个线程运行结束之后才能开始执行。要想要实现这一点,就必须对线程的状态有所感知,需要其他线程传递出信号来才行。我们可以使用threading中的Event工具来实现这一点。Event工具就是可以用来传递信号的,就好像是一个开关,当一个线程执行完成之后,会去启动这个开关。而这个开关控制着另外一段逻辑的运行。

我们来看下样例代码:

import time
from threading import Thread, Event

def run_in_thread():
    time.sleep(1)
    print('Thread is running')

t = Thread(target=run_in_thread)
t.start()

print('Main thread print')

我们在线程里面就只做了输出一行提示符,没有其他任何逻辑。由于我们在run_in_thread函数当中沉睡了1s,所以一定是先输出Main thread print再输出的Thread is running。假设这个线程是一个很重要的任务,我们希望主线程能够等待它运行到一个阶段再往下执行,我们应该怎么办呢?

注意,这里说的是运行到一个阶段,并不是运行结束。运行结束我们很好处理,可以通过join来完成。但如果不是运行结束,而是运行完成了某一个阶段,当然通过join也可以,但是会损害整体的效率。这个时候我们就必须要用上Event了。加上Event之后,我们再来看下代码:

import time
from threading import Thread, Event

def run_in_thread(event):
    time.sleep(1)
    print('Thread is running')
    # set一下event,这样外面wait的部分就会被启动
    event.set()

# 初始化Event
event = Event()
t = Thread(target=run_in_thread, args=(event, ))
t.start()

# event等待set
event.wait()
print('Main thread print')

整体的逻辑没有太多的修改,主要的是增加了几行关于Event的使用代码。

我们如果要用到Event,最好在代码当中只使用一次。当然通过Event中的clear方法我们可以重置Event的值,但问题是我们没办法保证重置的这个逻辑会在wait之前执行。如果是在之后执行的,那么就会问题,并且在debug的时候会异常痛苦,因为bug不是必现的,而是有时候会出现有时候不会出现。这种情况往往都是因为多线程的使用问题。

所以如果要多次使用开关和信号的话,不要使用Event,可以使用信号量。

三、信号量

Event的问题在于如果多个线程在等待Event的发生,当它一旦被set的时候,那么这些线程都会同时执行。但有时候我们并不希望这样,我们希望可以控制这些线程一个一个地运行。如果想要做到这一点,Event就无法满足了,而需要使用信号量。

信号量和Event的使用方法类似,不同的是,信号量可以保证每次只会启动一个线程。因为这两者的底层逻辑不太一致,对于Event来说,它更像是一个开关。一旦开关启动,所有和这个开关关联的逻辑都会同时执行。而信号量则像是许可证,只有拿到许可证的线程才能执行工作,并且许可证一次只发一张。

想要使用信号量并不需要自己开发,thread库当中为我们提供了现成的工具——Semaphore,我们来看它的使用代码:

# 工作线程
def worker(n, sema):
    # 等待信号量
    sema.acquire()
    print('Working', n)

# 初始化
sema = threading.Semaphore(0)
nworkers = 10
for n in range(nworkers):
    t = threading.Thread(target=worker, args=(n, sema,))
    t.start()

在上面的代码当中我们创建了10个线程,虽然这些线程都被启动了,但是都不会执行逻辑,因为sema.acquire是一个阻塞方法,没有监听到信号量是会一直挂起等待。

分析Python感知线程状态的解决方案之Event与信号量

当我们释放信号量之后,线程被启动,才开始了执行。我们每释放一个信号,则会多启动一个线程。这里面的逻辑应该不难理解。

四、总结

在并发场景当中,多线程的使用绝不是多启动几个线程做不同的任务而已,我们需要线程间协作,需要同步、获取它们的状态,这是非常不容易的。一不小心就会出现幽灵bug,时显时隐,这也是并发问题让人头疼的主要原因。

以上就是分析Python感知线程状态的解决方案之Event与信号量的详细内容,更多关于Python 感知线程状态 Event与信号量的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
Python中关于字符串对象的一些基础知识
Apr 08 Python
Python实现变量数值交换及判断数组是否含有某个元素的方法
Sep 18 Python
给你选择Python语言实现机器学习算法的三大理由
Nov 15 Python
Python实现的NN神经网络算法完整示例
Jun 19 Python
tensorflow 用矩阵运算替换for循环 用tf.tile而不写for的方法
Jul 27 Python
python使用minimax算法实现五子棋
Jul 29 Python
python实现图像检索的三种(直方图/OpenCV/哈希法)
Aug 08 Python
python字典的遍历3种方法详解
Aug 10 Python
python实现文件的分割与合并
Aug 29 Python
节日快乐! Python画一棵圣诞树送给你
Dec 24 Python
Tensorflow tf.nn.atrous_conv2d如何实现空洞卷积的
Apr 20 Python
详细分析Python可变对象和不可变对象
Jul 09 Python
Python中else的三种使用场景
Jun 16 #Python
Python基础之条件语句详解
教你怎么用Python实现GIF动图的提取及合成
如何理解python接口自动化之logging日志模块
Jun 15 #Python
python基于turtle绘制几何图形
详解Flask开发技巧之异常处理
Jun 15 #Python
Python Pandas常用函数方法总结
Jun 15 #Python
You might like
PHP下几种删除目录的方法总结
2007/08/19 PHP
php数组函数序列之array_unshift() 在数组开头插入一个或多个元素
2011/11/07 PHP
微信支付开发教程(一)微信支付URL配置
2014/05/28 PHP
php关联数组快速排序的方法
2015/04/17 PHP
php线性表的入栈与出栈实例分析
2015/06/12 PHP
ExtJS 2.0 实用简明教程之布局概述
2009/04/29 Javascript
js弹窗返回值详解(window.open方式)
2014/01/11 Javascript
javascript原生和jquery库实现iframe自适应高度和宽度
2014/07/18 Javascript
node.js中的fs.readdir方法使用说明
2014/12/17 Javascript
js实现iGoogleDivDrag模块拖动层拖动特效的方法
2015/03/04 Javascript
JS实现动态给图片添加边框的方法
2015/04/01 Javascript
JS实现alert中显示换行的方法
2015/12/17 Javascript
微信小程序 图片边框解决方法
2017/01/16 Javascript
vue项目初始化到登录login页面的示例
2019/10/31 Javascript
vue中echarts引入中国地图的案例
2020/07/28 Javascript
在vue中实现某一些路由页面隐藏导航栏的功能操作
2020/09/21 Javascript
vant自定义二级菜单操作
2020/11/02 Javascript
[08:54]DOTA2-DPC中国联赛 正赛 Aster vs LBZS 选手采访
2021/03/11 DOTA
Python实现的简单模板引擎功能示例
2017/09/02 Python
python中print()函数的“,”与java中System.out.print()函数中的“+”功能详解
2017/11/24 Python
pycharm下查看python的变量类型和变量内容的方法
2018/06/26 Python
基于python分析你的上网行为 看看你平时上网都在干嘛
2019/08/13 Python
使用浏览器访问python写的服务器程序
2019/10/10 Python
Python开发之身份证验证库id_validator验证身份证号合法性及根据身份证号返回住址年龄等信息
2020/03/20 Python
Restful_framework视图组件代码实例解析
2020/11/17 Python
使用html5 canvas 画时钟代码实例分享
2015/11/11 HTML / CSS
Vince官网:全球著名设计师品牌,休闲而优雅的服饰
2017/01/15 全球购物
Speedo澳大利亚官网:全球领先游泳品牌
2018/02/04 全球购物
Woods官网:加拿大最古老、最受尊敬的户外品牌之一
2020/09/12 全球购物
专升本自我鉴定
2013/10/10 职场文书
学习考察心得体会
2014/09/04 职场文书
搞笑欢迎词大全
2015/09/30 职场文书
小学语文教师竞聘演讲稿范文
2019/08/09 职场文书
导游词之河北野三坡
2019/12/11 职场文书
分享Python异步爬取知乎热榜
2022/04/12 Python
Python使用永中文档转换服务
2022/05/06 Python