async 1.32.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ed31b6b8b115f7013a59e0eb58fd10793c061a8be9968f83fc3d8ee750e92c8
4
- data.tar.gz: a7517e133ad3f944067399f2b9f9dd3e7753fb82cd4912424dd0690f17eebd3d
3
+ metadata.gz: 6e5f7f4cbc1cfc595409b58cfc0b74e2a2767837fc40bae5e1a1b60e7dcd3384
4
+ data.tar.gz: 4eeb27d8248bc7993cf7758e41c8ffee3a54141d7c521e2a2805bbc8e8be3edd
5
5
  SHA512:
6
- metadata.gz: 4819e554eec0bc91973791b6d87042638658f54e372ee11f32dd11c7c319d28cf9a08745f7f2a1af04ca0f20cff7a5c29174fcb7fddcc78a56a44f1499385a39
7
- data.tar.gz: 2e777449edd348c2db0dc0a50d756981cd5e0a2e5d9f709e1f0dbbc609e39e4e840b86258d0a1a744473855cbec8539b3398b91e01133128a84721c77bfc0e6d
6
+ metadata.gz: d0b59a751dd4ea8cd7f2a83ad76fbf4dcff30ca11629d2e7f6ba18e017cf5de06880c1a6b19a3297967793e80394e10f8873ed24aa278d2019b11eb90a002394
7
+ data.tar.gz: 1c23c709f983bc14ba1477b41b3ca7533275eb3464928c7d4d564e897fc2fb893617cde7bb6d1e90d050db9bbc1d1d2ed71c87da02e61cd559f0afef363fc567
@@ -0,0 +1,36 @@
1
+ A synchronization primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore}.
2
+
3
+ ## Example
4
+
5
+ ~~~ ruby
6
+ require 'async'
7
+ require 'async/barrier'
8
+
9
+ Sync do
10
+ barrier = Async::Barrier.new
11
+
12
+ # Generate an array of 10 numbers:
13
+ numbers = 10.times.map{rand(10)}
14
+ sorted = []
15
+
16
+ # Sleep sort the numbers:
17
+ numbers.each do |number|
18
+ barrier.async do |task|
19
+ task.sleep(number)
20
+ sorted << number
21
+ end
22
+ end
23
+
24
+ # Wait for all the numbers to be sorted:
25
+ barrier.wait
26
+
27
+ Console.logger.info("Sorted", sorted)
28
+ end
29
+ ~~~
30
+
31
+ ### Output
32
+
33
+ ~~~
34
+ 0.0s info: Sorted [ec=0x104] [pid=50291]
35
+ | [0, 0, 0, 0, 1, 2, 2, 3, 6, 6]
36
+ ~~~
data/lib/async/barrier.rb CHANGED
@@ -23,8 +23,12 @@
23
23
  require_relative 'task'
24
24
 
25
25
  module Async
26
- # A barrier is used to synchronize multiple tasks, waiting for them all to complete before continuing.
26
+ # A synchronization primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore}.
27
+ # @public Since `stable-v1`.
27
28
  class Barrier
29
+ # Initialize the barrier.
30
+ # @parameter parent [Task | Semaphore | Nil] The parent for holding any children tasks.
31
+ # @public Since `stable-v1`.
28
32
  def initialize(parent: nil)
29
33
  @tasks = []
30
34
 
@@ -34,10 +38,13 @@ module Async
34
38
  # All tasks which have been invoked into the barrier.
35
39
  attr :tasks
36
40
 
41
+ # The number of tasks currently held by the barrier.
37
42
  def size
38
43
  @tasks.size
39
44
  end
40
45
 
46
+ # Execute a child task and add it to the barrier.
47
+ # @asynchronous Executes the given block concurrently.
41
48
  def async(*arguments, parent: (@parent or Task.current), **options, &block)
42
49
  task = parent.async(*arguments, **options, &block)
43
50
 
@@ -46,6 +53,8 @@ module Async
46
53
  return task
47
54
  end
48
55
 
56
+ # Whether there are any tasks being held by the barrier.
57
+ # @returns [Boolean]
49
58
  def empty?
50
59
  @tasks.empty?
51
60
  end
@@ -60,15 +69,14 @@ module Async
60
69
  begin
61
70
  task.wait
62
71
  ensure
