async 2.2.1 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/async/barrier.md +2 -2
- data/lib/async/barrier.rb +25 -19
- data/lib/async/condition.md +3 -3
- data/lib/async/condition.rb +24 -18
- data/lib/async/list.rb +181 -0
- data/lib/async/node.rb +58 -134
- data/lib/async/notification.rb +1 -3
- data/lib/async/reactor.rb +4 -0
- data/lib/async/scheduler.rb +6 -0
- data/lib/async/semaphore.rb +21 -11
- data/lib/async/task.rb +12 -7
- data/lib/async/version.rb +1 -1
- data/lib/async/waiter.md +50 -0
- data/lib/async/waiter.rb +51 -0
- data/lib/kernel/async.rb +1 -0
- data/lib/kernel/sync.rb +1 -0
- data.tar.gz.sig +0 -0
- metadata +26 -9
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d60db4d677d9cb4ed92f654f19ca17a4dab336002ba44290a80545b1f25e9154
|
4
|
+
data.tar.gz: 9e84c874ef477cd0de6a86022c87a3fe92ae9276ddd3c4f4ccfd9d6600be0672
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7dd4367c1abb5cc79bf635a34b5819522e7f8363c385f69c995fe23d6fd4d7f6fc6a07784c789afbad0ff1ff6726d6d11899dd9b0a87ae582e9f78711c0ab346
|
7
|
+
data.tar.gz: 0cbef57bb4896a06b11a95f318ad8a0d87435b70f591abc73bb8c7bf76773a341aba47399ef59a813f3d87f32f57112d070081db8598d4d58967f13ac26b4c61
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/lib/async/barrier.md
CHANGED
data/lib/async/barrier.rb
CHANGED
@@ -3,35 +3,47 @@
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2019-2022, by Samuel Williams.
|
5
5
|
|
6
|
+
require_relative 'list'
|
6
7
|
require_relative 'task'
|
7
8
|
|
8
9
|
module Async
|
9
|
-
# A
|
10
|
+
# A general purpose synchronisation primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore}.
|
11
|
+
#
|
10
12
|
# @public Since `stable-v1`.
|
11
13
|
class Barrier
|
12
14
|
# Initialize the barrier.
|
13
15
|
# @parameter parent [Task | Semaphore | Nil] The parent for holding any children tasks.
|
14
16
|
# @public Since `stable-v1`.
|
15
17
|
def initialize(parent: nil)
|
16
|
-
@tasks =
|
18
|
+
@tasks = List.new
|
17
19
|
|
18
20
|
@parent = parent
|
19
21
|
end
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
+
class TaskNode < List::Node
|
24
|
+
def initialize(task)
|
25
|
+
@task = task
|
26
|
+
end
|
27
|
+
|
28
|
+
attr :task
|
29
|
+
end
|
23
30
|
|
24
|
-
|
31
|
+
private_constant :TaskNode
|
32
|
+
|
33
|
+
# Number of tasks being held by the barrier.
|
25
34
|
def size
|
26
35
|
@tasks.size
|
27
36
|
end
|
28
37
|
|
38
|
+
# All tasks which have been invoked into the barrier.
|
39
|
+
attr :tasks
|
40
|
+
|
29
41
|
# Execute a child task and add it to the barrier.
|
30
42
|
# @asynchronous Executes the given block concurrently.
|
31
43
|
def async(*arguments, parent: (@parent or Task.current), **options, &block)
|
32
44
|
task = parent.async(*arguments, **options, &block)
|
33
45
|
|
34
|
-
@tasks
|
46
|
+
@tasks.append(TaskNode.new(task))
|
35
47
|
|
36
48
|
return task
|
37
49
|
end
|
@@ -42,21 +54,15 @@ module Async
|
|
42
54
|
@tasks.empty?
|
43
55
|
end
|
44
56
|
|
45
|
-
# Wait for all tasks.
|
57
|
+
# Wait for all tasks to complete by invoking {Task#wait} on each waiting task, which may raise an error. As long as the task has completed, it will be removed from the barrier.
|
46
58
|
# @asynchronous Will wait for tasks to finish executing.
|
47
59
|
def wait
|
48
|
-
|
49
|
-
|
50
|
-
task = @tasks.first
|
51
|
-
|
60
|
+
@tasks.each do |waiting|
|
61
|
+
task = waiting.task
|
52
62
|
begin
|
53
63
|
task.wait
|
54
64
|
ensure
|
55
|
-
|
56
|
-
unless task.running?
|
57
|
-
# Remove the task from the waiting list if it's finished:
|
58
|
-
@tasks.shift if @tasks.first == task
|
59
|
-
end
|
65
|
+
@tasks.remove?(waiting) unless task.alive?
|
60
66
|
end
|
61
67
|
end
|
62
68
|
end
|
@@ -64,9 +70,9 @@ module Async
|
|
64
70
|
# Stop all tasks held by the barrier.
|
65
71
|
# @asynchronous May wait for tasks to finish executing.
|
66
72
|
def stop
|
67
|
-
|
68
|
-
|
69
|
-
|
73
|
+
@tasks.each do |waiting|
|
74
|
+
waiting.task.stop
|
75
|
+
end
|
70
76
|
end
|
71
77
|
end
|
72
78
|
end
|
data/lib/async/condition.md
CHANGED
@@ -25,7 +25,7 @@ end
|
|
25
25
|
### Output
|
26
26
|
|
27
27
|
~~~
|
28
|
-
0.0s info: Waiting for condition...
|
29
|
-
1.0s info: Signalling condition...
|
30
|
-
1.0s info: Condition was signalled: Hello World
|
28
|
+
0.0s info: Waiting for condition...
|
29
|
+
1.0s info: Signalling condition...
|
30
|
+
1.0s info: Condition was signalled: Hello World
|
31
31
|
~~~
|
data/lib/async/condition.rb
CHANGED
@@ -5,41 +5,38 @@
|
|
5
5
|
# Copyright, 2017, by Kent Gruber.
|
6
6
|
|
7
7
|
require 'fiber'
|
8
|
-
require_relative '
|
8
|
+
require_relative 'list'
|
9
9
|
|
10
10
|
module Async
|
11
11
|
# A synchronization primitive, which allows fibers to wait until a particular condition is (edge) triggered.
|
12
12
|
# @public Since `stable-v1`.
|
13
13
|
class Condition
|
14
14
|
def initialize
|
15
|
-
@waiting =
|
15
|
+
@waiting = List.new
|
16
16
|
end
|
17
17
|
|
18
|
-
|
19
|
-
def
|
20
|
-
fiber
|
18
|
+
class FiberNode < List::Node
|
19
|
+
def initialize(fiber)
|
20
|
+
@fiber = fiber
|
21
21
|
end
|
22
22
|
|
23
|
-
def
|
24
|
-
fiber
|
23
|
+
def transfer(*arguments)
|
24
|
+
@fiber.transfer(*arguments)
|
25
25
|
end
|
26
26
|
|
27
|
-
def
|
28
|
-
|
27
|
+
def alive?
|
28
|
+
@fiber.alive?
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
private_constant :
|
32
|
+
private_constant :FiberNode
|
33
33
|
|
34
34
|
# Queue up the current fiber and wait on yielding the task.
|
35
35
|
# @returns [Object]
|
36
36
|
def wait
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
Fiber.scheduler.transfer
|
41
|
-
ensure
|
42
|
-
queue.nullify
|
37
|
+
@waiting.stack(FiberNode.new(Fiber.current)) do
|
38
|
+
Fiber.scheduler.transfer
|
39
|
+
end
|
43
40
|
end
|
44
41
|
|
45
42
|
# Is any fiber waiting on this notification?
|
@@ -51,8 +48,9 @@ module Async
|
|
51
48
|
# Signal to a given task that it should resume operations.
|
52
49
|
# @parameter value [Object | Nil] The value to return to the waiting fibers.
|
53
50
|
def signal(value = nil)
|
54
|
-
|
55
|
-
|
51
|
+
return if @waiting.empty?
|
52
|
+
|
53
|
+
waiting = self.exchange
|
56
54
|
|
57
55
|
waiting.each do |fiber|
|
58
56
|
Fiber.scheduler.resume(fiber, value) if fiber.alive?
|
@@ -60,5 +58,13 @@ module Async
|
|
60
58
|
|
61
59
|
return nil
|
62
60
|
end
|
61
|
+
|
62
|
+
protected
|
63
|
+
|
64
|
+
def exchange
|
65
|
+
waiting = @waiting
|
66
|
+
@waiting = List.new
|
67
|
+
return waiting
|
68
|
+
end
|
63
69
|
end
|
64
70
|
end
|
data/lib/async/list.rb
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2022, by Samuel Williams.
|
5
|
+
|
6
|
+
module Async
|
7
|
+
# A general doublely linked list. This is used internally by {Async::Barrier} and {Async::Condition} to manage child tasks.
|
8
|
+
class List
|
9
|
+
# Initialize a new, empty, list.
|
10
|
+
def initialize
|
11
|
+
@head = self
|
12
|
+
@tail = self
|
13
|
+
@size = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
# Print a short summary of the list.
|
17
|
+
def to_s
|
18
|
+
"#<#{self.class.name} size=#{@size}>"
|
19
|
+
end
|
20
|
+
|
21
|
+
alias inspect to_s
|
22
|
+
|
23
|
+
# Points at the end of the list.
|
24
|
+
attr_accessor :head
|
25
|
+
|
26
|
+
# Points at the start of the list.
|
27
|
+
attr_accessor :tail
|
28
|
+
|
29
|
+
attr :size
|
30
|
+
|
31
|
+
# A callback that is invoked when an item is added to the list.
|
32
|
+
def added(node)
|
33
|
+
@size += 1
|
34
|
+
return node
|
35
|
+
end
|
36
|
+
|
37
|
+
# Append a node to the end of the list.
|
38
|
+
def append(node)
|
39
|
+
if node.head
|
40
|
+
raise ArgumentError, "Node is already in a list!"
|
41
|
+
end
|
42
|
+
|
43
|
+
node.tail = self
|
44
|
+
@head.tail = node
|
45
|
+
node.head = @head
|
46
|
+
@head = node
|
47
|
+
|
48
|
+
return added(node)
|
49
|
+
end
|
50
|
+
|
51
|
+
def prepend(node)
|
52
|
+
if node.head
|
53
|
+
raise ArgumentError, "Node is already in a list!"
|
54
|
+
end
|
55
|
+
|
56
|
+
node.head = self
|
57
|
+
@tail.head = node
|
58
|
+
node.tail = @tail
|
59
|
+
@tail = node
|
60
|
+
|
61
|
+
return added(node)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Add the node, yield, and the remove the node.
|
65
|
+
# @yields {|node| ...} Yields the node.
|
66
|
+
# @returns [Object] Returns the result of the block.
|
67
|
+
def stack(node, &block)
|
68
|
+
append(node)
|
69
|
+
return yield(node)
|
70
|
+
ensure
|
71
|
+
remove!(node)
|
72
|
+
end
|
73
|
+
|
74
|
+
# A callback that is invoked when an item is removed from the list.
|
75
|
+
def removed(node)
|
76
|
+
@size -= 1
|
77
|
+
return node
|
78
|
+
end
|
79
|
+
|
80
|
+
# Remove the node if it is in a list.
|
81
|
+
#
|
82
|
+
# You should be careful to only remove nodes that are part of this list.
|
83
|
+
#
|
84
|
+
# @returns [Node] Returns the node if it was removed, otherwise nil.
|
85
|
+
def remove?(node)
|
86
|
+
if node.head
|
87
|
+
return remove!(node)
|
88
|
+
end
|
89
|
+
|
90
|
+
return nil
|
91
|
+
end
|
92
|
+
|
93
|
+
# Remove the node. If it was already removed, this will raise an error.
|
94
|
+
#
|
95
|
+
# You should be careful to only remove nodes that are part of this list.
|
96
|
+
#
|
97
|
+
# @raises [ArgumentError] If the node is not part of this list.
|
98
|
+
# @returns [Node] Returns the node if it was removed, otherwise nil.
|
99
|
+
def remove(node)
|
100
|
+
# One downside of this interface is we don't actually check if the node is part of the list defined by `self`. This means that there is a potential for a node to be removed from a different list using this method, which in can throw off book-keeping when lists track size, etc.
|
101
|
+
unless node.head
|
102
|
+
raise ArgumentError, "Node is not in a list!"
|
103
|
+
end
|
104
|
+
|
105
|
+
remove!(node)
|
106
|
+
end
|
107
|
+
|
108
|
+
private def remove!(node)
|
109
|
+
node.head.tail = node.tail
|
110
|
+
node.tail.head = node.head
|
111
|
+
|
112
|
+
# This marks the node as being removed, and causes remove to fail if called a 2nd time.
|
113
|
+
node.head = nil
|
114
|
+
# node.tail = nil
|
115
|
+
|
116
|
+
return removed(node)
|
117
|
+
end
|
118
|
+
|
119
|
+
# @returns [Boolean] Returns true if the list is empty.
|
120
|
+
def empty?
|
121
|
+
@tail.equal?(self)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Iterate over each node in the linked list. It is generally safe to remove the current node, any previous node or any future node during iteration.
|
125
|
+
#
|
126
|
+
# @yields {|node| ...} Yields each node in the list.
|
127
|
+
# @returns [List] Returns self.
|
128
|
+
def each
|
129
|
+
return to_enum unless block_given?
|
130
|
+
|
131
|
+
current = self
|
132
|
+
|
133
|
+
while true
|
134
|
+
node = current.tail
|
135
|
+
# binding.irb if node.nil? && !node.equal?(self)
|
136
|
+
break if node.equal?(self)
|
137
|
+
|
138
|
+
yield node
|
139
|
+
|
140
|
+
# If the node has deleted itself or any subsequent node, it will no longer be the next node, so don't use it for continued traversal:
|
141
|
+
if current.tail.equal?(node)
|
142
|
+
current = node
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
return self
|
147
|
+
end
|
148
|
+
|
149
|
+
# Determine whether the given node is included in the list.
|
150
|
+
#
|
151
|
+
# @parameter needle [Node] The node to search for.
|
152
|
+
# @returns [Boolean] Returns true if the node is in the list.
|
153
|
+
def include?(needle)
|
154
|
+
self.each do |item|
|
155
|
+
return true if needle.equal?(item)
|
156
|
+
end
|
157
|
+
|
158
|
+
return false
|
159
|
+
end
|
160
|
+
|
161
|
+
# @returns [Node] Returns the first node in the list, if it is not empty.
|
162
|
+
def first
|
163
|
+
unless @tail.equal?(self)
|
164
|
+
@tail
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# @returns [Node] Returns the last node in the list, if it is not empty.
|
169
|
+
def last
|
170
|
+
unless @head.equal?(self)
|
171
|
+
@head
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# A linked list Node.
|
177
|
+
class List::Node
|
178
|
+
attr_accessor :head
|
179
|
+
attr_accessor :tail
|
180
|
+
end
|
181
|
+
end
|
data/lib/async/node.rb
CHANGED
@@ -5,138 +5,48 @@
|
|
5
5
|
# Copyright, 2017, by Kent Gruber.
|
6
6
|
# Copyright, 2022, by Shannon Skipper.
|
7
7
|
|
8
|
+
require_relative 'list'
|
9
|
+
|
8
10
|
module Async
|
9
|
-
# A double linked list used for managing tasks.
|
10
|
-
class List
|
11
|
-
def initialize
|
12
|
-
# 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.
|
13
|
-
@head = nil
|
14
|
-
@tail = nil
|
15
|
-
@size = 0
|
16
|
-
end
|
17
|
-
|
18
|
-
attr :size
|
19
|
-
|
20
|
-
attr_accessor :head
|
21
|
-
attr_accessor :tail
|
22
|
-
|
23
|
-
# Inserts an item at the end of the list.
|
24
|
-
def insert(item)
|
25
|
-
unless @tail
|
26
|
-
@tail = item
|
27
|
-
@head = item
|
28
|
-
|
29
|
-
# Consistency:
|
30
|
-
item.head = nil
|
31
|
-
item.tail = nil
|
32
|
-
else
|
33
|
-
@head.tail = item
|
34
|
-
item.head = @head
|
35
|
-
|
36
|
-
# Consistency:
|
37
|
-
item.tail = nil
|
38
|
-
|
39
|
-
@head = item
|
40
|
-
end
|
41
|
-
|
42
|
-
@size += 1
|
43
|
-
|
44
|
-
return self
|
45
|
-
end
|
46
|
-
|
47
|
-
def delete(item)
|
48
|
-
if @tail.equal?(item)
|
49
|
-
@tail = @tail.tail
|
50
|
-
else
|
51
|
-
item.head.tail = item.tail
|
52
|
-
end
|
53
|
-
|
54
|
-
if @head.equal?(item)
|
55
|
-
@head = @head.head
|
56
|
-
else
|
57
|
-
item.tail.head = item.head
|
58
|
-
end
|
59
|
-
|
60
|
-
item.head = nil
|
61
|
-
item.tail = nil
|
62
|
-
|
63
|
-
@size -= 1
|
64
|
-
|
65
|
-
return self
|
66
|
-
end
|
67
|
-
|
68
|
-
def each(&block)
|
69
|
-
return to_enum unless block_given?
|
70
|
-
|
71
|
-
current = self
|
72
|
-
while node = current.tail
|
73
|
-
yield node
|
74
|
-
|
75
|
-
# If the node has deleted itself or any subsequent node, it will no longer be the next node, so don't use it for continued traversal:
|
76
|
-
if current.tail.equal?(node)
|
77
|
-
current = node
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
def include?(needle)
|
83
|
-
self.each do |item|
|
84
|
-
return true if needle.equal?(item)
|
85
|
-
end
|
86
|
-
|
87
|
-
return false
|
88
|
-
end
|
89
|
-
|
90
|
-
def first
|
91
|
-
@tail
|
92
|
-
end
|
93
|
-
|
94
|
-
def last
|
95
|
-
@head
|
96
|
-
end
|
97
|
-
|
98
|
-
def empty?
|
99
|
-
@tail.nil?
|
100
|
-
end
|
101
|
-
|
102
|
-
def nil?
|
103
|
-
@tail.nil?
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
private_constant :List
|
108
|
-
|
109
11
|
# A list of children tasks.
|
110
12
|
class Children < List
|
111
13
|
def initialize
|
112
14
|
super
|
113
|
-
|
114
15
|
@transient_count = 0
|
115
16
|
end
|
116
17
|
|
117
|
-
#
|
18
|
+
# Some children may be marked as transient. Transient children do not prevent the parent from finishing.
|
19
|
+
# @returns [Boolean] Whether the node has transient children.
|
118
20
|
def transients?
|
119
21
|
@transient_count > 0
|
120
22
|
end
|
121
23
|
|
122
|
-
|
123
|
-
|
24
|
+
# Whether all children are considered finished. Ignores transient children.
|
25
|
+
def finished?
|
26
|
+
@size == @transient_count
|
27
|
+
end
|
28
|
+
|
29
|
+
# Whether the children is empty, preserved for compatibility.
|
30
|
+
def nil?
|
31
|
+
empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def added(node)
|
37
|
+
if node.transient?
|
124
38
|
@transient_count += 1
|
125
39
|
end
|
126
40
|
|
127
|
-
super
|
41
|
+
return super
|
128
42
|
end
|
129
43
|
|
130
|
-
def
|
131
|
-
if
|
44
|
+
def removed(node)
|
45
|
+
if node.transient?
|
132
46
|
@transient_count -= 1
|
133
47
|
end
|
134
48
|
|
135
|
-
super
|
136
|
-
end
|
137
|
-
|
138
|
-
def finished?
|
139
|
-
@size == @transient_count
|
49
|
+
return super
|
140
50
|
end
|
141
51
|
end
|
142
52
|
|
@@ -181,12 +91,18 @@ module Async
|
|
181
91
|
# A useful identifier for the current node.
|
182
92
|
attr :annotation
|
183
93
|
|
184
|
-
# Whether
|
94
|
+
# Whether this node has any children.
|
95
|
+
# @returns [Boolean]
|
185
96
|
def children?
|
186
|
-
@children
|
97
|
+
@children && !@children.empty?
|
187
98
|
end
|
188
99
|
|
189
|
-
#
|
100
|
+
# Represents whether a node is transient. Transient nodes are not considered
|
101
|
+
# when determining if a node is finished. This is useful for tasks which are
|
102
|
+
# internal to an object rather than explicit user concurrency. For example,
|
103
|
+
# a child task which is pruning a connection pool is transient, because it
|
104
|
+
# is not directly related to the parent task, and should not prevent the
|
105
|
+
# parent task from finishing.
|
190
106
|
def transient?
|
191
107
|
@transient
|
192
108
|
end
|
@@ -225,13 +141,14 @@ module Async
|
|
225
141
|
alias inspect to_s
|
226
142
|
|
227
143
|
# Change the parent of this node.
|
228
|
-
#
|
144
|
+
#
|
145
|
+
# @parameter parent [Node | Nil] The parent to attach to, or nil to detach.
|
229
146
|
# @returns [Node] Itself.
|
230
147
|
def parent=(parent)
|
231
148
|
return if @parent.equal?(parent)
|
232
149
|
|
233
150
|
if @parent
|
234
|
-
@parent.
|
151
|
+
@parent.remove_child(self)
|
235
152
|
@parent = nil
|
236
153
|
end
|
237
154
|
|
@@ -242,23 +159,23 @@ module Async
|
|
242
159
|
return self
|
243
160
|
end
|
244
161
|
|
245
|
-
protected def set_parent
|
162
|
+
protected def set_parent(parent)
|
246
163
|
@parent = parent
|
247
164
|
end
|
248
165
|
|
249
|
-
protected def add_child
|
166
|
+
protected def add_child(child)
|
250
167
|
@children ||= Children.new
|
251
|
-
@children.
|
168
|
+
@children.append(child)
|
252
169
|
child.set_parent(self)
|
253
170
|
end
|
254
171
|
|
255
|
-
protected def
|
256
|
-
@children.
|
172
|
+
protected def remove_child(child)
|
173
|
+
@children.remove(child)
|
257
174
|
child.set_parent(nil)
|
258
175
|
end
|
259
176
|
|
260
|
-
# Whether the node can be consumed safely. By default, checks if the
|
261
|
-
#
|
177
|
+
# Whether the node can be consumed (deleted) safely. By default, checks if the children set is empty.
|
178
|
+
#
|
262
179
|
# @returns [Boolean]
|
263
180
|
def finished?
|
264
181
|
@children.nil? || @children.finished?
|
@@ -268,15 +185,15 @@ module Async
|
|
268
185
|
# the parent.
|
269
186
|
def consume
|
270
187
|
if parent = @parent and finished?
|
271
|
-
parent.
|
188
|
+
parent.remove_child(self)
|
272
189
|
|
273
190
|
if @children
|
274
191
|
@children.each do |child|
|
275
192
|
if child.finished?
|
276
|
-
|
193
|
+
remove_child(child)
|
277
194
|
else
|
278
195
|
# In theory we don't need to do this... because we are throwing away the list. However, if you don't correctly update the list when moving the child to the parent, it foobars the enumeration, and subsequent nodes will be skipped, or in the worst case you might start enumerating the parents nodes.
|
279
|
-
|
196
|
+
remove_child(child)
|
280
197
|
parent.add_child(child)
|
281
198
|
end
|
282
199
|
end
|
@@ -288,18 +205,25 @@ module Async
|
|
288
205
|
end
|
289
206
|
end
|
290
207
|
|
291
|
-
# Traverse the tree.
|
208
|
+
# Traverse the task tree.
|
209
|
+
#
|
210
|
+
# @returns [Enumerator] An enumerator which will traverse the tree if no block is given.
|
292
211
|
# @yields {|node, level| ...} The node and the level relative to the given root.
|
293
|
-
def traverse(
|
212
|
+
def traverse(&block)
|
213
|
+
return enum_for(:traverse) unless block_given?
|
214
|
+
|
215
|
+
self.traverse_recurse(&block)
|
216
|
+
end
|
217
|
+
|
218
|
+
protected def traverse_recurse(level = 0, &block)
|
294
219
|
yield self, level
|
295
220
|
|
296
221
|
@children&.each do |child|
|
297
|
-
child.
|
222
|
+
child.traverse_recurse(level + 1, &block)
|
298
223
|
end
|
299
224
|
end
|
300
225
|
|
301
|
-
# Immediately terminate all children tasks, including transient tasks.
|
302
|
-
# Internally invokes `stop(false)` on all children.
|
226
|
+
# Immediately terminate all children tasks, including transient tasks. Internally invokes `stop(false)` on all children. This should be considered a last ditch effort and is used when closing the scheduler.
|
303
227
|
def terminate
|
304
228
|
# Attempt to stop the current task immediately, and all children:
|
305
229
|
stop(false)
|
@@ -310,8 +234,8 @@ module Async
|
|
310
234
|
end
|
311
235
|
end
|
312
236
|
|
313
|
-
# Attempt to stop the current node immediately, including all non-transient children.
|
314
|
-
#
|
237
|
+
# Attempt to stop the current node immediately, including all non-transient children. Invokes {#stop_children} to stop all children.
|
238
|
+
#
|
315
239
|
# @parameter later [Boolean] Whether to defer stopping until some point in the future.
|
316
240
|
def stop(later = false)
|
317
241
|
# The implementation of this method may defer calling `stop_children`.
|
data/lib/async/notification.rb
CHANGED
data/lib/async/reactor.rb
CHANGED
data/lib/async/scheduler.rb
CHANGED
@@ -34,6 +34,12 @@ module Async
|
|
34
34
|
@timers = ::Timers::Group.new
|
35
35
|
end
|
36
36
|
|
37
|
+
def scheduler_close
|
38
|
+
self.run
|
39
|
+
ensure
|
40
|
+
self.close
|
41
|
+
end
|
42
|
+
|
37
43
|
# @public Since `stable-v1`.
|
38
44
|
def close
|
39
45
|
# This is a critical step. Because tasks could be stored as instance variables, and since the reactor is (probably) going out of scope, we need to ensure they are stopped. Otherwise, the tasks will belong to a reactor that will never run again and are not stopped.
|
data/lib/async/semaphore.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2018-2022, by Samuel Williams.
|
5
5
|
|
6
|
+
require_relative 'list'
|
7
|
+
|
6
8
|
module Async
|
7
9
|
# A synchronization primitive, which limits access to a given resource.
|
8
10
|
# @public Since `stable-v1`.
|
@@ -12,7 +14,7 @@ module Async
|
|
12
14
|
def initialize(limit = 1, parent: nil)
|
13
15
|
@count = 0
|
14
16
|
@limit = limit
|
15
|
-
@waiting =
|
17
|
+
@waiting = List.new
|
16
18
|
|
17
19
|
@parent = parent
|
18
20
|
end
|
@@ -73,26 +75,34 @@ module Async
|
|
73
75
|
def release
|
74
76
|
@count -= 1
|
75
77
|
|
76
|
-
while (@limit - @count) > 0 and
|
77
|
-
|
78
|
-
Fiber.scheduler.resume(fiber)
|
79
|
-
end
|
78
|
+
while (@limit - @count) > 0 and node = @waiting.first
|
79
|
+
node.resume
|
80
80
|
end
|
81
81
|
end
|
82
82
|
|
83
83
|
private
|
84
84
|
|
85
|
+
class FiberNode < List::Node
|
86
|
+
def initialize(fiber)
|
87
|
+
@fiber = fiber
|
88
|
+
end
|
89
|
+
|
90
|
+
def resume
|
91
|
+
if @fiber.alive?
|
92
|
+
Fiber.scheduler.resume(@fiber)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private_constant :FiberNode
|
98
|
+
|
85
99
|
# Wait until the semaphore becomes available.
|
86
100
|
def wait
|
87
|
-
|
101
|
+
return unless blocking?
|
88
102
|
|
89
|
-
|
90
|
-
@waiting << fiber
|
103
|
+
@waiting.stack(FiberNode.new(Fiber.current)) do
|
91
104
|
Fiber.scheduler.transfer while blocking?
|
92
105
|
end
|
93
|
-
rescue Exception
|
94
|
-
@waiting.delete(fiber)
|
95
|
-
raise
|
96
106
|
end
|
97
107
|
end
|
98
108
|
end
|
data/lib/async/task.rb
CHANGED
@@ -111,7 +111,10 @@ module Async
|
|
111
111
|
end
|
112
112
|
end
|
113
113
|
|
114
|
+
# Run an asynchronous task as a child of the current task.
|
114
115
|
def async(*arguments, **options, &block)
|
116
|
+
raise "Cannot create child task within a task that has finished execution!" if self.finished?
|
117
|
+
|
115
118
|
task = Task.new(self, **options, &block)
|
116
119
|
|
117
120
|
task.run(*arguments)
|
@@ -119,26 +122,28 @@ module Async
|
|
119
122
|
return task
|
120
123
|
end
|
121
124
|
|
122
|
-
# Retrieve the current result of the task. Will cause the caller to wait until result is available.
|
123
|
-
#
|
125
|
+
# Retrieve the current result of the task. Will cause the caller to wait until result is available. If the result was an exception, raise that exception.
|
126
|
+
#
|
127
|
+
# Conceptually speaking, waiting on a task should return a result, and if it throws an exception, this is certainly an exceptional case that should represent a failure in your program, not an expected outcome. In other words, you should not design your programs to expect exceptions from `#wait` as a normal flow control, and prefer to catch known exceptions within the task itself and return a result that captures the intention of the failure, e.g. a `TimeoutError` might simply return `nil` or `false` to indicate that the operation did not generate a valid result (as a timeout was an expected outcome of the internal operation in this case).
|
128
|
+
#
|
129
|
+
# @raises [RuntimeError] If the task's fiber is the current fiber.
|
124
130
|
# @returns [Object] The final expression/result of the task's block.
|
125
131
|
def wait
|
126
|
-
raise "Cannot wait on own fiber" if Fiber.current.equal?(@fiber)
|
132
|
+
raise "Cannot wait on own fiber!" if Fiber.current.equal?(@fiber)
|
127
133
|
|
128
134
|
if running?
|
129
135
|
@finished ||= Condition.new
|
130
136
|
@finished.wait
|
131
137
|
end
|
132
138
|
|
133
|
-
|
134
|
-
when Exception
|
139
|
+
if @result.is_a?(Exception)
|
135
140
|
raise @result
|
136
141
|
else
|
137
142
|
return @result
|
138
143
|
end
|
139
144
|
end
|
140
145
|
|
141
|
-
# Access the result of the task without waiting. May be nil if the task is not completed.
|
146
|
+
# Access the result of the task without waiting. May be nil if the task is not completed. Does not raise exceptions.
|
142
147
|
attr :result
|
143
148
|
|
144
149
|
# Stop the task and all of its children.
|
@@ -268,7 +273,7 @@ module Async
|
|
268
273
|
|
269
274
|
# If this task was being used as a future, signal completion here:
|
270
275
|
if @finished
|
271
|
-
@finished.signal(
|
276
|
+
@finished.signal(self)
|
272
277
|
end
|
273
278
|
end
|
274
279
|
|
data/lib/async/version.rb
CHANGED
data/lib/async/waiter.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
A synchronization primitive, which allows you to wait for tasks to complete in order of completion. This is useful for implementing a task pool, where you want to wait for the first task to complete, and then cancel the rest.
|
2
|
+
|
3
|
+
If you try to wait for more things than you have added, you will deadlock.
|
4
|
+
|
5
|
+
## Example
|
6
|
+
|
7
|
+
~~~ ruby
|
8
|
+
require 'async'
|
9
|
+
require 'async/semaphore'
|
10
|
+
require 'async/barrier'
|
11
|
+
require 'async/waiter'
|
12
|
+
|
13
|
+
Sync do
|
14
|
+
barrier = Async::Barrier.new
|
15
|
+
waiter = Async::Waiter.new(parent: barrier)
|
16
|
+
semaphore = Async::Semaphore.new(2, parent: waiter)
|
17
|
+
|
18
|
+
# Sleep sort the numbers:
|
19
|
+
generator = Async do
|
20
|
+
while true
|
21
|
+
semaphore.async do |task|
|
22
|
+
number = rand(1..10)
|
23
|
+
sleep(number)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
numbers = []
|
29
|
+
|
30
|
+
4.times do
|
31
|
+
# Wait for all the numbers to be sorted:
|
32
|
+
numbers << waiter.wait
|
33
|
+
end
|
34
|
+
|
35
|
+
# Don't generate any more numbers:
|
36
|
+
generator.stop
|
37
|
+
|
38
|
+
# Stop all tasks which we don't care about:
|
39
|
+
barrier.stop
|
40
|
+
|
41
|
+
Console.logger.info("Smallest", numbers)
|
42
|
+
end
|
43
|
+
~~~
|
44
|
+
|
45
|
+
### Output
|
46
|
+
|
47
|
+
~~~
|
48
|
+
0.0s info: Smallest
|
49
|
+
| [3, 3, 1, 2]
|
50
|
+
~~~
|
data/lib/async/waiter.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2022, by Samuel Williams.
|
5
|
+
|
6
|
+
module Async
|
7
|
+
# A composable synchronization primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore} and/or {Barrier}.
|
8
|
+
class Waiter
|
9
|
+
def initialize(parent: nil, finished: Async::Condition.new)
|
10
|
+
@finished = finished
|
11
|
+
@done = []
|
12
|
+
|
13
|
+
@parent = parent
|
14
|
+
end
|
15
|
+
|
16
|
+
# Execute a child task and add it to the waiter.
|
17
|
+
# @asynchronous Executes the given block concurrently.
|
18
|
+
def async(parent: (@parent or Task.current), &block)
|
19
|
+
parent.async do |task|
|
20
|
+
yield(task)
|
21
|
+
ensure
|
22
|
+
@done << task
|
23
|
+
@finished.signal
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Wait for the first `count` tasks to complete.
|
28
|
+
# @parameter count [Integer | Nil] The number of tasks to wait for.
|
29
|
+
# @returns [Array(Async::Task)] If an integer is given, the tasks which have completed.
|
30
|
+
# @returns [Async::Task] Otherwise, the first task to complete.
|
31
|
+
def first(count = nil)
|
32
|
+
minimum = count || 1
|
33
|
+
|
34
|
+
while @done.size < minimum
|
35
|
+
@finished.wait
|
36
|
+
end
|
37
|
+
|
38
|
+
return @done.shift(*count)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Wait for the first `count` tasks to complete.
|
42
|
+
# @parameter count [Integer | Nil] The number of tasks to wait for.
|
43
|
+
def wait(count = nil)
|
44
|
+
if count
|
45
|
+
first(count).map(&:wait)
|
46
|
+
else
|
47
|
+
first.wait
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/kernel/async.rb
CHANGED
data/lib/kernel/sync.rb
CHANGED
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
@@ -54,7 +54,7 @@ cert_chain:
|
|
54
54
|
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
55
55
|
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
56
56
|
-----END CERTIFICATE-----
|
57
|
-
date: 2022-
|
57
|
+
date: 2022-12-04 00:00:00.000000000 Z
|
58
58
|
dependencies:
|
59
59
|
- !ruby/object:Gem::Dependency
|
60
60
|
name: console
|
@@ -76,14 +76,14 @@ dependencies:
|
|
76
76
|
requirements:
|
77
77
|
- - "~>"
|
78
78
|
- !ruby/object:Gem::Version
|
79
|
-
version: 1.1
|
79
|
+
version: '1.1'
|
80
80
|
type: :runtime
|
81
81
|
prerelease: false
|
82
82
|
version_requirements: !ruby/object:Gem::Requirement
|
83
83
|
requirements:
|
84
84
|
- - "~>"
|
85
85
|
- !ruby/object:Gem::Version
|
86
|
-
version: 1.1
|
86
|
+
version: '1.1'
|
87
87
|
- !ruby/object:Gem::Dependency
|
88
88
|
name: timers
|
89
89
|
requirement: !ruby/object:Gem::Requirement
|
@@ -174,28 +174,42 @@ dependencies:
|
|
174
174
|
requirements:
|
175
175
|
- - "~>"
|
176
176
|
- !ruby/object:Gem::Version
|
177
|
-
version:
|
177
|
+
version: 0.18.3
|
178
178
|
type: :development
|
179
179
|
prerelease: false
|
180
180
|
version_requirements: !ruby/object:Gem::Requirement
|
181
181
|
requirements:
|
182
182
|
- - "~>"
|
183
183
|
- !ruby/object:Gem::Version
|
184
|
-
version:
|
184
|
+
version: 0.18.3
|
185
185
|
- !ruby/object:Gem::Dependency
|
186
|
-
name:
|
186
|
+
name: sus
|
187
187
|
requirement: !ruby/object:Gem::Requirement
|
188
188
|
requirements:
|
189
189
|
- - "~>"
|
190
190
|
- !ruby/object:Gem::Version
|
191
|
-
version: '
|
191
|
+
version: '0.15'
|
192
192
|
type: :development
|
193
193
|
prerelease: false
|
194
194
|
version_requirements: !ruby/object:Gem::Requirement
|
195
195
|
requirements:
|
196
196
|
- - "~>"
|
197
197
|
- !ruby/object:Gem::Version
|
198
|
-
version: '
|
198
|
+
version: '0.15'
|
199
|
+
- !ruby/object:Gem::Dependency
|
200
|
+
name: sus-fixtures-async
|
201
|
+
requirement: !ruby/object:Gem::Requirement
|
202
|
+
requirements:
|
203
|
+
- - ">="
|
204
|
+
- !ruby/object:Gem::Version
|
205
|
+
version: '0'
|
206
|
+
type: :development
|
207
|
+
prerelease: false
|
208
|
+
version_requirements: !ruby/object:Gem::Requirement
|
209
|
+
requirements:
|
210
|
+
- - ">="
|
211
|
+
- !ruby/object:Gem::Version
|
212
|
+
version: '0'
|
199
213
|
description:
|
200
214
|
email:
|
201
215
|
executables: []
|
@@ -208,6 +222,7 @@ files:
|
|
208
222
|
- lib/async/clock.rb
|
209
223
|
- lib/async/condition.md
|
210
224
|
- lib/async/condition.rb
|
225
|
+
- lib/async/list.rb
|
211
226
|
- lib/async/node.rb
|
212
227
|
- lib/async/notification.rb
|
213
228
|
- lib/async/queue.rb
|
@@ -218,6 +233,8 @@ files:
|
|
218
233
|
- lib/async/task.rb
|
219
234
|
- lib/async/variable.rb
|
220
235
|
- lib/async/version.rb
|
236
|
+
- lib/async/waiter.md
|
237
|
+
- lib/async/waiter.rb
|
221
238
|
- lib/async/wrapper.rb
|
222
239
|
- lib/kernel/async.rb
|
223
240
|
- lib/kernel/sync.rb
|
metadata.gz.sig
CHANGED
Binary file
|