多线程的练习,并学到了经验教训
#python #multithreading

Cover image(c)Tai Kedzierski)

最近我要重写上一份工作中所做的事情,因为我想保留自己获得的知识。

在那个工作中,我们有一个硬件系统,它具有几个数据流,我们需要确保我们不会丢失任何数据,以及我们需要同时执行的几个硬件控制。我们会在一个流上接收流输入,通过另一个流发送命令,更改硬件状态,然后在流上检查以查看预期效果已经发生的证据。

我认为我从中学到的特定部分是流控制的实现:它们必须始终从至少串行线中读取,并且能够遵循通过SSH连接访问的实时日志。 /p>

,因此螺纹流组件诞生了。

我不会进入如何实现的挑剔 - 您可以在Github上看到源代码。

我在这篇文章中想做的是召集一些我在此过程中学到的东西。

关注的范围

我要谈的第一件事是分开关注的范围的重要性:多线程足以自行管理,而无需给您的呼叫和您的心理模型带来额外的行李。

螺纹流的设计是提供用于访问IO流的螺纹安全机制,同时能够支持不同类型的运输层。

一个人可以认为“好的,将您的通用流材料放入一个类中,然后将TCP/serial/ssh的东西放入子类中。”这是一个相当根本的分离,在早期发生 - 但是还有另一层。当您考虑“通用流”内容时,您实际上会错过线程安全部件。如果您将线程安全方面和流读数API置于一个范围内,那么您将负担两个责任范围:成为线程安全 愉快的使用。这并不是说“线程安全”不需要容易(这是必须的!),但应该是给定的。 nemeness 与“读取线”或“窥视缓冲区”或“读取直到找到条件x”之类的便利性。

因此,继承树实际上是这样的:

Thread
    |
    +- ThreadedStream
        |
        +- IOStream
            |
            +- TCP implementor
            +- Serial implementor
            +- SSH implementor

IOStream实现了与 streams Easy的Nice-Nicey read_line()read_until() API; ThreadedStream在基本read()write()操作上提供了螺纹安全。后者得到了实施者的_raw_read()_raw_write()方法的支持,这些方法除了其主要和明显的功能,还必须有额外的要求:它们必须是无障碍的。

对于串行,这很容易 - 库是自然的非障碍物。对于TCP,可以设置一种模式,并且需要管理。对于SSH,一些例外需要在不准备的状态下进行管理。如果您检查三个实施者的文件,您会发现,一旦评论被删除,每个都非常简单。他们关心忙碌的bytes的工作 - 不是很友善(IOtream的工作)或知道螺纹(ThreadedStream的Purview)。同样,IOStream有一个简单的时间实现单个read_until方法,read_line方法可以依靠,提供行分隔符的概念以及类似的概念; ThreadedStream可以专注于以安全的方式管理基础缓冲区的半复杂任务。

限制课堂关注的范围是 parmaint 保持理智 - 将来扩展实施。

快速记录可测试性

它还在提供可检验性方面支付。实际的流实施器很容易编写和阅读 - 但是验证线程和高级阅读的逻辑是否正常工作,因为它为整个项目提供了支持。能够从运输实施中尽可能多地移出,这意味着我能够实施基本的“人造”流,该流提供了自己的小内部缓冲区来嘲笑网络线。

因此,在嘲笑实际运输实施的端点本身就是一种练习,而人造运输实施不需要其他设置,并允许快速且轻松地验证实施。

放慢速度会加快您的速度

在通过PYTest进行基本测试时,我发现Pytest本身似乎正在大大减慢库的执行速度 - 在某一时刻,我什至没有停止测试,因为按下Ctrl+C导致打印堆栈跟踪。以每秒为单位,而是每行 秒。当然,pytest不是猪吗?我有一个次要的顿悟,要使用一个单元测试框架来构建集成测试实用程序,并质疑一个whlie,以及对我们的运行时间有害是否有害效果...

但实际上,问题是执行循环。我的执行循环 - 纯净而简单。它是神经质。我只有一段时间才发现,当您无限地通过操作循环时,在每个可用时钟周期中,在插座上都没有毫无意义的强迫症检查处理器是值得的。下层套接字堆栈本身保持一个缓冲区,足以使我们的过程很少查询它 - 我的意思是“很少”,我的意思是“每千循环左右”,在1.7 GHz(对于入门级处理器)仍在每秒10e8至10e9次之间。

这是tick参数进来的地方。它设置为一个缓慢的10E6,它在内部变成了ThreadedStream主循环的每个执行之间的等待时间。亲爱的读者,这就是释放CPU的原因。放慢循环使整个系统的工作速度更快。我喜欢它是一个有效的类比:暂停呼吸和轻松的经常放松,您会更有效地进行操作。

对于所有挂钩,pytest似乎都添加到代码中,从运行时性能的角度来看,它们变得无法检测到。现在处理器的负载尚未达到200%,一切都罚款了。

使用上下文管理的重新输入锁

我想涉及的下一个项目线程安全基础知识。当您拥有简单的用例时,多线程很容易。如果您不仔细考虑哪些执行线程会被调用哪些功能,则可以轻松地将自己编码为难以管理的情况。如果您无法正确管理锁,则会变得特别毛茸茸。

对于后一个问题,Python的上下文管理机制特别有用。如果将尝试访问线程敏感资源的任何代码放置在with self.lock:下方,则可以确保您锁定 will> will> will 一旦完成(只需锁定锁,请非常小心)在循环中)。您甚至可能具有较高的功能,该功能希望在资源上锁定,而低级功能也需要锁定 - 这在IoStream.read_until(...)中显着发生。它希望执行一个统一的动作,该动作包括多个操作,每个操作都为自己的安全而获得锁。通常,这将是一个问题,因为假设read_until()功能具有唯一的锁,并且其孩子将等待依次获得锁。重新输入锁允许相同的线程重新接口相同的锁以打破僵局(双关语),每个人都必须记住释放锁定的警告,哪种上下文管理有助于确保。

但请记住,我们不能让我们的阅读循环本身是神经质的。与其实施此功能的另一个等待tick tick(我最初是这样做的),而是回到一个事实,即我们对重新阅读缓冲区不感兴趣,直到至少有 comentic < /em>要更新。我们为此使用了事件对象,ThreadedStream中的主read()发出了任何等待线程,是的,我可能已经添加了一些东西,来获取。我们的read_until()操作具有Event.wait()-Ed收益,因此也可以背包读者线程。

Slow down.

解决数据丢失

这次我想做的一件事,我无法在原始工作中难过,正在处理错误。有时,您会遇到读取错误,一些例外...也许您尝试read_until()失败了,因为找不到目标项目。但是,也许目标不是真正的必要...您仍然想知道累积的数据在错误之前到底是什么。

好。这感觉就像是垫片,我不确定自己对此有多高兴或羞愧。但这绝对是我需要的用例。我的前身技术是公开缓冲区以直接访问。我的技术是...将缓冲区数据的副本附加到例外:Grimace:

IoStreamThreadedStream均对其读写操作执行动作,在遇到错误后,捕获它,将输​​入和输出缓冲区的副本附加到其上(不消耗它们),然后重新启动。这允许调用上下文捕获错误,并决定是否在保释之前处理接收到的数据(IoStream错误是非致命的,并且可以继续使用该流 - ThreadedStream错误是致命的,将杀死线程)。

这样,保留了错误控制流,同时也试图确保最大数据保留。

多处理很容易

不,不是。至少不一定是。但是,分开您的疑虑,确保您的抽象可以测试,并正确管理锁和事件,可以将问题解散。我认为。

完成另一个项目的游戏循环后,我会与您联系。