Programming clojure – Concurrency

Clojure的并发, 这兄弟写的比较系统, http://www.blogjava.net/killme2008/archive/2010/07/archive/2010/07/14/326027.html

 

Concurrency is a fact of life and, increasingly, a fact of software.

为什么需要并发? 
• Expensive computations may need to execute in parallel on multiple cores . 
• Tasks that are blocked waiting for a resource. 
• User interfaces need to remain responsive while performing longrunning tasks. 
• Operations that are logically independent are easier to implement

并发的问题是什么? 毫无疑问是状态同步 
如果大家各跑各的, 象pure FP, 那没有任何问题. 
但是当需要协调和同步的时候, 这个问题就变的很复杂. 
为了解决这个问题, Clojure提供了强大的并发库, 并把需要同步的状态分成4类, 分别用不同的API来handle

Clojure provides a powerful concurrency library, consisting of four APIs that enforce different concurrency models: refs, atoms, agents, and vars. 
• Refs manage coordinated, synchronous changes to shared state. 
• Atoms manage uncoordinated, synchronous changes to shared state. 
• Agents manage asynchronous changes to shared state. 
• Vars manage thread-local state.

 

6.1 The Problem with Locks

对于传统的语言, 状态默认是可变的(mutable), 所以当处于并发状况下, 对于所有可能被并发写的状态都要进行锁保护. 如果不小心遗漏了, 就会有很大的问题. 并且加锁并不是一件简单的事, Race conditions, Deadlocks等都很复杂... 可以说这种solution完全没有美感, 是否有更好的方案?

Yes, there is. In Clojure, immutable state is the default. Most data is immutable. The small parts of the codebase that truly benefit from mutability are distinct and must explicitly select one or more concurrency APIs. Using these APIs, you can split your models into two layers:

• A functional model that has no mutable state. Most of your code will normally be in this layer, which is easier to read, easier to test, and easier to run concurrently. 
• A mutable model for the parts of the application that you find more convenient to deal with using mutable state (despite its disadvantages).

当然这是FP核心优势之一, 妥善对待可变状态 
Clojure的好处是, 所有的状态默认是不可变的, 所以你只需要关注很小一部分真正需要mutable的状态(在默认可变的情况下, 你需要考虑full codebase, 任意一处遗漏都会带来很大的问题). 并且显式的把mutable state独立出来, 便于管理.

 

6.2 Refs and Software Transactional Memory

Most objects in Clojure are immutable. 
When you really want mutable data, creating a mutable reference (ref) to an immutable object. 
Clojure对象本身是不可变的, 如果需要可变数据, 做法是创建可变的reference, 可以指向不同的immutable object

Refs支持同步change state, 并可以支持同时在transaction里面change多个states

创建Ref

对于播放器应用, 歌曲本身是不变的对象, 但是当前播放歌曲, 是一个变化的状态 
创建ref, current-track


(def current-track (ref "Mars, the Bringer of War"))

读取contents of the reference, you can call deref (@ reader macro),


(deref current-track)
"Mars, the Bringer of War"

@current-track
"Mars, the Bringer of War"

修改Ref, ref-set


(ref-set reference new-value)

直接调用ref-set修改reference会报错, 这是比较好的保护机制, 防止误操作. 
在clojure可以使用transaction进行封装, 而对于一般语言必须使用lock, 这取决于实现方式.

Because refs are mutable, you must protect their updates. In many languages, you would use a lock for this purpose. 
In Clojure, you can use a transaction. Transactions are wrapped in a dosync:


(dosync & exprs)

(dosync (ref-set current-track "Venus, the Bringer of Peace"))
"Venus, the Bringer of Peace"

如上例子完成ref的切换, 歌曲对象本身没有发生变化.

Transactional Properties, 保证ACI, 不保证D 
Like database transactions, STM transactions guarantee some important properties: 
• Updates are atomic. 
• Updates are consistent. 
• Updates are isolated. 
Databases provide the additional guarantee that updates are durable
Because Clojure’s transactions are in-memory transactions, Clojure does not guarantee that updates are durable.

