ruby-concurrency / concurrent-ruby

Modern concurrency tools including agents, futures, promises, thread pools, supervisors, and more. Inspired by Erlang, Clojure, Scala, Go, Java, JavaScript, and classic concurrency patterns.
https://ruby-concurrency.github.io/concurrent-ruby/
Other
5.71k stars 420 forks source link

Does global_io_executor / Concurrent::CachedThreadPool clear threads? #958

Closed Overload119 closed 2 years ago

Overload119 commented 2 years ago
* Operating system:                mac
* Ruby implementation:             Ruby
* `concurrent-ruby` version:       1.1.10
* `concurrent-ruby-ext` installed: no
* `concurrent-ruby-edge` used:     no

I've been debugging an issue with Sidekiq outlined here. I'm trying to remove the number of threads in my long-running Ruby process.

It seems like GLOBAL_IO_EXECUTOR just keeps growing in the number of threads and never shrinks.

This can be tested in Ruby:

500.times { Concurrent::Promise.execute { puts "Hi" } }
Thread.list.count

I've gotten around this issue with this crazy piece of code:

Concurrent.send(
  :const_set,
  'GLOBAL_IO_EXECUTOR',
  Concurrent::Delay.new { Concurrent.new_fast_executor },
)

Any advice on what I should do here to reduce the number of threads in my Ruby process?

chrisseaton commented 2 years ago

By default, Promise uses the :io, as you've realised. This is for IO operations, where many threads often makes sense. If you want a fixed number of threads you can use for example :fast, which is designed for parallelism on runtimes that support that or C extensions that run in parallel on all runtimes. Or you can created your own FixedThreadPool.

What do you promises do?

Overload119 commented 2 years ago

Actually it's another gem that's using this library that I've debugged this down to. https://github.com/YusukeIwaki/puppeteer-ruby/issues/237

I don't think I could answer that question correctly since I'm not sure exactly what these calls are doing but it relates to interacting with a puppeteer browser over the network.

I guess the surprising thing for me is that CachedThreadPool never actually shrinks if I understand it correctly like it says here in the docs.

A thread pool that dynamically grows and shrinks to fit the current workload.

I have verified though that will re-use an idle thread which I guess makes sense.

500.times { sleep(0.00001); Concurrent::Promise.execute { puts "Hi" } }
Thread.list.count

If it's the case that I should be using FixedThreadPool but I don't want to go through the effort of changing the other gem, is there something better than changing the default like I've done in my snippet by redefining the constant?

IE. Something like Concurrent.set_defaults(...) would be helpful here.

chrisseaton commented 2 years ago

I think you (or this upstream Gem author) want to submit jobs to an executor, rather than creating promises manually. An executor has (can have) a queue of jobs, where jobs sit until there is available concurrency.

Concurrent Ruby is working as expected here. I can advise on design choices if I'm pointed at the relevant code and given the context. I don't have time to go off and find what they're doing and where this code is myself, but I'll happily help if I'm pointed at things.

chrisseaton commented 2 years ago

I'm going to close this issue, as there's no bug or feature request here. But feel free to keep engaging on this thread to get your problem fixed.