File: concurrency.adoc

package info (click to toggle)
nickle 2.107
  • links: PTS
  • area: main
  • in suites: sid
  • size: 3,756 kB
  • sloc: ansic: 27,954; yacc: 1,874; lex: 954; sh: 204; makefile: 13; lisp: 1
file content (232 lines) | stat: -rw-r--r-- 6,358 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
= Threads and Mutual Exclusion in Nickle

== Basic threading

Threads provide concurrent processing of calculations.  They are
created with the `fork` operator, which spawns a child thread to
evaluate its argument and returns it as a variable of the first-class
type `thread`:

`fork` _expr_

The thread it returns is typically stored like this: 

----
thread t = fork x!;
----

In the above example, `fork` immediately returns a thread, which is
stored in `t`.  That thread will calculate the factorial of `x` in the
background while the program continues; when the calculation is
finished it will block and wait for the parent thread (the one that
forked it) to kill, join, or otherwise recognize it.

_Threads share names_; if a thread changes the value of a variable,
that change will occur in the other threads as well.  See Mutual
exclusion below.

== Thread functions

The builtin namespace `Thread` has functions for manipulating threads
once they have been forked.

`int kill ( thread t, ... )`

Kills the threads it takes as arguments, regardless of whether or not
they are finished, and returns the number of threads successfully
killed.

`poly join ( thread t )`

Waits for thread `t` to finish and returns the value of its expression.
This is how to get the value back out of a thread.
Once joined, the thread will dissappear.
For example, 

----
thread t = fork 1000!;
# something else...
printf("1000! = %d\n", Thread::join(t));
----

will execute `something else` while `t` runs, then wait for it to
complete and print out its value.

`thread current ()`

Returns the currently running thread.  Note that things such as
`kill(current())` and `join(current())` are allowed, although the
former merely exits and the latter hangs forever; watch out for these
errors.

`int set_priority ( thread t, int i )` +
`int get_priority ( thread t )`

Priorities determine how runtime is divided among threads; a thread
with higher priority will always run before one with a lower
priority. `set_priority` sets the priority of `t` to `i` and returns
the new priority. `get_priority` returns the priority of thread `t`.

== Mutual exclusion

Consider the following situation: 

----
import Thread;

void function para() {
        printf("My next statement will be false.\n");
}

void function dox() {
        printf("My previous statement was true.\n");
}

thread t = fork para();
thread s = fork dox();
join(t);
join(s);
----

When run, this prints out the less than clear message 

----
MMyy  nperxetv isotuast esmteantte mweinltl  wbaes  ftarlusee..
----

Why? Because the two threads are running simultaneously and take turns
printing out their messages; the result is that they are interleaved
unreadably.  The solution is in the builtin namespace `Mutex`.  A
mutex is a first-class object which threads can use to coordinate
conflicting sections of code.  `Mutex` defines the following functions:

`mutex new ()`

Creates a new mutex and returns it. 

`bool acquire ( mutex m )` +
`bool try_acquire ( mutex m )`

`acquire` blocks until `m` is free, then locks it and returns true.
At the top of the conflicting code, each thread should acquire the
mutex; since only one at a time can have it, they will take turns
executing.  There is also a `try_acquire` that returns false
immediately rather than blocking if `m` is in use, but it is
deprecated.

`void release ( mutex m )`

When the thread which owns a mutex leaves the conflicting section of
code, it should call release to free it for the next thread to acquire
it.

`mutex_owner owner ( mutex m )`

Returns the owner of `m`: either the thread which currently owns it or null if it is free. 

=== An example

This is how the example above might work with mutual exclusion: 

----
import Mutex;
import Thread;

mutex m = new();

void function para() {
        acquire(m);
        printf("My next statement will be false.\n");
        release(m);
}

void function dox() {
        acquire(m);
        printf("My previous statement was true.\n");
        release(m);
}

thread t = fork para();
thread s = fork dox();
join(t);
join(s);
----

This prints out, as expected, 

----
My next statement will be false.
My previous statement was true.
----

== Semaphores

Nickle also has counting semaphores, implemented in the Semaphore
namespace.  Semaphores are similar to mutexes, but have some number of
threads that may run that isn't necessarily one, as it is with
mutexes.  A semaphore with a count of one behaves just like a mutex.

`semaphore new ( int c )`

Semaphores are created with `new`, which is unlike `Mutex::new` in
that it takes an argument: the number of threads it will run
simultaneously.

`void wait ( semaphore s )` +
`void signal ( semaphore s )`

`wait` decrements the count of `s`, which starts with the initial
value specified by `new`.  If the count, after the decrement, is
positive or zero, the thread continues to run; if it is negative, it
blocks until the count becomes non-negative again.  This will occur
when one of the running threads calls `signal`, which increments the
count of `s` and wakes up another thread if any are waiting.

=== Negative initial counts

If `new` is called with a negative initial count, much of the meaning
of the semaphore is inverted.  The count now refers to the number of
threads that must wait until one can execute; that is, the first `c`
threads will block, and the `c` + 1th will execute.

=== Why semaphores?

Semaphores are useful in situations where several threads can run
simultaneously, but not more than a certain number.  They would be
great, for instance, to work in a licensing system, where each thread
needs some command, but only a certain number may run at a given time.

=== Be careful

Semaphores, unlike mutexes, are very error-prone.  They are not
`owned`, in the sense that mutexes are, and therefore do not check
what threads are signalling or waiting on them.  Thus, situations like
this are possible:

----

> import Semaphore;
> semaphore s = new(3);
> s  
semaphore 1 (3);
> wait(s);       
> s
semaphore 1 (2);
> wait(s);
> s
semaphore 1 (1);
> s
semaphore 1 (1)
> for(int i=0; i  100; ++i)
+   signal(s);
> s
semaphore 1 (101)
> wait(s)
> s
semaphore 1 (100)
>
----

Therefore, code must be written carefully so that threads do not
signal the semaphore more than once, and only once they have waited on
it.