63
- # We don't know for sure that the exception was due to the task completion.
64
- unless task.running?
65
- # Remove the task from the waiting list if it's finished:
66
- @tasks.shift if @tasks.first == task
67
- end
72
+ # Remove the task from the waiting list if it's finished:
73
+ @tasks.shift if @tasks.first == task
68
74
  end
69
75
  end
70
76
  end
71
77
 
78
+ # Stop all tasks held by the barrier.
79
+ # @asynchronous May wait for tasks to finish executing.
72
80
  def stop
73
81
  # We have to be careful to avoid enumerating tasks while adding/removing to it:
74
82
  tasks = @tasks.dup
data/lib/async/clock.rb CHANGED
@@ -21,6 +21,8 @@
21
21
  # THE SOFTWARE.
22
22
 
23
23
  module Async
24
+ # A convenient wrapper around the internal monotonic clock.
25
+ # @public Since `stable-v1`.
24
26
  class Clock
25
27
  # Get the current elapsed monotonic time.
26
28
  def self.now
@@ -28,6 +30,8 @@ module Async
28
30
  end
29
31
 
30
32
  # Measure the execution of a block of code.
33
+ # @yields {...} The block to execute.
34
+ # @returns [Numeric] The total execution time.
31
35
  def self.measure
32
36
  start_time = self.now
33
37
 
@@ -36,19 +40,25 @@ module Async
36
40
  return self.now - start_time
37
41
  end
38
42
 
43
+ # Start measuring elapsed time from now.
44
+ # @returns [Clock]
39
45
  def self.start
40
46
  self.new.tap(&:start!)
41
47
  end
42
48
 
49
+ # Create a new clock with the initial total time.
50
+ # @parameter total [Numeric] The initial clock duration.
43
51
  def initialize(total = 0)
44
52
  @total = total
45
53
  @started = nil
46
54
  end
47
55
 
56
+ # Start measuring a duration.
48
57
  def start!
49
58
  @started ||= Clock.now
50
59
  end
51
60
 
61
+ # Stop measuring a duration and append the duration to the current total.
52
62
  def stop!
53
63
  if @started
54
64
  @total += (Clock.now - @started)
@@ -58,6 +68,7 @@ module Async
58
68
  return @total
59
69
  end
60
70
 
71
+ # The total elapsed time including any current duration.
61
72
  def total
62
73
  total = @total
63
74
 
@@ -0,0 +1,31 @@
1
+ A synchronization primitive, which allows fibers to wait until a particular condition is (edge) triggered. Zero or more fibers can wait on a condition. When the condition is signalled, the fibers will be resumed in order.
2
+
3
+ ## Example
4
+
5
+ ~~~ ruby
6
+ require 'async'
7
+
8
+ Sync do
9
+ condition = Async::Condition.new
10
+
11
+ Async do
12
+ Console.logger.info "Waiting for condition..."
13
+ value = condition.wait
14
+ Console.logger.info "Condition was signalled: #{value}"
15
+ end
16
+
17
+ Async do |task|
18
+ task.sleep(1)
19
+ Console.logger.info "Signalling condition..."
20
+ condition.signal("Hello World")
21
+ end
22
+ end
23
+ ~~~
24
+
25
+ ### Output
26
+
27
+ ~~~
28
+ 0.0s info: Waiting for condition... [ec=0x3c] [pid=47943]
29
+ 1.0s info: Signalling condition... [ec=0x64] [pid=47943]
30
+ 1.0s info: Condition was signalled: Hello World [ec=0x3c] [pid=47943]
31
+ ~~~
@@ -24,43 +24,54 @@ require 'fiber'
24
24
  require_relative 'node'
25
25
 
26
26
  module Async
27
- # A synchronization primative, which allows fibers to wait until a particular condition is triggered. Signalling the condition directly resumes the waiting fibers and thus blocks the caller.
27
+ # A synchronization primitive, which allows fibers to wait until a particular condition is (edge) triggered.
28
+ # @public Since `stable-v1`.
28
29
  class Condition
29
30
  def initialize
30
31
  @waiting = []
31
32
  end
32
33
 
34
+ Queue = Struct.new(:fiber) do
35
+ def transfer(*arguments)
36
+ fiber&.transfer(*arguments)
37
+ end
38
+
39
+ def alive?
40
+ fiber&.alive?
41
+ end
42
+
43
+ def nullify
44
+ self.fiber = nil
45
+ end
46
+ end
47
+
48
+ private_constant :Queue
49
+
33
50
  # Queue up the current fiber and wait on yielding the task.
