内容简介:多核或多CPU使得并发的要求更加迫切,传统使用锁来管理并发,遗憾的是已被证明不太理想,因为它们经常导致死锁、饥饿、竞争和容易出错。在这篇文章中,我们将探讨如何利用Clojure的软件事务存储器(STM)来更好地利用现代硬件。并发难题的核心是状态问题,实质上,大多数编程语言都会使用多线程共享数据更新的方式管理状态。
多核或多CPU使得并发的要求更加迫切,传统使用锁来管理并发,遗憾的是已被证明不太理想,因为它们经常导致死锁、饥饿、竞争和容易出错。在这篇文章中,我们将探讨如何利用Clojure的软件事务存储器(STM)来更好地利用现代硬件。
Clojure的方式
并发难题的核心是状态问题,实质上,大多数编程语言都会使用多线程共享数据更新的方式管理状态。
Clojure提供不可变数据结构和语言级语义来解决这个状态的并发更新问题,通过对状态的托管引用实现安全并发,这是由称为软件事务内存(STM)的控制机制提供的。
STM是一种软件级设计,用于控制对内存中共享数据的访问,它的工作方式类似于数据库事务(但限于控制对内存数据的访问,而不是持久的数据存储)。
STM事务确保以原子方式完成状态更改 ,与事务关联的所有更改,一旦过程中有失败,则全部回滚已经做出的更改,此外,必须实现一致地进行更改,这意味着如果更改无法满足某些指定的约束,则事务也会失败,最后,更改必须以隔离的方式发生,对事务中的数据所做的所有更改仅对当前的线程是可见的。
Clojure STM使用多版本控制并发,它在事务开始之前获取数据的快照,对快照所做的更改对于外部世界(其他线程)是不可见的,直到提交更改成功完成事务。
Clojure提供了ref(reference)结构,用于管理允许同步和
协调更改的数据,对refs的所有修改必须在STM事务中发生,该事务是使用dosync语法指定的。
银行业务情景
让我们考虑典型的银行账户处理方案,在一个帐户甚至多个帐户上进行大量交易。但是,该帐户预计必须在所有这些交易中保持一致的平衡。
让我们创建一个函数,使用ref创建一个帐户,指定帐户名、帐号和初始起始余额。
(defn create-account [ account-name account-number balance ]
( ref { :account-name account-name
:account-number account-number
:balance balance
} :validator allowable-balance? ) )
(defn allowable-balance? [ { :keys [ balance ]}]
( or ( > balance 0 )
(throw (IllegalStateException. "Balance cannot be less than zero" ) )
))
此函数返回一个ref,它表示调用时的帐户。我们还添加了
验证器功能,以确保帐户余额始终大于零。(毕竟我们是银行。)
现在,让我们创建三个帐号。
(def first-account ( create-account "Robert" 300045 120 ) )
( def second-account (create-account "Mike" 30046 500 ) )
( def third-account (create-account "Rose" 30047 200 ) )
这将分别创建具有指定详细信息的三个帐户引用,现在,假设Mike决定做慈善事业并指示银行从他的账户转账到Robert账户200美元。
要做到这一点,必须做两件事:首先,我们需要将Mike的账户扣除200美元,然后将其存入Robert的账户。对于外界来说,这两个操作必须同时发生(原子),转移需要以这种一种方式进行:即从任何外部观察者的角度来看,被转移的资金一次只能在一个账户中。
为此,Clojure要求使用dosync在事务中实现此操作。
让我们定义一个名为make-transfer的函数,它将指定数量的资金从一个帐户转移到另一个帐户。
(defn make-transfer [ from-account to-account transfer-amount ] (dosync (alter from-account update-in [ :balance ] - transfer-amount ) (alter to-account update-in [ :balance ] + transfer-amount )))
现在,让我们发出转移:
(transfer second-account first-account 200 )
(println "first account -> " @first-account "Second account -> " @second-account )
这是我屏幕上显示的输出。
first account -> {:account-name Robert, :account-number 300045, :balance 320} Second account -> {:account-name Mike, :account-number 30046, :balance 300}
但是,下面这样的转移失败了:
first account -> {:account-name Robert, :account-number 300045, :balance 320} Second account -> {:account-name Mike, :account-number 30046, :balance 300}
出现错误:
CompilerException java.lang.IllegalStateException: Balance cannot be less than zero, compiling:(~/clojure/concurrency.clj:44:1)
我们可以在不同的线程上执行这两个操作,如下所示:
(future (make-transfer second-account first-account 100 ) ) (future (make-transfer second-account third-account 600) )
理解alter函数
alter函数用于原子地更新一个Ref对象,它由提供的ref和一个函数来调用,该函数将ref作为参数并返回一个值,返回值将用作ref的新值。因为Clojure允许在多个线程中同时执行多个事务,所以传给alter函数的ref的快照值如果与ref的当前值相同时才允许事务提交,否则所有更改都将是丢弃,并使用ref的最新值重试事务。这种无锁方法允许线程自由执行事务而不会被阻塞,包括那些只能读取的事务。
commute函数
为了限制alter function在提交时需要的重试次数,可以在函数应用程序的顺序无关紧要的情况下使用commute函数,如果在两个事务结束时,哪个线程是否首先提交本身无所谓,可以采用这个函数。
因此,银行转账的顺序并不重要的情况下,可以像下面这样实施转移功能:
(defn make-transfer-2 [ from-account to-account transfer-amount ] (dosync (commute from-account update-in [ :balance ] - transfer-amount ) (commute to-account update-in [ :balance ] + transfer-amount ) ))
结论
Clojure是一种动态类型的函数式语言,它的设计考虑了并发性。
以上所述就是小编给大家介绍的《Clojure软件事务存储器》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 针对亚马逊云存储器S3 BUCKET的渗透测试
- 【Logisim实验】构建立即数-随机存储器-寄存器的传送
- 用go语言来做嵌入式-读写铁电存储器
- 网关 Spring-Cloud-Gateway 源码解析 —— 路由(1.3)之 RouteDefinitionRepository 存储器
- 块存储、文件存储、对象存储三者之比较
- 云原生存储详解:容器存储与 K8s 存储卷
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。