Scala对象和线程安全
我是
Scala的新手.
我试图弄清楚如何使用Scala对象(也称为单例)中的函数来确保线程安全 从我到目前为止所读到的内容来看,我似乎应该保持对函数范围(或下面)的可见性,并尽可能使用不可变变量.但是,我没有看到违反线程安全的例子,所以我不确定应该采取什么其他预防措施. 有人能指出我对这个问题的一个很好的讨论,最好举例说明线程安全被违反的地方吗? 解决方法
天啊.这是一个很大的话题.
Here’s a Scala-based intro to concurrency和Oracle的Java课程实际上有一个非常好的
intro as well.这是一个简短的介绍,它激发了为什么并发读取和写入共享状态(其中Scala对象是特定的特定情况)是一个问题并提供常见解决方案的快速概述.
在线程安全和状态变异方面存在两类(基本相关)问题: > Clobbering(缺失)写道 让我们依次看看这些中的每一个. 第一次破坏写道: object WritesExample { var myList: List[Int] = List.empty } 想象一下,我们有两个线程同时访问WritesExample,每个线程执行以下updateList def updateList(x: WritesExample.type): Unit = WritesExample.myList = 1 :: WritesExample.myList 你可能希望当两个线程都完成时,WritesExample.myList的长度为2.不幸的是,如果两个线程在另一个线程完成写入之前读取WritesExample.myList,则可能不是这种情况.如果两个线程都读取WritesExample.myList它为空,则两者都将写回长度为1的列表,其中一个写入覆盖另一个,因此最后WritesExample.myList的长度为1.因此,我们已经有效地丢失了我们应该执行的写入.不好. 现在让我们看看不准确的读取. object ReadsExample { val myMutableList: collection.mutable.MutableList[Int] } 再一次,假设我们有两个线程同时访问ReadsExample.这次它们每个都重复执行updateList2. def updateList2(x: ReadsExample.type): Unit = ReadsExample.myMutableList += ReadsExample.myMutableList.length 在单线程上下文中,当重复调用时,您会期望updateList2简单地生成递增数字的有序列表,例如,1,2,3,4 ……不幸的是,当多个线程同时使用updateList2访问ReadsExample.myMutableList时,可能在读取ReadsExample.myMutableList.length和最终持久写入之间,ReadsExample.myMutableList已被另一个线程修改.所以理论上你可以看到像0,1这样的东西,或者如果一个线程需要更长的时间来写另一个0,1(其中较慢的线程在另一个线程已经访问后最终写入列表的位置)并写入列表三次). 发生的事情是读取不准确/过时;更新的实际数据结构与读取的数据结构不同,即在事物中间从您下面更改.这也是一个巨大的错误来源,因为您可能希望保留的许多不变量(例如,列表中的每个数字与其索引完全对应,或者每个数字仅出现一次)保持在单线程上下文中,但在并发上下文中失败. 既然我们已经解决了一些问题,那么让我们深入研究一些解决方案.你提到了不变性,所以我们首先谈谈这个问题.您可能会注意到,在我的clobbering写入示例中,我使用了不可变数据结构,而在我不一致的读取示例中,我使用了可变数据结构.那是故意的.从某种意义上说,它们彼此是双重的. 对于不可变数据结构,您不能在上面列出的意义上进行“不准确”读取,因为您从不改变数据结构,而是将数据结构的新副本放在同一位置.数据结构不能从你下面改变,因为它不能改变!但是,通过将数据结构的版本放回其原始位置而不包含先前由另一个进程进行的更改,您可能会丢失写入过程. 另一方面,使用可变数据结构,您不会丢失写入,因为所有写入都是数据结构的就地突变,但您最终可以执行对数据结构的写入,该数据结构的状态与您分析它时的状态不同以形成写. 如果它是一种“挑选毒药”的场景,为什么你经常听到建议使用不可变数据结构来帮助实现并发?即使写入丢失,良好的不可变数据结构也可以更容易地确保有关被修改状态的不变量.例如,如果我重写了ReadsList示例以使用不可变List(而不是var),那么我可以自信地说列表的整数元素将始终对应于列表的索引.这意味着您的程序进入不一致状态的可能性要小得多(例如,不难想象,当并发变异时,一个天真的可变集实现可能会以非唯一元素结束).事实证明,处理并发的现代技术通常非常适合处理丢失的写入. 让我们看一下处理共享状态并发的一些方法.在他们的心中,他们都可以概括为各种序列化读/写对的方法. 锁(a.k.a.直接尝试序列化读/写对):这通常是您首先会听到的作为处理并发的基本方法.每个想要访问状态的进程都会锁定它.现在,任何其他进程都不会访问该状态.然后,该进程将写入该状态,并在完成时释放锁定.其他流程现在可以自由重复该流程.在我们的WritesExample中,updateList将在执行和释放锁之前首先获取锁;这将阻止其他进程读取WritesExample.myList直到写入完成,从而阻止他们看到导致破坏写入的myList的旧版本(请注意,这是更复杂的锁定过程,允许同时读取,但让我们坚持使用现在的基础知识). 锁通常不能很好地扩展到多个状态.对于多个锁,通常您需要按特定顺序获取和释放锁,否则您最终可能会结束deadlocking or livelocking. 最初链接的Oracle和Twitter文档对这种方法有很好的概述. 描述你的行为,不要执行它(也就是建立你的行为的串行表示并让其他人处理它):你不是直接访问和修改状态,而是描述如何执行此操作然后将其交给某人的操作否则实际执行动作.例如,您可以将消息传递给对象(例如Scala中的actor),这些对象将这些请求排队,然后在某个内部状态上逐个执行它们,它永远不会直接暴露给其他任何人.在actor的特定情况下,通过消除显式获取和释放锁的需要,这改善了锁的情况.只要您在一个对象中封装您需要立即访问的所有状态,消息传递就可以很好地进行.当你在多个对象之间分配状态时,Actor会崩溃(因此在这个范例中,这是非常不鼓励的). Akka actors是Scala中的一个很好的例子. 事务(也称暂时隔离其他人的一些读写操作,让隔离系统为您序列化事物):将所有读/写包裹在事务中,确保在读写过程中您的世界视图与其他任何事物隔离开来变化.通常有两种方法可以实现这一目标.您可以采用类似于锁的方法,在事务运行时阻止其他人访问数据,或者每当您检测到共享状态发生更改并从而丢弃任何进度时从一开始就重新启动事务已经制作(出于性能原因通常是后者).一方面,与锁和演员不同,交易可以非常好地扩展到不同的状态.只需将所有访问权限包含在交易中,就可以了.另一方面,您的读取和写入必须是无副作用的,因为它们可能被丢弃并重试多次,并且您无法真正撤消大多数副作用. 如果你真的不走运,虽然你通常不能真正实现与事务的良好实现的死锁,但是长期存在的事务可能会被其他短期事务中断,这样它就会被抛弃并重新执行,而实际上从未实际成功(相当于活锁).实际上,您放弃了对序列化订单的直接控制,并希望您的交易系统明智地订购事物. Scala’s STM library就是这种方法的一个很好的例子. 删除共享状态:最终的“解决方案”是完全重新思考问题并尝试考虑您是否真的需要可写的全局共享状态.如果您不需要可写共享状态,那么并发问题就会完全消失! 生活中的一切都是权衡取舍,并发也不例外.在考虑并发性时,首先要了解您拥有的状态以及您希望保留的有关该状态的不变量.然后使用它来指导您决定使用哪种工具来解决问题. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |