分析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匹配中文的正则表达式
May 11 Python
Python3实现发送QQ邮件功能(附件)
Dec 23 Python
Python动刷新抢12306火车票的代码(附源码)
Jan 24 Python
在Python中给Nan值更改为0的方法
Oct 30 Python
Python面向对象类编写细节分析【类,方法,继承,超类,接口等】
Jan 05 Python
numpy.linspace函数具体使用详解
May 27 Python
Python数据可视化:幂律分布实例详解
Dec 07 Python
Pycharm和Idea支持的vim插件的方法
Feb 21 Python
django实现将后台model对象转换成json对象并传递给前端jquery
Mar 16 Python
keras 模型参数,模型保存,中间结果输出操作
Jul 06 Python
Python爬虫进阶之爬取某视频并下载的实现
Dec 08 Python
python基于win32api实现键盘输入
Dec 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
关于url地址传参数时字符串有回车造成页面脚本赋值失败的解决方法
2013/06/28 PHP
分享一段PHP制作的中文拼音首字母工具类
2014/12/11 PHP
php中explode的负数limit用法分析
2015/02/27 PHP
PHP输出一个等腰三角形的方法
2015/05/12 PHP
PHP实现接收二进制流转换成图片的方法
2017/01/10 PHP
PHP开发中解决并发问题的几种实现方法分析
2017/11/13 PHP
PHP数组去重的更快实现方式分析
2018/05/09 PHP
JSQL SQLProxy 的 php 版本代码
2010/05/05 Javascript
jquery关于图形报表的运用实现代码
2011/01/06 Javascript
js随机生成网页背景颜色的方法
2015/02/26 Javascript
JS使用eval解析JSON的注意事项分析
2015/11/14 Javascript
js表单中选择框值的获取及表单的序列化
2015/12/17 Javascript
js中获取键盘事件的简单实现方法
2016/10/10 Javascript
详解vue-meta如何让你更优雅的管理头部标签
2018/01/18 Javascript
详解vue-cli 快速搭建单页应用之遇到的问题及解决办法
2018/03/01 Javascript
node.js遍历目录的方法示例
2018/08/01 Javascript
JavaScript 2018 中即将迎来的新功能
2018/09/21 Javascript
js中数组对象去重的两种方法
2019/01/18 Javascript
小程序简单两栏瀑布流效果的实现
2019/12/18 Javascript
[01:08]DOTA2“血战之命”预告片
2017/08/12 DOTA
[00:15]TI9观赛名额抽取
2019/07/10 DOTA
用python实现的去除win下文本文件头部BOM的代码
2013/02/10 Python
JSONLINT:python的json数据验证库实例解析
2017/11/28 Python
python学生管理系统开发
2019/01/30 Python
详解Python list和numpy array的存储和读取方法
2019/11/06 Python
关于keras.layers.Conv1D的kernel_size参数使用介绍
2020/05/22 Python
详解python UDP 编程
2020/08/24 Python
Python调用飞书发送消息的示例
2020/11/10 Python
2021年值得向Python开发者推荐的VS Code扩展插件
2021/01/25 Python
浅谈pc和移动端的响应式的使用
2019/01/03 HTML / CSS
PatPat阿根廷:妈妈们的购物平台
2019/05/30 全球购物
决心书范文
2014/03/11 职场文书
品德评语大全
2014/05/05 职场文书
房产公证书
2015/01/23 职场文书
Python网络编程之ZeroMQ知识总结
2021/04/25 Python
动画「半妖的夜叉姬」新BD特典图公开
2022/03/22 日漫