34
- # @return [Object]
51
+ # @returns [Object]
35
52
  def wait
36
- fiber = Fiber.current
37
- @waiting << fiber
38
-
39
- Task.yield
53
+ queue = Queue.new(Fiber.current)
54
+ @waiting << queue
40
55
 
41
- # It would be nice if there was a better construct for this. We only need to invoke #delete if the task was not resumed normally. This can only occur with `raise` and `throw`. But there is no easy way to detect this.
42
- # ensure when not return or ensure when raise, throw
43
- rescue Exception
44
- @waiting.delete(fiber)
45
- raise
56
+ Fiber.scheduler.transfer
57
+ ensure
58
+ queue.nullify
46
59
  end
47
60
 
48
61
  # Is any fiber waiting on this notification?
49
- # @return [Boolean]
62
+ # @returns [Boolean]
50
63
  def empty?
51
64
  @waiting.empty?
52
65
  end
53
66
 
54
67
  # Signal to a given task that it should resume operations.
55
- # @param value The value to return to the waiting fibers.
56
- # @see Task.yield which is responsible for handling value.
57
- # @return [void]
68
+ # @parameter value [Object | Nil] The value to return to the waiting fibers.
58
69
  def signal(value = nil)
59
70
  waiting = @waiting
60
71
  @waiting = []
61
72
 
62
73
  waiting.each do |fiber|
63
- fiber.resume(value) if fiber.alive?
74
+ Fiber.scheduler.resume(fiber, value) if fiber.alive?
64
75
  end
65
76
 
66
77
  return nil
data/lib/async/node.rb CHANGED
@@ -21,7 +21,7 @@
21
21
  # THE SOFTWARE.
22
22
 
23
23
  module Async
24
- # A double linked list.
24
+ # A double linked list used for managing tasks.
25
25
  class List
26
26
  def initialize
27
27
  # The list behaves like a list node, so @tail points to the next item (the first one) and head points to the previous item (the last one). This may be slightly confusing but it makes the interface more natural.
@@ -121,6 +121,7 @@ module Async
121
121
 
122
122
  private_constant :List
123
123
 
124
+ # A list of children tasks.
124
125
  class Children < List
125
126
  def initialize
126
127
  super
@@ -154,10 +155,10 @@ module Async
154
155
  end
155
156
  end
156
157
 
157
- # Represents a node in a tree, used for nested {Task} instances.
158
+ # A node in a tree, used for implementing the task hierarchy.
158
159
  class Node
159
160
  # Create a new node in the tree.
160
- # @param parent [Node, nil] This node will attach to the given parent.
161
+ # @parameter parent [Node | Nil] This node will attach to the given parent.
161
162
  def initialize(parent = nil, annotation: nil, transient: false)
162
163
  @parent = nil
163
164
  @children = nil
@@ -175,15 +176,21 @@ module Async
175
176
  end
176
177
  end
177
178
 
178
- # You should not directly rely on these pointers but instead use `#children`.
179
- # List pointers:
179
+ # @returns [Node] the root node in the hierarchy.
180
+ def root
181
+ @parent&.root || self
182
+ end
183
+
184
+ # @private
180
185
  attr_accessor :head
186
+
187
+ # @private
181
188
  attr_accessor :tail
182
189
 
183
- # @attr parent [Node, nil]
190
+ # @attribute [Node] The parent node.
184
191
  attr :parent
185
192
 
186
- # @attr children [List<Node>] Optional list of children.
193
+ # @attribute children [Children | Nil] Optional list of children.
187
194
  attr :children
188
195
 
189
196
  # A useful identifier for the current node.
@@ -215,6 +222,8 @@ module Async
215
222
 
216
223
  if @annotation
217
224
  "#{@object_name} #{@annotation}"
225
+ elsif line = self.backtrace(0, 1)&.first
226
+ "#{@object_name} #{line}"
218
227
  else
219
228
  @object_name
220
229
  end
@@ -225,12 +234,14 @@ module Async
225
234
  end
226
235
 
227
236
  def to_s
228
- "\#<#{description}>"
237
+ "\#<#{self.description}>"
229
238
  end
230
239
 
240
+ alias inspect to_s
241
+
231
242
  # Change the parent of this node.
