@@ -69,7 +69,6 @@ module Fiber::ExecutionContext
6969
7070 @parked = Atomic (Int32 ).new(0 )
7171 @spinning = Atomic (Int32 ).new(0 )
72- @capacity : Int32
7372
7473 # :nodoc:
7574 protected def self.default (maximum : Int32 ) : self
@@ -102,12 +101,12 @@ module Fiber::ExecutionContext
102101 @condition = Thread ::ConditionVariable .new
103102
104103 @global_queue = GlobalQueue .new(@mutex )
105- @schedulers = Array (Scheduler ).new(@ capacity )
106- @threads = Array (Thread ).new(@ capacity )
104+ @schedulers = Array (Scheduler ).new(capacity)
105+ @threads = Array (Thread ).new(capacity)
107106
108107 @rng = Random ::PCG32 .new
109108
110- start_schedulers
109+ start_schedulers(capacity)
111110 @threads << hijack_current_thread(@schedulers .first) if hijack
112111
113112 ExecutionContext .execution_contexts.push(self )
@@ -120,7 +119,7 @@ module Fiber::ExecutionContext
120119
121120 # The maximum number of threads that can be started.
122121 def capacity : Int32
123- @capacity
122+ @schedulers .size
124123 end
125124
126125 # :nodoc:
@@ -140,7 +139,7 @@ module Fiber::ExecutionContext
140139 # OPTIMIZE: consider storing schedulers to an array-like object that would
141140 # use an atomic/fence to make sure that @size can only be incremented
142141 # *after* the value has been written to @buffer.
143- private def start_schedulers
142+ private def start_schedulers ( capacity )
144143 capacity.times do |index |
145144 @schedulers << Scheduler .new(self , " #{ @name } -#{ index } " )
146145 end
@@ -176,6 +175,71 @@ module Fiber::ExecutionContext
176175 end
177176 end
178177
178+ # Resizes the context to the new *maximum* parallelism.
179+ #
180+ # The new *maximum* can grow, in which case more schedulers are created to
181+ # eventually increase the parallelism.
182+ #
183+ # The new *maximum* can also shrink, in which case the overflow schedulers
184+ # are removed and told to shutdown immediately. The actual shutdown is
185+ # cooperative, so running schedulers won't stop until their current fiber
186+ # tries to switch to another fiber.
187+ def resize (maximum : Int32 ) : Nil
188+ raise ArgumentError .new(" Parallelism can't be less than one." ) if maximum < 1
189+ removed_schedulers = nil
190+
191+ @mutex .synchronize do
192+ # can run in parallel to #steal that dereferences @schedulers (once)
193+ # without locking the mutex, so we dup the schedulers, mutate the copy,
194+ # and eventually assign the copy as @schedulers; this way #steal can
195+ # safely access the array (never mutated).
196+ new_capacity = maximum
197+ old_threads = @threads
198+ old_schedulers = @schedulers
199+ old_capacity = capacity
200+
201+ if new_capacity > old_capacity
202+ @schedulers = Array (Scheduler ).new(new_capacity) do |index |
203+ old_schedulers[index]? || Scheduler .new(self , " #{ @name } -#{ index } " )
204+ end
205+ threads = Array (Thread ).new(new_capacity)
206+ old_threads.each { |thread | threads << thread }
207+ @threads = threads
208+ elsif new_capacity < old_capacity
209+ # tell the overflow schedulers to shutdown
210+ removed_schedulers = old_schedulers[new_capacity..]
211+ removed_schedulers.each(& .shutdown!)
212+
213+ # resize
214+ @schedulers = old_schedulers[0 ...new_capacity]
215+ @threads = old_threads[0 ...new_capacity]
216+
217+ # reset @parked counter (we wake all parked threads) so they can
218+ # shutdown (if told to):
219+ woken_threads = @parked .get(:relaxed )
220+ @parked .set(0 , :relaxed )
221+
222+ # update @spinning prior to unpark threads; we use acquire release
223+ # semantics to make sure that all the above stores are visible before
224+ # the following wakeup calls (maybe not needed, but let's err on the
225+ # safe side)
226+ @spinning .add(woken_threads, :acquire_release )
227+
228+ # wake every waiting thread:
229+ @condition .broadcast
230+ @event_loop .interrupt
231+ end
232+ end
233+
234+ return unless removed_schedulers
235+
236+ # drain the local queues of removed schedulers since they're no longer
237+ # available for stealing
238+ removed_schedulers.each do |scheduler |
239+ scheduler.@runnables .drain
240+ end
241+ end
242+
179243 # :nodoc:
180244 def spawn (* , name : String ? = nil , same_thread : Bool , & block : - > ) : Fiber
181245 raise ArgumentError .new(" #{ self .class.name} #spawn doesn't support same_thread:true" ) if same_thread
@@ -200,11 +264,12 @@ module Fiber::ExecutionContext
200264 protected def steal (& : Scheduler - > ) : Nil
201265 return if capacity == 1
202266
267+ schedulers = @schedulers
203268 i = @rng .next_int
204- n = @ schedulers .size
269+ n = schedulers.size
205270
206271 n.times do |j |
207- if scheduler = @ schedulers [(i &+ j) % n]?
272+ if scheduler = schedulers[(i &+ j) % n]?
208273 yield scheduler
209274 end
210275 end
@@ -282,11 +347,11 @@ module Fiber::ExecutionContext
282347 # check if we can start another thread; no need for atomics, the values
283348 # shall be rather stable over time and we check them again inside the
284349 # mutex
285- return if @threads .size = = capacity
350+ return if @threads .size > = capacity
286351
287352 @mutex .synchronize do
288353 index = @threads .size
289- return if index = = capacity # check again
354+ return if index > = capacity # check again
290355
291356 @threads << start_thread(@schedulers [index])
292357 end
0 commit comments