Skip to content

Commit 0d5812d

Browse files
authored
Merge 5e0830f into 2d426d7
2 parents 2d426d7 + 5e0830f commit 0d5812d

1 file changed

Lines changed: 233 additions & 0 deletions

File tree

proposal/2-concurrency.md

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# Proposal: Concurrency on Nash
2+
3+
There has been some discussion on how to provide concurrency to nash.
4+
There is a [discussion here](https://github.com/NeowayLabs/nash/issues/224)
5+
on how concurrency could be added as a set of built-in functions.
6+
7+
As we progressed discussing it seemed desirable to have a concurrency
8+
that enforced no sharing between concurrent functions. It eliminates
9+
races and forces all communication to happen explicitly, and the
10+
performance overhead would not be a problem to a high level language
11+
as nash.
12+
13+
## Lightweight Processes
14+
15+
This idea is inspired on Erlang concurrency model. Since Nash does
16+
not aspire to do everything that Erlang does (like distributed programming)
17+
so this is not a copy, we just take some things as inspiration.
18+
19+
Why call this a process ? On the [Erlang docs](http://erlang.org/doc/getting_started/conc_prog.html)
20+
there is a interesting definition of process:
21+
22+
```
23+
the term "process" is usually used when the threads of execution share no
24+
data with each other and the term "thread" when they share data in some way.
25+
Threads of execution in Erlang share no data,
26+
that is why they are called processes
27+
```
28+
29+
In this context the process word is used to mean a concurrent thread of
30+
execution that does not share any data. The only means of communication
31+
are through message passing. Since these processes are lightweight
32+
creating a lot of them will be cheap (at least must cheaper than
33+
OS processes).
34+
35+
Instead of using channel instances in this model you send messages
36+
to processes (actor model), it works pretty much like a networking
37+
model using UDP datagrams.
38+
39+
The idea is to leverage this as a syntactic construction of the language
40+
to make it as explicit and easy as possible to use.
41+
42+
This idea introduces 4 new concepts, 3 built-in functions and one
43+
new keyword.
44+
45+
The keyword **spawn** is used to spawn a function as a new process.
46+
The function **send** is used to send messages to a process.
47+
The function **receive** is used to receive messages from a process.
48+
The function **self** returns the pid of the process calling it.
49+
50+
An example of a simple ping/pong:
51+
52+
```
53+
pid <= spawn fn () {
54+
ping, senderpid <= receive()
55+
echo $ping
56+
send($senderpid, "pong")
57+
}()
58+
59+
send($pid, "ping", self())
60+
pong <= receive()
61+
62+
echo $pong
63+
```
64+
65+
Spawned functions can also receive parameters (always deep copies):
66+
67+
```
68+
pid <= spawn fn (answerpid) {
69+
send($answerpid, "pong")
70+
}(self())
71+
72+
pong <= receive()
73+
echo $pong
74+
```
75+
76+
A simple fan-out/fan-in implementation:
77+
78+
```
79+
jobs = ("1" "2" "3" "4" "5")
80+
81+
for job in $jobs {
82+
spawn fn (job, answerpid) {
83+
import io
84+
85+
io_println("job[%s] done", $job)
86+
send($answerpid, format("result [%s]", $job))
87+
}($job, self())
88+
}
89+
90+
for job in $jobs {
91+
result <= receive()
92+
echo $result
93+
}
94+
```
95+
96+
### Error Handling
97+
98+
Error handling on this concurrency model is very similar to
99+
how we do it on a distributed system. If a remote service fails and
100+
just dies and you are using UDP you will never be informed of it,
101+
the behavior will be to timeout the request and try again (possibly
102+
to another service instance through a load balancer).
103+
104+
To implement this idea we can add a timeout to the receive an add
105+
a new parameter, a boolean, indicating if there is a message or if a
106+
timeout has occurred.
107+
108+
Example:
109+
110+
```
111+
msg, ok <= receive(timeout)
112+
if !ok {
113+
echo "oops timeout"
114+
}
115+
```
116+
117+
The timeout can be omitted if you wish to just wait forever.
118+
119+
For send operations we need to add just one boolean return value indicating
120+
if the process pid exists and the message has been delivered:
121+
122+
```
123+
if !send($pid, $msg) {
124+
echo "oops message cant be sent"
125+
}
126+
```
127+
128+
Since the processes are always local there is no need for a more
129+
detailed error message (the message would always be the same), the
130+
error will always involve a pid that has no owner (the process never
131+
existed or already exited).
132+
133+
We could add a more specific error message if we decide that
134+
the process message queue can get too big and we start to
135+
drop messages. The error would help to differentiate
136+
from a dead process or a overloaded process.
137+
138+
An error indicating a overloaded process could help
139+
to implement back pressure logic (try again later).
140+
But if we are sticking with local concurrency only this
141+
may be unnecessary complexity. You can avoid this by
142+
always sending N messages and waiting for N responses
143+
before sending more messages.
144+
145+
146+
### TODO
147+
148+
Spawned functions should have access to imported modules ?
149+
(seems like no, but some usages of this may seem odd)
150+
151+
If send is never blocking, what if process queue gets too big ?
152+
just go on until memory exhausts ?
153+
154+
Not sure if passing parameters in spawn will not make things
155+
inconsistent with function calls
156+
157+
What happens when something is written on the stdout of a spawned
158+
process ? redirect to parent shell ?
159+
160+
161+
## Extend rfork
162+
163+
Converging to a no shared state between concurrent functions initiated
164+
the idea of using the current rfork built-in as a means to express
165+
concurrency on Nash. This would already be possible today, the idea
166+
is just to make it even easier, specially the communication between
167+
different concurrent processes.
168+
169+
This idea enables an even greater amount of isolation between concurrent
170+
processes since rfork enables different namespaces isolation (besides memory),
171+
but it has the obvious fallback of not being very lightweight.
172+
173+
Since the idea of nash is to write simple scripts this does not seem
174+
to be a problem. If it is on the future we can create lightweight concurrent
175+
processes (green threads) that works orthogonally with rfork.
176+
177+
The prototype for the new rfork would be something like this:
178+
179+
```sh
180+
chan <= rfork [ns_param1, ns_param2] (chan) {
181+
//some code
182+
}
183+
```
184+
185+
The code on the rfork block does not have access to the
186+
lexical outer scope but it receives as a parameter a channel
187+
instance.
188+
189+
This channel instance can be used by the forked processes and
190+
by the creator of the process to communicate. We could use built-in functions:
191+
192+
```sh
193+
chan <= rfork [ns_param1, ns_param2] (chan) {
194+
cwrite($chan, "hi")
195+
}
196+
197+
a <= cread($chan)
198+
```
199+
200+
Or some syntactic extension:
201+
202+
```sh
203+
chan <= rfork [ns_param1, ns_param2] (chan) {
204+
$chan <- "hi"
205+
}
206+
207+
a <= <-$chan
208+
```
209+
210+
Since this channel is meant only to be used to communicate with
211+
the created process, it will be closed when the process exit:
212+
213+
```sh
214+
chan <= rfork [ns_param1, ns_param2] (chan) {
215+
}
216+
217+
# returns empty string when channel is closed
218+
<-$chan
219+
```
220+
221+
Fan out and fan in should be pretty trivial:
222+
223+
```sh
224+
chan1 <= rfork [ns_param1, ns_param2] (chan) {
225+
}
226+
227+
chan2 <= rfork [ns_param1, ns_param2] (chan) {
228+
}
229+
230+
# waiting for both to finish
231+
<-$chan1
232+
<-$chan2
233+
```

0 commit comments

Comments
 (0)