Transaction包含多条语句,


(def current-track (ref "Venus, the Bringer of Peace"))
(def current-composer (ref "Holst"))

(dosync
(ref-set current-track "Credo")
(ref-set current-composer "Byrd"))

Read-and-write, alter, commute

ref-set, 直接覆盖write, 比较简单 
更常用的是Read-and-write, 比如简单的累加器, 先要知道当前值是多少, 才能更新. 

(alter ref update-fn & args...)  ;ref = update-fn(ref, &args) 
messager的应用, 更新message

(defn add-message [msg]
  (dosync (alter messages conj msg)))

 

How STM Works: MVCC 
Clojure’s STM uses a technique called Multiversion Concurrency Control (MVCC), which is also used in several major databases. 
这个机制在DB里面也被广泛使用, 比如couchDB. 可以参考Practical Clojure - 简介 
同时clojure通过persistent data structures来保证MVCC的空间利用效率 
这就是为什么Clojure可以简单的实现transaction和保证ACI的原因, 因为所有的更新, 都只有在reference切换的时候才对外可见.

那么对于read-and-write肯定要解决冲突问题, 在read到write的过程中, 如果有其他transaction修改value, 怎么处理?

做法是当前的transaction会被强制retry, 从而保证transaction内的执行顺序

 

What if you don’t care that another transaction altered a reference out from under you in the middle of your transaction?

If another transaction alters a ref that you are trying to commute, the STM will not restart your transaction. Instead, it will simply run your commute function again, out of order.

如果可以容忍transaction过程中被其他的transaction alter reference, 那么就使用commute 
当发生冲突时, 不会restart整个transaction, 而只是从新run一下commute, 这就意味着commute更新什么时候执行都可以(否则就会有问题) 
这样便于STM系统进行reorder优化, Tradeoff, 换取更高并发程度, 更好性能 

(commute ref update-fn & args...)

在没有特别要求的情况下, 不要使用commute, 因为用alter逻辑一定是正确的, 而误用commute会导致错误

Validation to Refs, 增加约束条件

Validation function to the messages reference that guarantees that all messages have non-nil values for :sender and :text:


(def validate-message-list
    (partial every? #(and (:sender %) (:text %))))

(def messages (ref () :validator validate-message-list))

 

6.3 Use Atoms for Uncoordinated, Synchronous Updates

Atoms are a lighter-weight mechanism than refs. 
Where multiple ref updates can be coordinated in a transaction, atoms allow updates of a single value, uncoordinated with anything else.

Atoms就是轻量级的refs, 效率更高. 只允许对单个state进行更新. 
所以不需要transaction的封装, 减少开销

Atoms do not participate in transactions and thus do not require a dosync. To set the value of an atom, simply call reset!


(def current-track (atom "Venus, the Bringer of Peace"))

(reset! current-track "Credo") ;ref-set

为啥reset!和swap!都要加个!?


(swap! an-atom f & args) ;alter

(def current-track (atom {:title "Credo" :composer "Byrd"}))

(swap! current-track assoc :title "Sancte Deus")

 

6.4 Use Agents for Asynchronous Updates

Agent用于异步更新


(def counter (agent 0))

更新agent的命令, send. 很形象, 异步就是发过去就返回.


(send agent update-fn & args)

(send counter inc)

Notice that the call to send does not return the new value of the agent, returning instead the agent itself.

If you want to be sure that the agent has completed the actions you sent to it, you can call await or await-for:

(await & agents) 
(await-for timeout-millis & agents) 
 

Validating Agents and Handling Errors

Agent也可以和ref一样, 增加约束条件

(def counter (agent 0 :validator number?))

如果valid条件不满足会报错, 但对于所有异步而言, 怎样handle errors?

(send counter (fn [_] "boo"))

查看agent, 发现有错误

@counter 
java.lang.Exception: Agent has errors

通过agent-errors去查看具体错误信息.

(agent-errors counter) 
(#<IllegalStateException ...>)

最后通过clear-agent-errors, 清除error, 这样再查看agent, 就不会报错了

(clear-agent-errors counter) 
@counter 
0

 

Including Agents in Transactions

Transactions should not have side effects, because Clojure may retry a transaction an arbitrary number of times. 
However, sometimes you want a side effect when a transaction succeeds. 
Agents provide a solution. 
If you send an action to an agent from within a transaction, that action will be sent exactly once, if and only if the transaction succeeds.

因为冲突的原因, transaction会可能被retry很多次, 所以transaction里面不能有side effects, 比如写文件, IO等, 否则就会被执行很多遍, 大家都清楚clojure transaction的实现, 不可能象mysql那样有回滚的操作. 
所以agent作为一个很好的solution, 因为只有当transaction成功时, 才会发一次agent action.

这儿谈到side effects一般都是IO操作, 比较耗时, 所以用send发送不合适, 需要使用send-off

具体agent实现的原理参考这篇blog,http://www.blogjava.net/killme2008/archive/2010/07/archive/2010/07/archive/2010/07/19/326540.html

agent本身只是个普通的java对象,它的内部维持一个状态和一个队列 
然后线程会来队列里面取action, 并处理. 这些线程是放在线程池里面的 
两种线程池, 
固定大小的线程池(CPU核数加上2), 处理send发送的action 
没有大小限制(取决于内存)的线程池, 处理send-off发送的action 
最重要的是, 这些线程池是所有线程公用的, 所以耗时的用send发送, 会导致其他agent的更新被block. 
实现并不复杂, 典型的producer-consumer模式

 

6.5 Managing Per-Thread State with Vars

可以参考这个blog 
http://www.blogjava.net/killme2008/archive/2010/07/archive/2010/07/archive/2010/07/archive/2010/07/23/326976.html

var的binding有几种方式,

root binding, 用def定义, 被所有线程共享

local binding, 用let定义, 即静态lexical binding, 因其只在lexical scope内起作用

thread-local dynamic binding, 用binding定义, 线程内的binding, 不局限于lexical scope

通过例子来看let和binding的不同,


(def foo 1)

(defn print-foo [] (println foo))

 

(let [foo 2] (print-foo)) ;不在同一个lexical scope中, 所以let不起作用
1

(binding [foo 2] (print-foo)) ;在同一个线程中, binding起作用
2

这是由于let的绑定是静态的,它并不是改变变量foo的值,而是用一个词法作用域的foo“遮蔽”了外部的foo的值. 
而binding则是在变量的root binding之外在线程的ThreadLocal内存储了一个绑定值, 所以只要在该线程内就可以看到, 而不限于lexical scope

 

binding用于临时的修改function逻辑

只在该线程范围内, 给fib加上memorize


user=> (defn fib [n]
         (loop [ n n r 1]
            (if (= n 1)
                r
                (recur (dec n) (* n r))))) 

user=> (binding [fib (memoize fib)]
                (call-fibs 9 10))
3991680

Used occasionally, dynamic binding has great power. But it should not become your primary mechanism for extension or reuse. Functions that use dynamic bindings are not pure functions and can quickly lose the benefits of Clojure’s functional style. 
慎用, not pure, 会引入复杂性, 和FP的初衷不符合.

 

Working with Java Callback APIs

Several Java APIs depend on callback event handlers. 
XML parsers such as SAX depend on the user implementing a callback handler interface.

These callback handlers are written with mutable objects in mind. Also, they tend to be single-threaded. 
In Clojure, the best way to meet such APIs halfway is to use dynamic bindings. This will involve mutable references that feel almost like variables, but because they are used in a single-threaded setting, they will not present any concurrency problems.

Clojure provides the set! special form for setting a thread-local dynamic binding: 
(set! var-symbol new-value)

set! should be used rarely. In fact, the only place in the entire Clojure core that uses set! is the Clojure implementation of a SAX ContentHandler.

Clojure可以用set!来修改thread-local dynamic binding, 但是应该尽量慎用set!, 仅仅用于对Java Callback的处理


本文章摘自博客园,原文发布日期:2013-02-20

时间: 2024-09-19 13:43:11

Programming clojure – Concurrency的相关文章

Programming clojure – Recursion and Lazy-seq

5.1 Functional Programming Concepts The Six Rules Although the benefits of FP are compelling, FP is a wholesale change from the imperative programming style that dominates much of the programming world today. Plus, Clojure takes a unique approach to

Programming Clojure - Unifying Data with Sequences

In Clojure, all these data structures can be accessed through a single abstraction: the sequence (or seq).  A seq (pronounced "seek") is a logical list, the seq is an abstraction that can be used everywhere.   Collections that can be viewed as s

Programming clojure – Multimethods

Multimethods, 其实就是FP基础里面说的, Pattern Matching, 说白了, 就是根据不同的参数定义不同的逻辑.  我首先想到的是函数重载, http://www.cnblogs.com/skynet/archive/2010/09/05/1818636.html 参数个数重载, 对于这种clojure函数天然支持, 如下可以定义多组参数列表 (defmacro and ([] true) ([x] x) ([x & rest] `(let [and# ~x] (if a

Clojure - 基本语法

Installing Clojure Clojure is an open-source project hosted at github.com. git clone https://github.com/clojure/clojure.git This will download the code from the master branch into the clojure directory in your workspace. Clojure is a Java project, an

RDBMS vs. NoSQL &amp; Clojure概述

RDBMS vs. NoSQL 合作还是竞争 数据库要解决的主要问题 不管是RDBMS还是NoSQL,在大的方面他们都属于数据库这个范畴,这个范畴之内所要面临的一些共同问题有哪些呢.下面的图是一个大致的归纳. 从图中可以看出,一个数据库系统主要解决以下几个问题: 数据的存储,即要存入哪些数据到系统中,当然在data definition这一块,有schema和no schema两种,说白了就是数据格式和数据关系的定义问题 完成了data definition,那么接下来自然要发生的事情就是将数据

技术书单整理

算法 算法导论 Introduction to Algorithms, Second Edition, by Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein 算法概论  Algorithms, S. Dasgupta, C. H. Papadimitriou, and U. V. Vazirani Python Algorithms-Mastering Basic Algorithms in

常见的后台实践

题外 objc.io 是一个非常棒的iOS进阶学习的网站,上面有很多超赞的学习资源和例子.最近我和 @方一雄,@answer-huang 和社区的另外几名小伙伴在主持做一个 objc.io 的译文整理汇总和后续翻译跟进的项目,我暂时略自我狂妄地把它叫做 objc中国(objccn.io) 项目,希望它能给现在已经很红火的中国objc社区锦上添花.现在上面已经有一些文章,您可以时不时地访问我们的首页来查看新的动态.如果有兴趣,也可以考虑加入我们,来为中国objc社区的发展贡献一点力量. 对objc

软件事务内存导论(二)软件事务内存

1.1    软件事务内存 将实体与状态分离的做法有助于STM(软件事务内存)解决与同步相关的两大主要问题:跨越内存栅栏和避免竞争条件.让我们先来看一下在Clojure上下文中的STM是什么样子,然后再在Java里面使用它. 通过将对内存的访问封装在事务(transactions)中,Clojure消除了内存同步过程中我们易犯的那些错误(见 <Programming Clojure>[Hal09]和<The Joy of Clojure>[FH11]).Clojure会敏锐地观察和

Pratical Cljr – loop/recur

Programming Clojure这块写的过于简单, Pratical Clojure写的还不错, 这本书在某些章节写的深度不错, 讨论why, 而不是仅仅how, 故摘录 首先, Clojure是不提供直接的looping语法的!!! 这个理解很重要 因为looping是imperative的概念, 对于FP, 需要改变思维的方式, 用递归来解决同样的问题, 所以下面也说对于从imperative 转来的程序员, 习惯从递归(recursive)的角度思考问题, 是很大的挑战. It wi