232
- # @param parent [Node, nil] the parent to attach to, or nil to detach.
233
- # @return [self]
243
+ # @parameter parent [Node | Nil] the parent to attach to, or nil to detach.
244
+ # @returns [Node] Itself.
234
245
  def parent=(parent)
235
246
  return if @parent.equal?(parent)
236
247
 
@@ -263,7 +274,7 @@ module Async
263
274
 
264
275
  # Whether the node can be consumed safely. By default, checks if the
265
276
  # children set is empty.
266
- # @return [Boolean]
277
+ # @returns [Boolean]
267
278
  def finished?
268
279
  @children.nil? || @children.finished?
269
280
  end
@@ -293,7 +304,7 @@ module Async
293
304
  end
294
305
 
295
306
  # Traverse the tree.
296
- # @yield [node, level] The node and the level relative to the given root.
307
+ # @yields {|node, level| ...} The node and the level relative to the given root.
297
308
  def traverse(level = 0, &block)
298
309
  yield self, level
299
310
 
@@ -329,6 +340,10 @@ module Async
329
340
  end
330
341
  end
331
342
 
343
+ def stopped?
344
+ @children.nil?
345
+ end
346
+
332
347
  def print_hierarchy(out = $stdout, backtrace: true)
333
348
  self.traverse do |node, level|
334
349
  indent = "\t" * level
@@ -24,13 +24,13 @@ require_relative 'condition'
24
24
 
25
25
  module Async
26
26
  # A synchronization primitive, which allows fibers to wait until a notification is received. Does not block the task which signals the notification. Waiting tasks are resumed on next iteration of the reactor.
27
+ # @public Since `stable-v1`.
27
28
  class Notification < Condition
28
29
  # Signal to a given task that it should resume operations.
29
- # @return [void]
30
30
  def signal(value = nil, task: Task.current)
31
31
  return if @waiting.empty?
32
32
 
33
- task.reactor << Signal.new(@waiting, value)
33
+ Fiber.scheduler.push Signal.new(@waiting, value)
34
34
 
35
35
  @waiting = []
36
36
 
@@ -42,9 +42,9 @@ module Async
42
42
  true
43
43
  end
44
44
 
45
- def resume
45
+ def transfer
46
46
  waiting.each do |fiber|
47
- fiber.resume(value) if fiber.alive?
47
+ fiber.transfer(value) if fiber.alive?
48
48
  end
49
49
  end
50
50
  end
data/lib/async/queue.rb CHANGED
@@ -24,6 +24,7 @@ require_relative 'notification'
24
24
 
25
25
  module Async
26
26
  # A queue which allows items to be processed in order.
27
+ # @public Since `stable-v1`.
27
28
  class Queue < Notification
28
29
  def initialize(parent: nil)
29
30
  super()
@@ -42,16 +43,14 @@ module Async
42
43
  @items.empty?
43
44
  end
44
45
 
45
- def <<(item)
46
- @items << item
46
+ def enqueue(item)
47
+ @items.push(item)
47
48
 
48
49
  self.signal unless self.empty?
49
50
  end
50
51
 
51
- def enqueue(*items)
52
- @items.concat(items)
53
-
54
- self.signal unless self.empty?
52
+ def <<(item)
53
+ enqueue(item)
55
54
  end
56
55
 
57
56
  def dequeue
@@ -75,6 +74,7 @@ module Async
75
74
  end
76
75
  end
77
76
 
77
+ # @public Since `stable-v1`.
78
78
  class LimitedQueue < Queue
79
79
  def initialize(limit = 1, **options)
80
80
  super(**options)
@@ -86,12 +86,12 @@ module Async
86
86
 
87
87
  attr :limit
88
88
 
89
- # @return [Boolean] Whether trying to enqueue an item would block.
89
+ # @returns [Boolean] Whether trying to enqueue an item would block.
90
90
  def limited?
91
91
  @items.size >= @limit
92
92
  end
93
93
 
94
- def <<(item)
94
+ def enqueue item
95
95
  while limited?
96
96
  @full.wait
97
97
  end
@@ -99,19 +99,6 @@ module Async
99
99
  super
100
100
  end
101
101
 
102
- def enqueue *items
103
- while !items.empty?
104
- while limited?
105
- @full.wait
106
- end
107
-
108
- available = @limit - @items.size
109
- @items.concat(items.shift(available))
110
-
111
- self.signal unless self.empty?
112
- end
113
- end
114
-
115
102
  def dequeue
116
103
  item = super
117
104