7.5 使用代理管理状态
为了在多线程程序中妥善地管理可修改的状态,Clojure提供了代理(agent)。每个代理都负责管理一个包含其状态的对象。在大多数情况下,状态对象都存储在不可修改的Clojure数据结构中。要修改特定代理的状态,可向它发送一个操作(action)。操作是普通的非阻塞函数,由代理执行;而操作的返回值将取代代理的当前状态。

代理运行在Clojure管理的一个内部线程池提供的线程中,它们可随时做出响应;处理操作时Clojure不会加锁。任何时候,其他代码都可安全地读取代理的状态,而不管这些代码运行在哪个线程中。操作是以异步方式发送给代理的,代理所在的线程收到操作后将执行它,并将代理的状态设置为操作的结果。
可给代理添加验证器(validator)。验证器是一个函数,对新状态进行验证,进而接受或拒绝。如果给代理添加了验证器,操作将首先发送给验证器;如果验证器接受了新状态,操作的响应将成为新的代理状态;如果验证器拒绝了操作,操作的返回值将被丢弃,并引发异常。

还可给代理添加监视器(watcher)。如果添加了监视器,将在成功地修改了代理的状态后调用它。引发异常后,代理将缓存错误,并停止接受操作,直到被重启。
由于代理运行在线程池提供的线程中,因此只要有代理在运行,JVM就不会关闭。为确保资源得以正确地释放,必须使用Clojure提供的函数来妥善地关闭代理。
代理示例
要创建代理,可使用函数agent。下面来创建一个存储发货单状态的虚构代理。为简单起见,我们只存储客户名称以及一个表明发货单是否已处理的布尔标志:
(def invoice-agent (agent (hash-map :name nil, :isProcessed false)))
这创建了一个代理,并将指向它的引用存储到变量invoice-agent中。为了存储这个代理的状态,我们使用了一个不可修改的映射,其中包含客户名称(最初为空)和一个指出发货单是否已处理的标志(最初为false)。
下面来给这个代理创建两个操作:
- 更新客户名称的操作
update-customer-name; - 更新发货单标志
isProcessed的操作update-processed。
前面说过,操作是返回代理新状态的普通函数。这些操作将代理的当前状态作为输入,它们的代码如下:
(defn update-customer-name [state name] (assoc state :name name))(defn update-processed [state flag] (assoc state :isProcessed flag))
这两个函数都自动从代理那里获取其当前状态,它们的第二个参数分别是新的客户名称以及表明发货单是否处理过的标志的新值。这两个函数都返回存储代理状态的散列映射的副本。有关散列映射和函数assoc,请参阅前面介绍散列映射的一节。这个返回值将成为代理的新状态。
下面通过设置客户名称来测试这个代理:
(send invoice-agent update-customer-name "Your Name")
函数send的第一个参数指定了要将操作发送给哪个代理实例,第二个参数是要发送的操作,而其他所有参数都是操作的参数[但操作的第一个参数(在这里,两个操作的第一个参数都是state)不是在这里指定的]。操作及其参数被发送给代理,但函数send在代理处理操作前就已返回。由于代理运行在独立的线程中,因此无法预测操作将在何时被处理。然而,由于这并不是一个有众多线程同时运行的繁忙应用程序,因此操作很可能在你按下回车后就能得到处理。函数send返回相应的代理实例,但由于我们有指向这个代理实例的引用(变量invoice-agent),因此忽略这个返回值。
要检查代理的当前状态,可输入指向它的变量并加上前缀@:
@invoice-agent
这个表达式的结果应为{:name "Your Name", :isProcessed false}。
这说明前述代码管用!下面来添加一个验证器,它在发货单处理过后拒绝将客户名称设置为nil的操作。验证器函数获取新的状态,并在这种修改可接受时返回true,在要拒绝修改时返回false:
(defn validator-invoice [state](if(and(get state :isProcessed)(clojure.string/blank? (get state :name)))falsetrue))
函数blank?来自clojure.string库,它在指定的字符串为nil时返回true,否则返回false。下面将这个验证器添加到代理中,为此可使用函数set-validator!:
(set-validator! invoice-agent validator-invoice)
接下来,将已处理标志改为true,过段时间后再查看当前状态:
(send invoice-agent update-processed true)@invoice-agent
你将看到发货单已处理过:{:name "Your Name", :isProcessed true}。
下面尝试将客户名称设置为nil,看看这个验证器是否管用:
(send invoice-agent update-customer-name nil)@invoice-agent
你将看到客户名称并没有被重置为nil,因为验证器禁止这样做。要查看代理是否出现了错误,可使用函数agent-error:
(agent-error invoice-agent)
操作被验证器拒绝时,代理系统将自动引发栈跟踪(traceback),因此上述代码返回的内容如下(为简洁起见做了删节):
#error {:cause "Invalid reference state"...}
注意,除非重启,否则这个代理不会再接受新的操作。在此期间,收到的操作存储在缓冲区。要重启代理,只需调用函数restart-agent即可:
(restart-agent invoice-agent (hash-map :name nil, :isProcessed false):clear-actions true)
重启代理时,必须指定其状态。要让代理对其在没有响应期间存储到缓冲区的操作进行处理,可在可选的关键字:clear-actions后面指定false。
要妥善地关闭代理系统,可执行函数shutdown-agents:
(shutdown-agents)
这样代理线程将停止,不再对操作做出响应。
