许多服务器端的应用和许多桌面应用都含有与一个特殊任务的执行有关的数据。常见的解决方案是将这类数据放到线程本地存储区中,将变量中的数据与其执行线程相绑定。的确很方便,但这是一个基于有缺陷的假设的实践方案。
Bob Martin 写了一篇关于假设线程跟工作单元存在一对一关系的专题文章:
ThreadLocal 变量对一个给定的线程而言是一个极为便利的关联数据的方式。例如,类似于 Hibernate 这样的框架利用它来保存会话信息。但是,这种 practice 非常依赖于将一个线程和一个工作单元等同视之。这是一种错误的假设。
ThreadLocal 是一个 Java 术语,但其构造在多线程环境中是通用的。Bob 还记得:
十三年前写我的第一本书时,我和 Jim Coplien 针对线程和对象的本质发生了争论。他做出了一个令我如今仍记忆犹新的澄清,他说:“一个对象是功能的抽象,一个线程是调度的抽象”。
当前,将工作单元中的数据映射到一个线程中是一个标准的模式,一个在许多流行的框架中可以发现的模式,但即便是这种方法存在了相当长的时间,这个不完美的抽象仍然缺乏一些情景。
在一个任务的不同部分存在不同的优先级是很常见的事情,而且也没有规则约束说一个任务必须是单线程的。Bob 举例说明一个工作单元在基于得到的数据执行一个相当长时间的计算时,可能会非常需要跟一个外部服务的响应性沟通,这样一个问题通常是分解成两个线程来解决。他问道:
工作单元相关的变量应该放在哪儿?它们不应被保存到 ThreadLocal 中,因为任务的每一个部分在一个分离的线程中运行. 它们也不能被保存在静态变量中,因为有很多的线程。最终答案是它们必须在线程间作为栈中的功能参数进行传递,并且被记录在存放于 queue 队列之中的数据结构内。
TapsaKoo 不久前也遇到了一个同样的情况。当在 WinForms 中努力尝试领域驱动的方式时,他描述了他寻找一个保存会话专有数据的问题:
如果应用程序每次只有一个表单被打开,我会把会话对象保存到 Callcontext。如果应用每次打开多个表单,并且这些表单希望拥有我的 session 类的一个单独的实例时该怎么办?CallContext 已经不能满足要求了。那么全都是线程专有的备选方案吗?还剩下什么了?什么也没有剩下?我已经不是考虑这个问题的第一个人了。可能会有存在一个解决方案,但我找不到它。我应该把会话对象注入达到需要它的每个对象实例中吗?还是我将领域类的许多行为重构到服务中,然后将会话对象注入其中?我不太喜欢这种方式,因为我希望我的类被数据容器有更多的含义。
在阅读完 Bob 大叔的回复后,TapsaKoo也同意对此问题没有轻松的解决方案:
无论你有 1 个还是 10 个线程,问题总是相同的。工作单元或者会话状态应该存放在一个不依赖于线程的地方。认为工作单元直接对应于一个单个的线程是非常危险的假设。这种假设会严重限制你的其他的架构性选择。
Bob 推断可能丢失了某些东西:
所以,尽管是司空见惯,ThreadLocal 变量混淆了调度与功能的分离问题。它们诱惑我们将功能和调度耦合在了一起。这是非常不幸的事情,因为功能和调度的对应是脆弱的、偶然的。我们实际上应该做的是让建立一个 UnitOfWorkLocal 变量成为可能。
查看英文原文: Confusing unit-of-work with threads - - - - - -
译者简介:孙向晖,儿子小名“豆豆”,常被人称为“豆豆他爹”。1998 年开始步入 IT 行业,现任浪潮软件质保中心副主任。专注于研究和实践 MDA/UP/UML/SCM 等相关技术在团队中的大规模应用,对产品化的软件项目管理、需求管理和配置管理略有心得。他的博客为 http://blog.csdn.net/xiaosun/ 。参与 InfoQ 中文站内容建设,请邮件至 china-editorial[at]infoq.com 。
评论