@@ -17,20 +17,89 @@ material, feel free to skip ahead to the
1717:ref: `Motivation <Ada_In_Practice_Idioms_For_Protected_Objects_Motivation >`
1818section for this chapter.
1919
20- Interacting threads require two capabilities: communication and
21- synchronization. There are two forms of this synchronization: mutual exclusion
22- and condition synchronization.
20+ Concurrent programming is more complex (and more fun) than purely sequential
21+ programming. The cause of this complexity is two-fold: 1) the executing
22+ threads' statements are likely interleaved at the assembly language level, and
23+ 2) the order of that interleaving is unpredictable. As a result, developers
24+ cannot know, in general, where in its sequence of statements any given thread is
25+ executing. Developers can only assume that the threads are making finite
26+ progress when they execute.
27+
28+ A consequence of unpredictable interleaving is that the bugs specific to this
29+ type of programming are timing-dependent. Such bugs are said to be
30+ *Heisenbugs * because they "go away when you look at them," i.e., changing the
31+ code |mdash | adding debugging statements or inserting debugger breakpoints
32+ |mdash | changes the timing. The bug might then disappear entirely, or simply
33+ appear elsewhere in time. We developers must reason about the possible effects
34+ of interleaving and design our code to prevent the resulting bugs. (That's why
35+ this is fun.) Such bugs are really design errors.
36+
37+ One of these errors is known as a *race condition *. A race condition is
38+ possible when multiple threads access some shared resource that requires
39+ mutually exclusive access. If we accidentally forget the finite progress
40+ assumption we may incorrectly assume that the threads sharing that resource
41+ will access it serially. Unpredictable execution interleaving cannot support
42+ that assumption.
43+
44+ Race conditions on memory locations are the most common, but the issue is
45+ general in nature, including for example hardware devices and OS files. Hence
46+ the term "resource."
47+
48+ For example, suppose multiple threads concurrently access an output display
49+ device. This device can be ordered to move its cursor to arbitrary points on
50+ the display by writing a specific sequence of bytes to it, including the
51+ numeric values for X and Y coordinates. A common use is to send the "move
52+ cursor to X, Y" sequence and then send the text intended to appear at coordinates
53+ X and Y.
54+
55+ Clearly, this device requires each client thread to have mutually exclusive
56+ access to the device while sending those two byte sequences. Otherwise,
57+ uncoordinated interleaving could result in one thread preempting another
58+ thread in the middle of sending those two sequences. The result would be an
59+ intermingled sequence of bytes sent to the device. (On a graphics display the
60+ chaotic output can be entertaining to observe.)
61+
62+ Memory races on variables are less obvious. Imagine two threads, Thread1 and
63+ Thread2, that both increment a variable visible to both (an integer, let's
64+ say).
65+
66+ Suppose that the shared integer variable has a value of zero. Both threads
67+ increment the variable, so after they do so the new value should be two. The
68+ compiler will use a hardware register to hold and manipulate the variable's
69+ value because of the increased performance over memory accesses. Each thread
70+ has an independent copy of the registers, and will perform the same assembly
71+ instructions:
72+
73+ #. load a register's value from the memory location containing the variable's value
74+ #. increment the register's value
75+ #. store the register's new value back to the variable's memory location.
76+
77+ The two threads' executions may be interleaved in these three steps. It is
78+ therefore possible that Thread1 will execute step 1 and step 2, and then be
79+ preempted by the execution of Thread2. Thread2 also executes those two steps.
80+ As a result, both threads' registers have the new value of 1. Finally, Thread1
81+ and Thread2 perform the third step, both storing a value of 1 to the
82+ variable's memory location. The resulting value of the shared variable will be
83+ 1, rather than 2. The problem appears as long as preemption occurs prior to
84+ step 3.
85+
86+ Another common design bug is assuming that some required program state has been
87+ achieved. For example, for a thread to retrieve some data from a shared
88+ buffer, the buffer must not be empty. Some other thread must already have
89+ inserted data. Likewise, for a thread to insert some data, the buffer must not
90+ be full. Again, the finite progress assumption means that we cannot know
91+ whether either of those two states are achieved.
92+
93+ Therefore, interacting threads require two forms of synchronization: *mutual
94+ exclusion * and *condition synchronization *. These two kinds of synchronization
95+ enable developers to reason rigorously about the execution of their code in the
96+ context of the finite progress assumption.
2397
2498Mutual exclusion synchronization prevents threads that access some shared
25- resource from doing so at the same time. The resource is said to require
26- mutually exclusive access. Without that access control, race conditions are
27- possible, wherein the effect of the statements executed by the threads depends
28- upon the order in which those statements are executed. In a concurrent
29- programming context, the threads' statements are likely interleaved at the
30- assembly language level, in an order that is unpredictable. That interleaving,
31- therefore, must be prevented during the code that accesses the shared resource.
32- Memory race conditions are the most common, but the issue is general, including
33- for example hardware devices and OS files, hence the term "resource."
99+ resource from doing so at the same time, i.e., it provides mutually exclusive
100+ access to that resource. The effect is achieved by ensuring that, while any
101+ given thread is accessing the resource, that execution will not be interleaved
102+ with the execution of any other thread that also accesses that shared resource.
34103
35104Condition synchronization suspends a thread until the condition |mdash | an
36105arbitrary Boolean expression |mdash | is :ada: `True `. Only when the expression
@@ -44,16 +113,21 @@ require mutually exclusive access for both producers and consumers.
44113Furthermore, producers must be blocked (suspended) as long as the given buffer
45114is full, and consumers must be blocked as long as the given buffer is empty.
46115
116+ Concurrent programming languages support mechanisms providing the two forms
117+ of synchronization. In some languages these are explicit constructs; other
118+ languages take different approaches. In any case, developers can apply these
119+ mechanisms to enforce assumptions more specific than simple finite progress.
120+
47121Ada uses the term :ada: `task ` rather than thread so we will use that term from
48- here on. The concept is much the same.
122+ here on.
49123
50124The protected procedures and protected entries declared by a protected object
51125(PO) automatically execute with mutually exclusive access to the entire
52126protected object. No other caller task can be executing these operations at the
53127same time, so execution of the procedure or entry body statements will not be
54128interleaved. (Functions are special because they have read-only access to the
55129data in the PO.) Therefore, there can be no race conditions on the data
56- encapsulated within it. Even if no data are declared inside , these operations
130+ encapsulated within it. Even if the protected object has no encapsulated data , these operations
57131always execute with mutually exclusive access. During such execution we can say
58132that the PO is locked, speaking informally, because all other caller tasks are
59133held pending.
@@ -75,7 +149,7 @@ from the caller task's point of view there is only one call being made.
75149The requeue statement may not be familiar to many readers. To explain
76150its semantics we first need to provide its rationale.
77151
78- Ada synchronization constructs are based on avoidance synchronization, meaning
152+ Ada synchronization constructs are based on * avoidance synchronization * , meaning
79153that:
80154
81155#. the user-written controls that enable/disable the execution of protected
@@ -96,7 +170,7 @@ sufficient, but not always. If we can't know precisely what the operations will
96170do when we write the code, avoidance synchronization won't suffice.
97171
98172The :ada: `requeue ` statement is employed when avoidance synchronization is
99- insufficient . A task calling an entry that executes a requeue statement is much
173+ not sufficient . A task calling an entry that executes a requeue statement is much
100174like a person calling a large company on the telephone. Calling the main number
101175connects you to a receptionist (if you're lucky and don't get an annoying
102176menu). If the receptionist can answer your question, they do so and then you
@@ -117,8 +191,8 @@ requeue target, i.e., the second entry.
117191
118192A requeue statement is not required in all cases but, as you will see,
119193sometimes it is essential. Note that protected procedures cannot execute
120- requeue statements, only protected entries can do so.
121-
194+ requeue statements, only protected entries can do so. Protected procedures
195+ are appropriate when only mutual exclusion is required (to update encapsulated data).
122196
123197.. _Ada_In_Practice_Idioms_For_Protected_Objects_Motivation :
124198
@@ -138,7 +212,7 @@ itself. If the application requires what amounts to an atomic action with two
138212task participants, then the rendezvous meets this requirement nicely.
139213
140214But, as a synchronous mechanism, the rendezvous is far too expensive when only
141- an asynchronous mechanism is required. Older mechanisms used for asynchronous
215+ an asynchronous mechanism (involving only one task) is required. Older mechanisms used for asynchronous
142216interactions, such as semaphores, mutexes, and condition variables, are nowhere
143217near as simple and robust for clients, but are much faster.
144218
@@ -244,6 +318,17 @@ caller task can continue execution with their unique serial number copy. No
244318race conditions are possible and the shared serial number value increments
245319safely each time :ada: `Get_Next ` is called.
246320
321+ Note the robust nature of a protected object's procedural interface: clients
322+ simply call the protected procedures, entries, or functions. The called
323+ procedure or entry body, when it executes, will always do so with mutually
324+ exclusive access. (Functions can have some additional semantics that we can
325+ ignore here.) There is no explicit lower level synchronization mechanism for
326+ the client to acquire and release. The semantics of protected objects are
327+ implemented by the underlying Ada run-time library, hence all error cases are
328+ also covered. This procedural interface, with automatic implementation for
329+ mutual exclusion, is one of the significant benefits of the monitor construct
330+ on which protected objects are based.
331+
247332
248333Second Idiom Category: Developer-Defined Concurrency Abstractions
249334~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -536,7 +621,7 @@ the :ada:`Scope_Lock` type.
536621The implementation of type :ada: `Mutex ` above doesn't have quite the full
537622canonical semantics. So far it is really just that of a binary semaphore. In
538623particular, a mutex should only be released by the same task that previously
539- acquired it, i.e., the current owner. We can implement that as a fuller
624+ acquired it, i.e., the current owner. We can implement that consistency check in a fuller
540625illustration of this example, one that raises an exception if the caller to
541626:ada: `Release ` is not the current owner of the :ada: `Mutex ` object.
542627
@@ -655,13 +740,13 @@ The barrier for entry :ada:`Acquire` is always set to :ada:`True` because the
655740test for availability is not possible until the body begins to execute. If the
656741PO is not available, the caller task is requeued onto the :ada: `Retry ` entry.
657742(A barrier set to :ada: `True ` like this is a strong indicator of a requeue
658- operation.) The :ada: `Retry ` entry will allow a caller to return |mdash | in the
659- caller's point of view, from the call to :ada: `Acquire ` |mdash | only when no
743+ operation.) The :ada: `Retry ` entry will allow a caller to return |mdash | from the
744+ caller's point of view, a return from the call to :ada: `Acquire ` |mdash | only when no
660745other caller currently owns the PO.
661746
662747The examples so far exist primarily for providing mutual exclusion to code that
663748includes potentially blocking operations. By no means, however, are these the
664- only examples. Much more powerful abstractions are possible.
749+ only examples. Much more sophisticated abstractions are possible.
665750
666751For example, let's say we want to have a notion of *events * that application
667752tasks can await, suspending until the specified event is *signaled *. At some
@@ -837,7 +922,7 @@ signaling multiple events. Here then is the visible part:
837922
838923 Both the entry and the procedure take an argument of the array type, indicating
839924one or more client events. The entry, called by waiting tasks, also has an
840- output argument, :ada: `Enabler `, indicating which specific entry enabled the
925+ output argument, :ada: `Enabler `, indicating which specific event enabled the
841926task to resume, i.e., which event was found signaled and was selected to
842927unblock the task. We need that parameter because the task may have specified
843928that any one of several events would suffice, and more than one could have been
@@ -1248,7 +1333,7 @@ Because we did not make the implementation visible to the package's clients,
12481333our internal changes will not require users to change any of their code.
12491334
12501335Note that both the Ravenscar and Jorvik profiles allow entry families, but
1251- Ravenscar only allows one member per family because only one entry is allowed
1336+ Ravenscar allows only one member per family because only one entry is allowed
12521337per protected object. Such an entry family doesn't provide any benefit over a
12531338single entry declaration. Jorvik allows multiple entry family members because
12541339it allows multiple entries per protected object. However, neither profile
@@ -1382,9 +1467,8 @@ to kill it manually.
13821467Each task writes to :ada: `Standard_Output `. Strictly speaking, this tasking
13831468structure allows race conditions on that shared (logical) file, but this is
13841469just a simple demo of the event facility so it is not worth bothering to
1385- prevent them. For the same reason, for the tasks that await a single event we
1386- didn't declare a task type parameterized with a discriminant to specify that
1387- event.
1470+ prevent them. For the same reason, we didn't declare a task type parameterized
1471+ with a discriminant for those tasks that await a single event.
13881472
13891473
13901474Concurrent Programming
@@ -1527,7 +1611,8 @@ Full Code for :ada:`Event_Management` Generic Package
15271611
15281612The full implementation of the approach that works regardless of whether the
15291613queues are FIFO ordered is provided below. Note that it includes some defensive
1530- code that we did not mention above, for the sake of simplicity.
1614+ code that we did not mention above, for the sake of simplicity. See in
1615+ particular procedures Signal and Wait that take an Event_List as inputs.
15311616
15321617When compiling this generic package, you may get warnings indicating that the
15331618use of parentheses for aggregates is an obsolete feature and that square
@@ -1556,14 +1641,20 @@ even required, but those situations don't appear here.
15561641 procedure Wait
15571642 (This : in out Manager;
15581643 Any_Of_These : Event_List;
1559- Enabler : out Event);
1644+ Enabler : out Event)
1645+ with
1646+ Pre => Any_Of_These'Length > 0;
15601647 -- Block until/unless any one of the events in Any_Of_These has
15611648 -- been signaled. The one enabling event will be returned in the
15621649 -- Enabler parameter, and is cleared internally as Wait exits.
15631650 -- Any other signaled events remain signaled. Note that,
15641651 -- when Signal is called, the events within the aggregate
15651652 -- Any_of_These are checked (for whether they are signaled)
1566- -- in the order they appear in the aggregate.
1653+ -- in the order they appear in the aggregate. We use a precondition
1654+ -- on Wait because the formal parameter Enabler is mode out, and type
1655+ -- Event is a discrete type. As such, if there was nothing in the list
1656+ -- to await, the call would return immediately, leaving Enabler's value
1657+ -- undefined.
15671658
15681659 procedure Wait
15691660 (This : in out Manager;
@@ -1577,6 +1668,9 @@ even required, but those situations don't appear here.
15771668 All_Of_These : Event_List);
15781669 -- Indicate that all of the events in All_Of_These are now
15791670 -- signaled. The events remain signaled until cleared by Wait.
1671+ -- We don't use a similar precondition like that of procedure
1672+ -- Wait because, for Signal, doing nothing is what the empty
1673+ -- list requests.
15801674
15811675 procedure Signal
15821676 (This : in out Manager;
@@ -1626,9 +1720,7 @@ And the generic package body:
16261720 Enabler : out Event)
16271721 is
16281722 begin
1629- if Any_Of_These'Length > 0 then
1630- This.Wait (Any_Of_These, Enabler);
1631- end if;
1723+ This.Wait (Any_Of_These, Enabler);
16321724 end Wait;
16331725
16341726 ----------
@@ -1653,6 +1745,8 @@ And the generic package body:
16531745 All_Of_These : Event_List)
16541746 is
16551747 begin
1748+ -- Calling Manager.Signal has an effect even when the list
1749+ -- is empty, albeit minor, so we don't call it in that case
16561750 if All_Of_These'Length > 0 then
16571751 This.Signal (All_Of_These);
16581752 end if;
0 commit comments