冻结的中间件与Rack冻结

142 阅读3分钟

带有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 方法被调用时被递增,这发生在每个请求中。如果你不熟悉什么是中间件,或者它们是如何被使用的,这些资源可能是有用的。

在单线程环境下运行该应用,会有如下输出:

request_count

你可以在单线程模式下运行这个是------。

Rack::Server.start :app => Counter.new, server: :puma, max_threads: 1, min_threads: 1

然而,在多线程环境中运行这个,会出现以下输出:

request_count

在多线程环境下,计数器不会递增。这被称为竞赛条件,当两个或更多的线程可以访问共享数据,并且它们试图同时改变数据时就会发生。因为线程调度算法可以在任何时候在线程之间进行交换,你不知道线程将尝试访问共享数据的顺序。因此,数据变化的结果取决于线程调度算法,即两个线程都是racing ,以访问/改变数据。

实现线程安全

当我们想在多线程环境中避免线程安全问题时,我们有一些选择:

  1. 不改变中间件中的状态
  2. 冻结中间件实例,以捕捉中间件中不是你自己写的线程安全问题。
  3. 使用来自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 方法。当我们运行这个程序,并多次点击/afreeze_app 方法将通过引发一个异常来通知我们有问题,否则你不会知道:

FrozenError: can't modify frozen #<Class:0x00007f9b0d1e95b0>

服务器将对所有其他的URL作出200的回应,因为我们没有在这些URL中修改实例变量。在内部freeze_app 方法在中间件实例上调用一个.freeze