polyphony 0.27 → 0.28
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -1
- data/CHANGELOG.md +13 -0
- data/Gemfile +12 -1
- data/Gemfile.lock +83 -5
- data/Rakefile +4 -0
- data/TODO.md +11 -20
- data/docs/_config.yml +15 -0
- data/docs/_includes/nav.html +47 -0
- data/docs/_sass/custom/custom.scss +5 -0
- data/docs/_sass/overrides.scss +45 -0
- data/docs/assets/img/echo-fibers.svg +1 -0
- data/docs/assets/img/sleeping-fiber.svg +1 -0
- data/docs/faq.md +182 -0
- data/docs/getting-started/installing.md +10 -2
- data/docs/getting-started/tutorial.md +333 -26
- data/docs/getting-started.md +10 -0
- data/docs/index.md +91 -0
- data/docs/technical-overview/concurrency.md +78 -16
- data/docs/technical-overview/design-principles.md +7 -0
- data/docs/technical-overview/exception-handling.md +57 -9
- data/docs/technical-overview/extending.md +7 -0
- data/docs/technical-overview/fiber-scheduling.md +128 -18
- data/docs/technical-overview.md +10 -0
- data/docs/user-guide/web-server.md +7 -0
- data/docs/user-guide.md +10 -0
- data/examples/core/xx-deadlock.rb +8 -0
- data/examples/core/xx-state-machine.rb +51 -0
- data/examples/core/xx-trace.rb +80 -0
- data/examples/interfaces/pg_notify.rb +35 -0
- data/examples/io/xx-httparty.rb +31 -6
- data/examples/io/xx-irb.rb +1 -11
- data/examples/io/xx-switch.rb +15 -0
- data/ext/gyro/gyro.c +77 -38
- data/ext/gyro/gyro.h +15 -5
- data/ext/gyro/gyro_ext.c +3 -0
- data/ext/gyro/thread.c +47 -32
- data/ext/gyro/tracing.c +11 -0
- data/lib/polyphony/core/global_api.rb +11 -4
- data/lib/polyphony/core/supervisor.rb +1 -0
- data/lib/polyphony/core/thread_pool.rb +44 -35
- data/lib/polyphony/extensions/fiber.rb +19 -9
- data/lib/polyphony/extensions/io.rb +14 -14
- data/lib/polyphony/extensions/socket.rb +3 -3
- data/lib/polyphony/irb.rb +13 -0
- data/lib/polyphony/postgres.rb +15 -0
- data/lib/polyphony/trace.rb +98 -0
- data/lib/polyphony/version.rb +1 -1
- data/lib/polyphony.rb +1 -0
- data/polyphony.gemspec +21 -12
- data/test/helper.rb +3 -2
- data/test/test_fiber.rb +53 -3
- data/test/test_global_api.rb +12 -0
- data/test/test_gyro.rb +2 -2
- data/test/test_supervisor.rb +12 -0
- data/test/test_thread.rb +12 -0
- data/test/test_thread_pool.rb +75 -0
- data/test/test_throttler.rb +6 -0
- data/test/test_trace.rb +66 -0
- metadata +99 -9
- data/docs/README.md +0 -36
- data/docs/summary.md +0 -60
- data/docs/technical-overview/faq.md +0 -97
@@ -1,9 +1,143 @@
|
|
1
|
-
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: A Gentle Introduction to Polyphony
|
4
|
+
nav_order: 2
|
5
|
+
parent: Getting Started
|
6
|
+
permalink: /getting-started/tutorial/
|
7
|
+
---
|
8
|
+
# A Gentle Introduction to Polyphony
|
9
|
+
|
10
|
+
Polyphony is a new Ruby library aimed at making writing concurrent Ruby apps
|
11
|
+
easy and fun. In this article, we'll introduce Polyphony's fiber-based
|
12
|
+
concurrency model, some of Polyphony's API, and demonstrate how to solve some
|
13
|
+
simple situations related to concurrent computing.
|
14
|
+
|
15
|
+
## What are Fibers and What are They Good For?
|
16
|
+
|
17
|
+
Fibers are some of Ruby's most underappreciated hidden gems. Up until now,
|
18
|
+
fibers have been used mostly as the underlying mechanism for implementing
|
19
|
+
lazy enumerators and asynchronous generators. Fibers encapsulate, in short,
|
20
|
+
an execution context that can be paused and resumed at will.
|
21
|
+
|
22
|
+
Fibers are also at the heart of Polyphony's concurrency model. Polyphony employs
|
23
|
+
fibers as a way to run multiple tasks at once, each task advancing at its own
|
24
|
+
pace, pausing when waiting for an event to occur, and automatically resuming
|
25
|
+
when that event has occurred.
|
26
|
+
|
27
|
+
Take for example a web app: in order to fulfil an incoming request, multiple
|
28
|
+
steps are required: querying the database, fetching cached entries from Redis,
|
29
|
+
talking to third-party services such as Twilio or AWS S3. Each step can last
|
30
|
+
tens of milliseconds, and blocks the current thread. Such an app is said to be
|
31
|
+
I/O-bound, that is, it mostly spends its time waiting for some external
|
32
|
+
services.
|
33
|
+
|
34
|
+
The traditional approach to handling multiple requests concurrently is to employ
|
35
|
+
multiple threads or processes, but this approach has numerous disavantages:
|
36
|
+
|
37
|
+
- Both threads and processes are heavyweight, in both memory consmption and
|
38
|
+
the cost associated with context-switching.
|
39
|
+
- Threads introduce hard-to-debug race conditions, and do not offer true
|
40
|
+
parallelism, owing to Ruby's GVL.
|
41
|
+
- Processes are more difficult to coordinate, since they do not share memory.
|
42
|
+
- Both threads and processes are limited to a few thousand at best on a single
|
43
|
+
machine. Trying to spawn a thread per client essentially limits the scaling
|
44
|
+
capacity of your system.
|
45
|
+
|
46
|
+
Polyphony eschews both threads and processes in favor of fibers as the basic
|
47
|
+
unit of concurrency. The idea is that any time a blocking I/O operation occurs,
|
48
|
+
the current fiber is paused, and another fiber which has been marked as
|
49
|
+
*runnable* is resumed. This way, your Ruby code can keep on handling incoming
|
50
|
+
HTTP requests as they come with a scaling capacity that is virtually only
|
51
|
+
limited by available memory.
|
52
|
+
|
53
|
+
## Switchpoints and the Fiber-Switching Dance
|
54
|
+
|
55
|
+
In order to make pausing and resuming fibers completely automatic and painfree,
|
56
|
+
we need to know when an operation is going to block, and when it can be
|
57
|
+
completed without blocking. Operations that might block execution are considered
|
58
|
+
*switchpoints*. A switchpoint is a point in time at which control might switch
|
59
|
+
from the currently running fiber to another fiber that is in a runnable state.
|
60
|
+
Switchpoints may occur in any of the following cases:
|
61
|
+
|
62
|
+
- On a call to any blocking operation, such as `#sleep`, `Fiber#await`,
|
63
|
+
`Thread#join` etc.
|
64
|
+
- On fiber termination
|
65
|
+
- On a call to `#suspend`
|
66
|
+
- On a call to `#snooze`
|
67
|
+
- On a call to `Thread#switch_fiber`
|
68
|
+
|
69
|
+
At any switchpoint, the following takes place:
|
70
|
+
|
71
|
+
- Check if any fiber is runnable, that is, ready to continue processing.
|
72
|
+
- If no fiber is runnable, watch for events (see below) and wait for at least
|
73
|
+
one fiber to become runnable.
|
74
|
+
- Pause the current fiber and switch to the first runnable fiber, which resumes
|
75
|
+
at the point it was last paused.
|
76
|
+
|
77
|
+
The automatic switching between fibers is complemented by employing
|
78
|
+
[libev](http://software.schmorp.de/pkg/libev.html), a multi-platform high
|
79
|
+
performance event reactor that allows listening to I/O, timer and other events.
|
80
|
+
At every switchpoint where no fibers are runnable, the libev evet loop is run
|
81
|
+
until events occur, which in turn cause the relevant fibers to become runnable.
|
82
|
+
|
83
|
+
Let's examine a simple example:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
require 'polyphony'
|
87
|
+
|
88
|
+
spin do
|
89
|
+
puts "Going to sleep..."
|
90
|
+
sleep 1
|
91
|
+
puts "Woke up"
|
92
|
+
end
|
93
|
+
|
94
|
+
suspend
|
95
|
+
puts "We're done"
|
96
|
+
```
|
97
|
+
|
98
|
+
The above program does nothing exceptional, it just sleeps for 1 second and
|
99
|
+
prints a bunch of messages. But it is enough to demonstrate how concurrency
|
100
|
+
works in Polyphony. Here's a flow chart of the transfer of control:
|
101
|
+
|
102
|
+
<p class="img-figure"><img src="../../assets/img/sleeping-fiber.svg"></p>
|
103
|
+
|
104
|
+
Here's the actual sequence of execution (in pseudo-code)
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
# (main fiber)
|
108
|
+
sleeper = spin { ... } # The main fiber spins up a new fiber marked as runnable
|
109
|
+
suspend # The main fiber suspends, waiting for all other work to finish
|
110
|
+
Thread.current.switch_fiber # Polyphony looks for other runnable fibers
|
111
|
+
|
112
|
+
# (sleeper fiber)
|
113
|
+
puts "Going to sleep..." # The sleeper fiber starts running
|
114
|
+
sleep 1 # The sleeper fiber goes to sleep
|
115
|
+
Gyro::Timer.new(1, 0).await # A timer event watcher is setup and yields
|
116
|
+
Thread.current.switch_fiber # Polyphony looks for other runnable fibers
|
117
|
+
Thread.current.selector.run # With no work left, the event loop is ran
|
118
|
+
fiber.schedule # The timer event fires, scheduling the sleeper fiber
|
119
|
+
# <= The sleep method returns
|
120
|
+
puts "Woke up"
|
121
|
+
Thread.current.switch_fiber # With the fiber done, Polyphony looks for work
|
122
|
+
|
123
|
+
# with no more work, control is returned to the main fiber
|
124
|
+
# (main fiber)
|
125
|
+
# <=
|
126
|
+
# With no more work left, the main fiber is resumed and the suspend call returns
|
127
|
+
puts "We're done"
|
128
|
+
```
|
129
|
+
|
130
|
+
What we have done in fact is we multiplexed two different contexts of execution
|
131
|
+
(fibers) onto a single thread, each fiber continuing at its own pace and
|
132
|
+
yielding control when waiting for something to happen. This context-switching
|
133
|
+
dance, performed automatically by Polyphony behind the scenes, enables building
|
134
|
+
highly-concurrent Ruby apps, with minimal impact on performance.
|
2
135
|
|
3
136
|
## Building a Simple Echo Server with Polyphony
|
4
137
|
|
5
|
-
|
6
|
-
|
138
|
+
Let's now turn our attention to something a bit more useful: a concurrent echo
|
139
|
+
server. Our server will accept TCP connections and send back whatever it receives
|
140
|
+
from the client.
|
7
141
|
|
8
142
|
We'll start by opening a server socket:
|
9
143
|
|
@@ -42,7 +176,7 @@ continue to run until the call to `#handle_client` returns, and that will not
|
|
42
176
|
happen as long as the read loop is not done.
|
43
177
|
|
44
178
|
Fortunately, Polyphony makes it super easy to do more than one thing at once.
|
45
|
-
Let's spin up a separate
|
179
|
+
Let's spin up a separate fiber for each client:
|
46
180
|
|
47
181
|
```ruby
|
48
182
|
while (client = server.accept)
|
@@ -50,31 +184,33 @@ while (client = server.accept)
|
|
50
184
|
end
|
51
185
|
```
|
52
186
|
|
53
|
-
Now, our little program can handle
|
54
|
-
little sprinkling of `spin`.
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
187
|
+
Now, our little program can effectively handle thousands of clients, all with a
|
188
|
+
little sprinkling of `spin`. The call to `server.accept` suspends the main fiber
|
189
|
+
until a connection is made, allowing other fibers to run while it's waiting.
|
190
|
+
Likewise, the call to `client.gets` suspends the *client's fiber* until incoming
|
191
|
+
data becomes available. Again, all of that is handled automatically by
|
192
|
+
Polyphony, and the only hint that our program is concurrent is that little
|
193
|
+
innocent call to `#spin`.
|
59
194
|
|
60
|
-
|
61
|
-
made, allowing other coprocesses to continue running. Likewise, the call to
|
62
|
-
`client.gets` suspends the *client's coprocess* until incoming data becomes
|
63
|
-
available. All this is handled automatically by Polyphony, and the only hint
|
64
|
-
that our program is concurrent is that innocent call to `spin`.
|
195
|
+
Here's a flow chart showing the transfer of control between the different fibers:
|
65
196
|
|
66
|
-
|
197
|
+
<p class="img-figure"><img src="../../assets/img/echo-fibers.svg"></p>
|
198
|
+
|
199
|
+
Let's consider the advantage of the Polyphony concurrency model:
|
67
200
|
|
68
201
|
- We didn't need to create custom handler classes with callbacks.
|
69
202
|
- We didn't need to use custom classes or APIs for our networking code.
|
70
|
-
- Our code is terse, easy to read and
|
71
|
-
|
203
|
+
- Each task is expressed sequentially. Our code is terse, easy to read and, most
|
204
|
+
importantly, expresses the order of events clearly and without having our
|
205
|
+
logic split across different methods.
|
206
|
+
- We have a server that can scale to thousands of clients without breaking a
|
207
|
+
sweat.
|
72
208
|
|
73
209
|
## Handling Inactive Connections
|
74
210
|
|
75
211
|
Now that we have a working concurrent echo server, let's add some bells and
|
76
212
|
whistles. First of all, let's get rid of clients that are not active. We'll do
|
77
|
-
this by using a Polyphony construct called a cancel scope. Cancel
|
213
|
+
this by using a Polyphony construct called a cancel scope. Cancel scopes define
|
78
214
|
an execution context that can cancel any operation ocurring within its scope:
|
79
215
|
|
80
216
|
```ruby
|
@@ -100,9 +236,10 @@ thus reset the cancel scope timer.
|
|
100
236
|
|
101
237
|
In addition, we use an `ensure` block to make sure the client connection is
|
102
238
|
closed, whether or not it was interrupted by the cancel scope timer. The habit
|
103
|
-
of always cleaning up using `ensure` in the face of interruptions is a
|
104
|
-
fundamental element of using Polyphony. It makes your code robust,
|
105
|
-
highly concurrent execution environment
|
239
|
+
of always cleaning up using `ensure` in the face of potential interruptions is a
|
240
|
+
fundamental element of using Polyphony correctly. It makes your code robust,
|
241
|
+
even in a highly chaotic concurrent execution environment where tasks can be
|
242
|
+
interrupted at any time.
|
106
243
|
|
107
244
|
Here's the complete source code for our Polyphony-based echo server:
|
108
245
|
|
@@ -130,8 +267,178 @@ while (client = server.accept)
|
|
130
267
|
end
|
131
268
|
```
|
132
269
|
|
133
|
-
##
|
270
|
+
## Waiting and Interrupting
|
271
|
+
|
272
|
+
Polyphony makes it very easy to run multiple concurrent fibers. You can
|
273
|
+
basically start a fiber for any operation that involves talking to the outside
|
274
|
+
world - running a database query, making an HTTP request, sending off a webhook
|
275
|
+
invocation etc. While it's trivial to spin off thousands of fibers, we'd also
|
276
|
+
like a way to control all those fibers.
|
277
|
+
|
278
|
+
Polyphony provides a number of tools for controlling fiber execution. Let's
|
279
|
+
examine some of these tools and how they work. Suppose we have a fiber that was
|
280
|
+
previously spun:
|
281
|
+
|
282
|
+
```ruby
|
283
|
+
fiber = spin { do_some_work }
|
284
|
+
```
|
285
|
+
|
286
|
+
We can wait for the fiber to terminate:
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
fiber.await # alternatively fiber.join
|
290
|
+
```
|
291
|
+
|
292
|
+
Notice that the await call returns the return value of the fiber block:
|
293
|
+
|
294
|
+
```ruby
|
295
|
+
fiber = spin { 2 + 2}
|
296
|
+
fiber.await #=> 4
|
297
|
+
```
|
298
|
+
|
299
|
+
We can also stop the fiber at any point:
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
fiber.stop # or fiber.interrupt
|
303
|
+
```
|
304
|
+
|
305
|
+
We can inject a return value for the fiber using `stop`:
|
306
|
+
|
307
|
+
```ruby
|
308
|
+
fiber = spin do
|
309
|
+
sleep 1
|
310
|
+
1 + 1
|
311
|
+
end
|
312
|
+
|
313
|
+
spin { puts "1 + 1 = #{fiber.await} wha?" }
|
314
|
+
|
315
|
+
fiber.stop(3)
|
316
|
+
suspend
|
317
|
+
```
|
318
|
+
|
319
|
+
We can also *cancel* the fiber, which raises a `Polyphony::Cancel` exception:
|
320
|
+
|
321
|
+
```ruby
|
322
|
+
fiber.cancel!
|
323
|
+
```
|
324
|
+
|
325
|
+
And finally, we can interrupt the fiber with an exception raised in its current
|
326
|
+
context:
|
327
|
+
|
328
|
+
```ruby
|
329
|
+
fiber.raise 'foo'
|
330
|
+
```
|
331
|
+
|
332
|
+
For more information on how exceptions are handled in Polyphony, see [exception
|
333
|
+
handling](../../technical-overview/exception-handling/).
|
334
|
+
|
335
|
+
## Supervising - controlling multiple fibers at once
|
336
|
+
|
337
|
+
Here's a simple example that we'll use to demonstrate some of the tools provided
|
338
|
+
by Polyphony for controlling fibers. Let's build a script that fetches the local
|
339
|
+
time for multiple time zones:
|
340
|
+
|
341
|
+
```ruby
|
342
|
+
require 'polyphony'
|
343
|
+
require 'httparty'
|
344
|
+
require 'json'
|
345
|
+
|
346
|
+
def get_time(tzone)
|
347
|
+
res = HTTParty.get("http://worldtimeapi.org/api/timezone/#{tzone}")
|
348
|
+
json = JSON.parse(res.body)
|
349
|
+
Time.parse(json['datetime'])
|
350
|
+
end
|
351
|
+
|
352
|
+
zones = %w{
|
353
|
+
Europe/London Europe/Paris Europe/Bucharest America/New_York Asia/Bangkok
|
354
|
+
}
|
355
|
+
zones.each do |tzone|
|
356
|
+
spin do
|
357
|
+
time = get_time(tzone)
|
358
|
+
puts "Time in #{tzone}: #{time}"
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
suspend
|
363
|
+
```
|
364
|
+
|
365
|
+
Now that we're familiar with the use of the `#spin` method, we know that all
|
366
|
+
those HTTP requests will be processed concurrently, and we can expect those 5
|
367
|
+
separate requests to occur within a fraction of a second (depending on our
|
368
|
+
machine's location). Also notice how we just used `httparty` with fiber-level
|
369
|
+
concurrency, without any boilerplate or employing special wrapper classes.
|
370
|
+
|
371
|
+
Just as before, we suspend the main fiber after spinning off the worker fibers,
|
372
|
+
in order to wait for everything else to be done. But if we needed to do other
|
373
|
+
work? For example, we might want to collect the different local times into a
|
374
|
+
hash to be processed later. In that case, we can use a `Supervisor`:
|
375
|
+
|
376
|
+
```ruby
|
377
|
+
def get_times(zones)
|
378
|
+
Polyphony::Supervisor.new do |s|
|
379
|
+
zones.each do |tzone|
|
380
|
+
s.spin { [tzone, get_time(tzone)] }
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
get_times(zones).await.each do |tzone, time|
|
386
|
+
puts "Time in #{tzone}: #{time}"
|
387
|
+
end
|
388
|
+
```
|
389
|
+
|
390
|
+
There's quite a bit going on here, so let's break it down. We first construct a
|
391
|
+
supervisor and spin our fibers in its context using `Supervisor#spin`.
|
392
|
+
|
393
|
+
```ruby
|
394
|
+
Polyphony::Supervisor.new do |s|
|
395
|
+
...
|
396
|
+
s.spin { ... }
|
397
|
+
...
|
398
|
+
end
|
399
|
+
```
|
400
|
+
|
401
|
+
Once our worker fibers are spun, the supervisor can be used to control them. We
|
402
|
+
can wait for all fibers to terminate using `Supervisor#await`, which returns an
|
403
|
+
array with the return values of all fibers (in the above example, each fiber
|
404
|
+
returns the time zone and the local time).
|
405
|
+
|
406
|
+
```ruby
|
407
|
+
results = supervisor.await
|
408
|
+
```
|
409
|
+
|
410
|
+
We can also select the result of the first fiber that has finished executing.
|
411
|
+
All the other fibers will be interrupted:
|
412
|
+
|
413
|
+
```ruby
|
414
|
+
result, fiber = supervisor.select
|
415
|
+
```
|
416
|
+
|
417
|
+
(Notice how `Supervisor#select` returns both the fiber's return value and the
|
418
|
+
fiber itself).
|
419
|
+
|
420
|
+
We can also interrupt all the supervised fibers by using `Supervisor#interrupt`
|
421
|
+
(or `#stop`) just like with single fibers:
|
422
|
+
|
423
|
+
```ruby
|
424
|
+
supervisor.interrupt
|
425
|
+
```
|
426
|
+
|
427
|
+
## What Else Can I Do with Polyphony?
|
428
|
+
|
429
|
+
Polyphony currently provides support for any library that uses Ruby's stock
|
430
|
+
`socket` and `openssl` classes. Polyphony also includes adapters for the `pg`,
|
431
|
+
`redis` and `irb` gems. It also includes an implementation of an integrated HTTP
|
432
|
+
1 / HTTP 2 / websockets web server with support for TLS termination, ALPN
|
433
|
+
protocol selection and preliminary rack support.
|
434
|
+
|
435
|
+
## Fibers are the Future!
|
134
436
|
|
135
|
-
|
136
|
-
|
137
|
-
|
437
|
+
Implementing concurrency at the level of fibers opens up so many new
|
438
|
+
possibilities for Ruby. Polyphony has the performance characteristics and
|
439
|
+
provides the necessary tools for transforming how concurrent Ruby apps are
|
440
|
+
written. Polyphony is still new, and the present documentation is far from being
|
441
|
+
complete. To learn more about Polyphony, read the [technical
|
442
|
+
overview](../../technical-overview/design-principles/). For more examples please
|
443
|
+
consult the [Github
|
444
|
+
repository](https://github.com/digital-fabric/polyphony/tree/master/examples).
|
data/docs/index.md
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: Home
|
4
|
+
nav_order: 1
|
5
|
+
description: Lorem ipsum
|
6
|
+
permalink: /
|
7
|
+
---
|
8
|
+
|
9
|
+
# Polyphony - fine-grained concurrency for Ruby
|
10
|
+
|
11
|
+
> Polyphony \| pəˈlɪf\(ə\)ni \|
|
12
|
+
> 1. _Music_ the style of simultaneously combining a number of parts, each
|
13
|
+
> forming an individual melody and harmonizing with each other.
|
14
|
+
> 2. _Programming_ a Ruby gem for concurrent programming focusing on performance
|
15
|
+
> and developer happiness.
|
16
|
+
|
17
|
+
Polyphony is a library for building concurrent applications in Ruby. Polyphony
|
18
|
+
harnesses the power of [Ruby fibers](https://ruby-doc.org/core-2.5.1/Fiber.html)
|
19
|
+
to provide a cooperative, sequential coroutine-based concurrency model. Under
|
20
|
+
the hood, Polyphony uses [libev](https://github.com/enki/libev) as a
|
21
|
+
high-performance event reactor that provides timers, I/O watchers and other
|
22
|
+
asynchronous event primitives.
|
23
|
+
|
24
|
+
|
25
|
+
## Focused on Developer Happiness
|
26
|
+
|
27
|
+
Polyphony is designed to make concurrent Ruby programming feel natural and
|
28
|
+
fluent. Polyphony reduces the boilerplate usually associated with concurrent
|
29
|
+
programming, and introduces concurrency primitives that are easy to use, easy to
|
30
|
+
understand, and above all idiomatic.
|
31
|
+
|
32
|
+
## Optimized for High Performance
|
33
|
+
|
34
|
+
Polyphony offers high performance for I/O bound Ruby apps. Distributing
|
35
|
+
concurrent tasks over fibers, instead of threads or processes, minimizes memory
|
36
|
+
consumption and reduces the cost of context-switching.
|
37
|
+
|
38
|
+
## Designed for Interoperability
|
39
|
+
|
40
|
+
Polyphony makes it possible to use normal Ruby built-in classes like `IO`, and
|
41
|
+
`Socket` in a concurrent multi-fiber environment. Polyphony takes care of
|
42
|
+
context-switching automatically whenever a blocking call like `Socket#accept`,
|
43
|
+
`IO#read` or `Kernel#sleep` is issued.
|
44
|
+
|
45
|
+
## A Growing Ecosystem
|
46
|
+
|
47
|
+
Polyphony includes a full-blown HTTP server implementation with integrated
|
48
|
+
support for HTTP 1, HTTP 2 and WebSockets, TLS/SSL termination, automatic
|
49
|
+
ALPN protocol selection, and body streaming. Polyphony also includes fiber-aware
|
50
|
+
extensions for PostgreSQL and Redis. More databases and services are forthcoming.
|
51
|
+
|
52
|
+
## Features
|
53
|
+
|
54
|
+
* Co-operative scheduling of concurrent tasks using Ruby fibers.
|
55
|
+
* High-performance event reactor for handling I/O events and timers.
|
56
|
+
* Natural, sequential programming style that makes it easy to reason about
|
57
|
+
concurrent code.
|
58
|
+
* Abstractions and constructs for controlling the execution of concurrent code:
|
59
|
+
supervisors, cancel scopes, throttling, resource pools etc.
|
60
|
+
* Code can use native networking classes and libraries, growing support for
|
61
|
+
third-party gems such as `pg` and `redis`.
|
62
|
+
* Use stdlib classes such as `TCPServer` and `TCPSocket` and `Net::HTTP`.
|
63
|
+
* Competitive performance and scalability characteristics, in terms of both
|
64
|
+
throughput and memory consumption.
|
65
|
+
|
66
|
+
## Prior Art
|
67
|
+
|
68
|
+
Polyphony draws inspiration from the following, in no particular order:
|
69
|
+
|
70
|
+
* [nio4r](https://github.com/socketry/nio4r/) and
|
71
|
+
[async](https://github.com/socketry/async) (Polyphony's C-extension code is
|
72
|
+
largely a spinoff of
|
73
|
+
[nio4r's](https://github.com/socketry/nio4r/tree/master/ext))
|
74
|
+
* The [go scheduler](https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html)
|
75
|
+
* [EventMachine](https://github.com/eventmachine/eventmachine)
|
76
|
+
* [Trio](https://trio.readthedocs.io/)
|
77
|
+
* [Erlang supervisors](http://erlang.org/doc/man/supervisor.html) (and actually,
|
78
|
+
Erlang in general)
|
79
|
+
|
80
|
+
## Going further
|
81
|
+
|
82
|
+
To learn more about using Polyphony to build concurrent applications, read the
|
83
|
+
technical overview below, or look at the [included
|
84
|
+
examples](https://github.com/digital-fabric/polyphony/tree/9e0f3b09213156bdf376ef33684ef267517f06e8/examples/README.md).
|
85
|
+
A thorough reference is forthcoming.
|
86
|
+
|
87
|
+
## Contributing to Polyphony
|
88
|
+
|
89
|
+
Issues and pull requests will be gladly accepted. Please use the git repository
|
90
|
+
at https://github.com/digital-fabric/polyphony as your primary point of
|
91
|
+
departure for contributing.
|
@@ -1,28 +1,86 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: Concurrency the Easy Way
|
4
|
+
nav_order: 2
|
5
|
+
parent: Technical Overview
|
6
|
+
permalink: /technical-overview/concurrency/
|
7
|
+
---
|
1
8
|
# Concurrency the Easy Way
|
2
9
|
|
3
|
-
Concurrency is a major consideration for modern programmers. Applications and
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
Concurrency is a major consideration for modern programmers. Applications and
|
11
|
+
digital platforms are nowadays expected to do multiple things at once: serve
|
12
|
+
multiple clients, process multiple background jobs, talk to multiple external
|
13
|
+
services. Concurrency is the property of our programming environment allowing us
|
14
|
+
to schedule and control multiple ongoing operations.
|
15
|
+
|
16
|
+
Traditionally, concurrency has been achieved by using multiple processes or
|
17
|
+
threads. Both approaches have proven problematic. Processes consume relatively a
|
18
|
+
lot of memory, and are relatively difficult to coordinate. Threads consume less
|
19
|
+
memory than processes and make it difficult to synchronize access to shared
|
20
|
+
resources, often leading to race conditions and memory corruption. Using threads
|
21
|
+
often necessitates either using special-purpose thread-safe data structures, or
|
22
|
+
otherwise protecting shared resource access using mutexes and critical sections.
|
23
|
+
In addition, dynamic languages such as Ruby and Python will synchronize multiple
|
24
|
+
threads using a global interpreter lock, which means thread execution cannot be
|
25
|
+
parallelized. Furthermore, the amount of threads and processes on a single
|
26
|
+
system is relatively limited, to the order of several hundreds or a few thousand
|
27
|
+
at most.
|
28
|
+
|
29
|
+
Polyphony offers a third way to write concurrent programs, by using a Ruby
|
30
|
+
construct called [fibers](https://ruby-doc.org/core-2.6.5/Fiber.html). Fibers,
|
31
|
+
based on the idea of [coroutines](https://en.wikipedia.org/wiki/Coroutine),
|
32
|
+
provide a way to run a computation that can be suspended and resumed at any
|
33
|
+
moment. For example, a computation waiting for a reply from a database can
|
34
|
+
suspend itself, transferring control to another ongoing computation, and be
|
35
|
+
resumed once the database has sent back its reply. Meanwhile, another
|
36
|
+
computation is started that opens a socket to a remote service, and then
|
37
|
+
suspends itself, waiting for the connection to be established.
|
38
|
+
|
39
|
+
This form of concurrency, called cooperative concurrency \(in contrast to
|
40
|
+
pre-emptive concurrency, like threads and processes\), offers many advantages,
|
41
|
+
especially for applications that are [I/O
|
42
|
+
bound](https://en.wikipedia.org/wiki/I/O_bound). Fibers are very lightweight
|
43
|
+
\(starting at about 20KB\), can be context-switched faster than threads or
|
44
|
+
processes, and literally millions of them can be created on a single system -
|
45
|
+
the only limiting factor is available memory.
|
46
|
+
|
47
|
+
Polyphony takes Ruby's fibers and adds a way to schedule and switch between
|
48
|
+
fibers automatically whenever a blocking operation is started, such as waiting
|
49
|
+
for a TCP connection to be established, or waiting for an I/O object to be
|
50
|
+
readable, or waiting for a timer to elapse. In addition, Polyphony patches the
|
51
|
+
stock Ruby classes to support its concurrency model, letting developers use all
|
52
|
+
of Ruby's stdlib, for example `Net::HTTP` and `Mail` while reaping the benefits
|
53
|
+
of lightweight, highly performant, fiber-based concurrency.
|
54
|
+
|
55
|
+
Writing concurrent applications using Polyphony's fiber-based concurrency model
|
56
|
+
offers a significant performance advantage. Computational tasks can be broken
|
57
|
+
down into many fine-grained concurrent operations that cost very little in
|
58
|
+
memory and context-switching time. More importantly, this concurrency model lets
|
59
|
+
developers express their ideas in a sequential manner, leading to source code
|
60
|
+
that is easy to read and reason about.
|
14
61
|
|
15
62
|
## Fibers - Polyphony's basic unit of concurrency
|
16
63
|
|
17
|
-
Polyphony extends the core `Fiber` class with additional functionality that
|
64
|
+
Polyphony extends the core `Fiber` class with additional functionality that
|
65
|
+
allows scheduling, synchronizing, interrupting and otherwise controlling running
|
66
|
+
fibers. Polyphony makes sure any exception raised while a is running is [handled
|
67
|
+
correctly](exception-handling.md). Moreover, fibers can communicate with each
|
68
|
+
other using message passing, turning them into autonomous actors in a
|
69
|
+
fine-grained concurrent environment.
|
18
70
|
|
19
71
|
## Higher-Order Concurrency Constructs
|
20
72
|
|
21
|
-
Polyphony also provides several methods and constructs for controlling multiple
|
73
|
+
Polyphony also provides several methods and constructs for controlling multiple
|
74
|
+
fibers. Methods like `cancel_after` and `move_on_after` allow interrupting a
|
75
|
+
fiber that's blocking on any arbitrary operation.
|
22
76
|
|
23
|
-
Cancel scopes \(borrowed from the brilliant Python library
|
77
|
+
Cancel scopes \(borrowed from the brilliant Python library
|
78
|
+
[Trio](https://trio.readthedocs.io/en/stable/)\) allows cancelling ongoing
|
79
|
+
operations for any reason with more control over cancelling behaviour.
|
24
80
|
|
25
|
-
Supervisors allow controlling multiple fibers. They offer enhanced exception
|
81
|
+
Supervisors allow controlling multiple fibers. They offer enhanced exception
|
82
|
+
handling and can be nested to create complex supervision trees ala
|
83
|
+
[Erlang](https://adoptingerlang.org/docs/development/supervision_trees/).
|
26
84
|
|
27
85
|
Some other constructs offered by Polyphony:
|
28
86
|
|
@@ -41,5 +99,9 @@ Some other constructs offered by Polyphony:
|
|
41
99
|
|
42
100
|
— Yukihiro “Matz” Matsumoto
|
43
101
|
|
44
|
-
Polyphony's goal is to make programmers even happier by offering them an easy
|
102
|
+
Polyphony's goal is to make programmers even happier by offering them an easy
|
103
|
+
way to write concurrent applications in Ruby. Polyphony aims to show that Ruby
|
104
|
+
can be used for developing sufficiently high-performance applications, while
|
105
|
+
offering all the advantages of Ruby, with source code that is easy to read and
|
106
|
+
understand.
|
45
107
|
|