我邀请您阅读Jesse Storimer的系列文章“没有人理解GIL”。这可能有助于更好地理解一些MRI内部机制。
我还发现Ruby并发编程实践很有趣。其中有一些测试并发性的示例。
编辑:此外,我可以推荐文章“移除config.threadsafe!”。这可能与Rails 4无关,但它解释了配置选项,其中之一可以用于允许并发。
让我们讨论一下您的问题的答案。
您可以使用多个线程(使用MRI),即使使用Puma。 GIL确保同一时间只有一个线程是活动的,这是开发人员称为限制性的约束条件(由于没有真正的并行执行)。请记住,GIL不能保证线程安全。这并不意味着其他线程没有运行,它们正在等待它们的轮到。它们可以交错执行(这些文章可以帮助更好地理解)。
让我澄清一些术语:工作进程,线程。进程在单独的内存空间中运行,并且可以为多个线程提供服务。同一进程的线程在共享内存空间中运行,即其进程的内存空间。在线程中,我们指的是Ruby线程,而不是CPU线程。
关于您的问题的配置和您分享的GitHub repo,请注意适当的配置(我使用Puma)是设置4个工作进程和1到40个线程。想法是一个工作进程为一个选项卡服务。每个选项卡最多发送10个请求。
所以让我们开始吧:
我在虚拟机上使用Ubuntu。因此,首先我在我的虚拟机设置中启用了4个核心(还有其他设置,我认为它可能有帮助)。我可以在我的计算机上验证这一点。所以我采用了这个设置。
Linux command --> lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 4
On-line CPU(s) list: 0-3
Thread(s) per core: 1
Core(s) per socket: 4
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 69
Stepping: 1
CPU MHz: 2306.141
BogoMIPS: 4612.28
L1d cache: 32K
L1d cache: 32K
L2d cache: 6144K
NUMA node0 CPU(s): 0-3
我使用了你分享的GitHub项目,并对其进行了轻微修改。我创建了一个名为puma.rb
的Puma配置文件(将其放在config
目录中),其内容如下:
workers Integer(ENV['WEB_CONCURRENCY'] || 1)
threads_count = Integer(ENV['MAX_THREADS'] || 1)
threads 1, threads_count
preload_app!
rackup DefaultRackup
port ENV['PORT'] || 3000
environment ENV['RACK_ENV'] || 'development'
on_worker_boot do
end
默认情况下,Puma使用1个worker和1个thread启动。您可以使用环境变量来修改这些参数。我就这样做了:
export MAX_THREADS=40
export WEB_CONCURRENCY=4
为了使用此配置启动Puma,我输入了以下命令:
bundle exec puma -C config/puma.rb
在Rails应用程序目录中。
我用四个选项卡打开浏览器来调用应用程序的URL。
第一个请求开始于15:45:05,最后一个请求大约在15h49:44左右。这是4分39秒的经过时间。还可以在日志文件中看到请求ID按非排序顺序排列。(见下文)
GitHub项目中的每个API调用休眠15秒。我们有四个选项卡,每个选项卡有10个API呼叫。在严格串行模式下,最长的经过时间为600秒,即10分钟。
理论上的理想结果是全部并行,并且经过时间与15秒不远,但我根本没有期望那样。
我不确定会有什么样的结果,但我仍然感到积极(考虑到我是在虚拟机上运行的,MRI受到GIL和其他因素的限制)。这个测试的经过时间少于最长的经过时间的一半(在严格串行模式下),我们把结果缩短了一半以上。
编辑:我阅读了关于Rack::Lock的更多内容,其在每个请求周围包裹了互斥锁(上述第三篇文章)。我发现选项config.allow_concurrency = true可以节省时间。小小的警告是要增加连接池(尽管请求不查询数据库,但必须相应地设置),最大线程数是一个很好的默认值。在这种情况下为40。
我用jRuby测试了应用程序,实际经过时间为2分钟,allow_concurrency=true。
我用MRI测试了应用程序,实际经过时间为1分47秒,allow_concurrency=true。这让我非常惊讶。这真让我感到惊讶,因为我预计MRI比JRuby慢。它不是。这使我对MRI和JRuby之间的速度差异的广泛讨论产生了疑问。
现在观察不同选项卡的响应更“随机”了。有时候,第3或第4个选项卡会在第1个选项卡之前完成,尽管我先请求了第1个选项卡。
我认为,由于没有竞争条件,测试似乎是正常的。然而,如果您在真实世界应用程序中将config.allow_concurrency=true设置为true,我不确定是否会产生整个应用程序的后果。
请随意查看并让我知道读者可能拥有的任何反馈。我仍然有我的克隆机。如果您有兴趣,请告诉我。
按顺序回答您的问题:
- 我认为您的示例结果是有效的。然而,在并发方面,最好使用共享资源进行测试(例如第二篇文章所述)。
- 关于您的声明,正如本答案开头提到的那样,MRI是多线程的,但受GIL限制一次只能有一个活动线程。这引出了一个问题:对于MRI来说,使用更多进程和更少线程来进行测试是否更好?我不太清楚,第一印象会是不或者没有太大差别。也许有人能够解释一下。
- 我认为您的示例很好。只需要进行一些微小的修改即可。
附录
Rails 应用程序日志文件:
**config.allow_concurrency = false (by default)**
-> Ideally 1 worker per core, each worker servers up to 10 threads.
[3045] Puma starting in cluster mode...
[3045] * Version 2.11.2 (ruby 2.1.5-p273), codename: Intrepid Squirrel
[3045] * Min threads: 1, max threads: 40
[3045] * Environment: development
[3045] * Process workers: 4
[3045] * Preloading application
[3045] * Listening on tcp://0.0.0.0:3000
[3045] Use Ctrl-C to stop
[3045] - Worker 0 (pid: 3075) booted, phase: 0
[3045] - Worker 1 (pid: 3080) booted, phase: 0
[3045] - Worker 2 (pid: 3087) booted, phase: 0
[3045] - Worker 3 (pid: 3098) booted, phase: 0
Started GET "/assets/angular-ui-router/release/angular-ui-router.js?body=1" for 127.0.0.1 at 2015-05-11 15:45:05 +0800
...
...
...
Processing by ApplicationController#api_call as JSON
Parameters: {"t"=>"15?id=9"}
Completed 200 OK in 15002ms (Views: 0.2ms | ActiveRecord: 0.0ms)
[3075] 127.0.0.1 - - [11/May/2015:15:49:44 +0800] "GET /api_call.json?t=15?id=9 HTTP/1.1" 304 - 60.0230
**config.allow_concurrency = true**
-> Ideally 1 worker per core, each worker servers up to 10 threads.
[22802] Puma starting in cluster mode...
[22802] * Version 2.11.2 (ruby 2.2.0-p0), codename: Intrepid Squirrel
[22802] * Min threads: 1, max threads: 40
[22802] * Environment: development
[22802] * Process workers: 4
[22802] * Preloading application
[22802] * Listening on tcp://0.0.0.0:3000
[22802] Use Ctrl-C to stop
[22802] - Worker 0 (pid: 22832) booted, phase: 0
[22802] - Worker 1 (pid: 22835) booted, phase: 0
[22802] - Worker 3 (pid: 22852) booted, phase: 0
[22802] - Worker 2 (pid: 22843) booted, phase: 0
Started GET "/" for 127.0.0.1 at 2015-05-13 17:58:20 +0800
Processing by ApplicationController#index as HTML
Rendered application/index.html.erb within layouts/application (3.6ms)
Completed 200 OK in 216ms (Views: 200.0ms | ActiveRecord: 0.0ms)
[22832] 127.0.0.1 - - [13/May/2015:17:58:20 +0800] "GET / HTTP/1.1" 200 - 0.8190
...
...
...
Completed 200 OK in 15003ms (Views: 0.1ms | ActiveRecord: 0.0ms)
[22852] 127.0.0.1 - - [13/May/2015:18:00:07 +0800] "GET /api_call.json?t=15?id=10 HTTP/1.1" 304 - 15.0103
**config.allow_concurrency = true (by default)**
-> Ideally each thread serves a request.
Puma starting in single mode...
* Version 2.11.2 (jruby 2.2.2), codename: Intrepid Squirrel
* Min threads: 1, max threads: 40
* Environment: development
NOTE: ActiveRecord 4.2 is not (yet) fully supported by AR-JDBC, please help us finish 4.2 support - check http://bit.ly/jruby-42 for starters
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
Started GET "/" for 127.0.0.1 at 2015-05-13 18:23:04 +0800
Processing by ApplicationController#index as HTML
Rendered application/index.html.erb within layouts/application (35.0ms)
...
...
...
Completed 200 OK in 15020ms (Views: 0.7ms | ActiveRecord: 0.0ms)
127.0.0.1 - - [13/May/2015:18:25:19 +0800] "GET /api_call.json?t=15?id=9 HTTP/1.1" 304 - 15.0640