带有Rack freeze的中间件
我最喜欢的消遣之一是翻阅我喜欢的库的GitHub问题。其中一个是Rack gem,我在那里发现了一个名为 "中间件应该默认冻结"的问题。我有几个问题。究竟什么是冻结的中间件? 为什么要这样做?
例子:网络请求计数
作为一个简单的第一个例子,让我们考虑一个Rack中间件,它计算服务器收到的请求数。一个非常简单的(和坏的)实现可能是这样的:
class Counter
def initialize
@counter = 0
end
def call(_env)
counter = @counter
sleep 1
counter += 1
@counter = counter
[200, { 'Content-Type' => 'text/html' }, ["#{@counter}"]]
end
end
@counter 实例变量在每次call 方法被调用时被递增,这发生在每个请求中。如果你不熟悉什么是中间件,或者它们是如何被使用的,这些资源可能是有用的。
在单线程环境下运行该应用,会有如下输出:

你可以在单线程模式下运行这个是------。
Rack::Server.start :app => Counter.new, server: :puma, max_threads: 1, min_threads: 1
然而,在多线程环境中运行这个,会出现以下输出:

在多线程环境下,计数器不会递增。这被称为竞赛条件,当两个或更多的线程可以访问共享数据,并且它们试图同时改变数据时就会发生。因为线程调度算法可以在任何时候在线程之间进行交换,你不知道线程将尝试访问共享数据的顺序。因此,数据变化的结果取决于线程调度算法,即两个线程都是racing ,以访问/改变数据。
实现线程安全
当我们想在多线程环境中避免线程安全问题时,我们有一些选择:
- 不改变中间件中的状态
- 冻结中间件实例,以捕捉中间件中不是你自己写的线程安全问题。
- 使用来自concurrent-ruby gem的数据结构。
我们如何做到这一点呢?
例子:网络请求线程安全计数
class Counter
def initialize
@atomic = Concurrent::AtomicReference.new(0)
end
def call(_env)
@atomic.update { |v| v + 1 }
[200, { 'Content-Type' => 'text/html' }, ["{@atomic}"]]
end
end
atomic ,它意味着块的内容被执行到完成,而其他线程无法读取/修改该值(注意,这与mutex不同)。多个线程试图改变同一个AtomicReference 对象,不会使其最终处于不一致的状态。
冻结中间件实例
Rack中间件只在进程的第一个请求时被初始化。所以任何实例变量的行为都像类变量一样,在call() 中修改它们并不是线程安全的。有必要将中间件dup ,使其成为线程安全的。一个中间件应该被冻结,以避免处理并发请求的潜在问题。Rack最近引入了一个freeze_app 方法来冻结中间件实例。这方面的一个使用例子是:
use (Class.new do
def call(env)
@a = 1 if env['PATH_INFO'] == '/a'
@app.call(env)
end
freeze_app
end)
在这个例子中,当我们点击/a url时,我们正在初始化一个实例变量到1 。我们在中间件中调用freeze_app 方法。当我们运行这个程序,并多次点击/a ,freeze_app 方法将通过引发一个异常来通知我们有问题,否则你不会知道:
FrozenError: can't modify frozen #<Class:0x00007f9b0d1e95b0>
服务器将对所有其他的URL作出200的回应,因为我们没有在这些URL中修改实例变量。在内部freeze_app 方法在中间件实例上调用一个.freeze 。