1. 自动并发
Rails 自动允许同时执行各种操作。
当使用线程化 Web 服务器(如默认的 Puma)时,多个 HTTP 请求将同时处理,每个请求都提供其自己的控制器实例。
线程化的 Active Job 适配器(包括内置的 Async)也将同时执行多个作业。Action Cable 通道也以这种方式管理。
这些机制都涉及多个线程,每个线程管理某个对象(控制器、作业、通道)的唯一实例的工作,同时共享全局进程空间(例如类及其配置,以及全局变量)。只要您的代码不修改任何这些共享内容,它就可以基本忽略其他线程的存在。
本指南的其余部分描述了 Rails 使其“基本可忽略”的机制,以及具有特殊需求的扩展和应用程序如何使用它们。
2. 执行器
Rails 执行器将应用程序代码与框架代码分离:每当框架调用您在应用程序中编写的代码时,它都将被执行器封装。
执行器包含两个回调:to_run 和 to_complete。Run 回调在应用程序代码之前调用,Complete 回调在应用程序代码之后调用。
2.1. 默认回调
在默认的 Rails 应用程序中,执行器回调用于
- 跟踪哪些线程处于安全的自动加载和重新加载位置
- 启用和禁用 Active Record 查询缓存
- 将获取的 Active Record 连接返回到连接池
- 限制内部缓存的生命周期
在 Rails 5.0 之前,其中一些是由单独的 Rack 中间件类(例如 ActiveRecord::ConnectionAdapters::ConnectionManagement)处理的,或直接使用 ActiveRecord::Base.connection_pool.with_connection 等方法封装代码。执行器用一个更抽象的接口替换了这些。
2.2. 封装应用程序代码
如果您正在编写将调用应用程序代码的库或组件,则应使用对执行器的调用将其封装
Rails.application.executor.wrap do
# call application code here
end
如果您从一个长时间运行的进程中反复调用应用程序代码,您可能希望改用 重新加载器 进行封装。
每个线程在运行应用程序代码之前都应该被封装,所以如果您的应用程序手动将工作委托给其他线程(例如通过 Thread.new 或使用线程池的 Concurrent Ruby 功能),您应该立即封装该代码块
Thread.new do
Rails.application.executor.wrap do
# your code here
end
end
Concurrent Ruby 使用 ThreadPoolExecutor,有时会使用 executor 选项进行配置。尽管名称相似,但它们是无关的。
执行器是可安全重入的;如果它已在当前线程上激活,则 wrap 是一个空操作。
如果将应用程序代码封装在块中不切实际(例如,Rack API 使这变得有问题),您还可以使用 run! / complete! 对
Thread.new do
execution_context = Rails.application.executor.run!
# your code here
ensure
execution_context.complete! if execution_context
end
2.3. 并发
执行器将把当前线程置于 重新加载互锁 的 running 模式。如果另一个线程当前正在卸载/重新加载应用程序,此操作将暂时阻塞。
3. 重新加载器
与执行器一样,重新加载器也封装应用程序代码。如果执行器尚未在当前线程上激活,重新加载器将为您调用它,因此您只需要调用一个。这还保证了重新加载器所做的一切,包括其所有回调调用,都封装在执行器内部。
Rails.application.reloader.wrap do
# call application code here
end
重新加载器仅适用于长时间运行的框架级进程反复调用应用程序代码的情况,例如 Web 服务器或作业队列。Rails 会自动封装 Web 请求和 Active Job worker,因此您很少需要自行调用重新加载器。始终考虑执行器是否更适合您的用例。
3.1. 回调
在进入封装块之前,重新加载器将检查正在运行的应用程序是否需要重新加载——例如,因为模型的源文件已被修改。如果它确定需要重新加载,它将等待直到安全,然后执行重新加载,然后继续。当应用程序配置为无论是否检测到任何更改都始终重新加载时,重新加载将在块结束时执行。
重新加载器还提供了 to_run 和 to_complete 回调;它们在与执行器相同的点调用,但仅当当前执行已启动应用程序重新加载时。当不需要重新加载时,重新加载器将调用封装块,而不执行其他回调。
3.2. 类卸载
重新加载过程中最重要的部分是类卸载,其中所有自动加载的类都被移除,准备再次加载。这将紧接在 Run 或 Complete 回调之前发生,具体取决于 reload_classes_only_on_change 设置。
通常,需要在类卸载之前或之后执行额外的重新加载操作,因此重新加载器还提供了 before_class_unload 和 after_class_unload 回调。
3.3. 并发
只有长时间运行的“顶层”进程才应该调用重新加载器,因为如果它确定需要重新加载,它将阻塞直到所有其他线程都完成了任何执行器调用。
如果这发生在“子”线程中,而父线程在执行器内部等待,将导致不可避免的死锁:重新加载必须在子线程执行之前发生,但父线程在执行中时无法安全执行。子线程应该改用执行器。
4. 框架行为
Rails 框架组件也使用这些工具来管理自己的并发需求。
ActionDispatch::Executor 和 ActionDispatch::Reloader 是 Rack 中间件,它们分别用提供的执行器或重新加载器封装请求。它们会自动包含在默认的应用程序堆栈中。如果发生了任何代码更改,重新加载器将确保任何到达的 HTTP 请求都使用最新加载的应用程序副本进行服务。
Active Job 也使用重新加载器封装其作业执行,加载最新代码以执行队列中的每个作业。
Action Cable 改用执行器:由于 Cable 连接链接到类的特定实例,因此不可能为每个传入的 WebSocket 消息重新加载。但是,只有消息处理程序被封装;长时间运行的 Cable 连接不会阻止由新的传入请求或作业触发的重新加载。相反,Action Cable 使用重新加载器的 before_class_unload 回调来断开所有连接。当客户端自动重新连接时,它将与新版本的代码通信。
以上是框架的入口点,因此它们负责确保其各自的线程受到保护,并决定是否需要重新加载。其他组件只有在生成额外线程时才需要使用执行器。
4.1. 配置
重新加载器仅在 config.enable_reloading 为 true 且 config.reload_classes_only_on_change 也为 true 时检查文件更改。这些是 development 环境中的默认值。
当 config.enable_reloading 为 false(在 production 中,默认情况下)时,重新加载器仅是执行器的直通。
执行器总是有重要的工作要做,例如数据库连接管理。当 config.enable_reloading 为 false 且 config.eager_load 为 true(production 默认值)时,不会发生重新加载,因此不需要重新加载互锁。在 development 环境中使用默认设置时,执行器将使用重新加载互锁来确保安全地执行代码重新加载。
5. 重新加载互锁
重新加载互锁确保在多线程运行时环境中可以安全地执行代码重新加载。
只有当没有应用程序代码正在执行时,才能安全地执行卸载/重新加载:重新加载后,例如 User 常量可能指向不同的类。如果没有此规则,一个时机不佳的重新加载将意味着 User.new.class == User,甚至 User == User,可能为 false。
重新加载互锁通过跟踪当前正在运行应用程序代码的线程,并确保重新加载等待直到没有其他线程正在执行应用程序代码来解决此限制。