Communicating Processes
| ← Previous (Going Parallel) | Communicating Processes | Next (Selecting Input) → |
Running two or more processes concurrently is easy, and fun, but perhaps a little boring in the long run. They execute concurrently and do not interact. They cannot, and do not, share any state, so is impossible -- at least with what we know so far -- for two concurrent processes to exchange information.
However, ProcessJ has a primitive data-type called a channel. A channel is a way for two processes to communicate. A channel consists of two ends: the write (or sender) end and the read (or receiver) end. Which ever process has the writing end can write a value to the channel and which ever process has the reading end can read the value that the sender sent. All channels are uni-directional -- that means that values flow only from one end to the other, and never in the opposite direction. Values are passed from writers to readers.
At this point, it is important that we bring up the topic of synchronization. When a writer wants to write a value to a channel it cannot finish doing so until the reader is ready to read. Similarly (and perhaps not so perplexing), a reader that wants to read from a channel cannot finish doing so until the writer has written a value. The operation of reading a value from a channel or writing a value from a channel is blocking. That means that once the writer has committed to writing a value (as called the write() operation on it) it is stuck in the call waiting for the reader to also engange. Similarly, if a reader commits to reading a value from a channel, it is also stuck until the writer is ready to engage. Furthermore, the writer will not unblock until the value has been successfully transferred to the reader. We call this synchronous, blocking communication. We shall return to the theoretical remifications of this later. In ProcessJ, all communication (and indeed all synchronization events) are synchronous. This allows us to use CSP as a process algebra to formally reason about our programs.
Let us now look at how to declare a channel.There are a number of different types of channels, but let us start with the simplest, a non-shared channel:
ProcessJ
chan<int> c;
A channel is declared using the keyword chan. When declaring a channel, we must specify the type of data carried on that channel. We do that by giving a type between a set of angular brackets <>. The above declaration declares a channel called c as a local variable. Such a channel has a reading end and a writing end. We can obtain these ends by the following two expressions:
c.read
c.write
c.read is a channel-end expression that evaluates to the reading end of the channel and c.write is a channel-end expression that results in the writing end of the same channel. All we have to do now to communicate is to write something to the writing end and read something from the reading end. In order to do so we need two processes running concurrently, say writer() and reader(). We need to pass the writing end to the writer() process and the reading end to the reader() process. Note, entire channels cannot be passed as parameter values. It would no make any sense to do so as the callee may as well declare the channel instead.
(SimpleCommunication.pj)
import std.io;
public proc void writer(chan<int>.write out) {
out.write(42);
}
public proc void reader(chan<int>.read in) {
int value;
value = in.read();
println("The value is " + value);
}
public proc void main(string args[]) {
chan<int> c;
par {
writer(c.write);
reader(c.read);
}
}
The writer() process gets the writing end of the channel c and writes the value 42 to it. The reader() process gets the reading end of the same channel and calls read() on it. Calling read() on a channel's reading end results in the value that was read being returned, so we need to either use it in an expression or assign it to a local variable. Here, we assign it to the local variable value, which we proceed to print to the screen.
It is important to note the construction in main(). The writer() and the reader() processes are run in parallel in a par block, therefore executing concurrently. What happened if we changed the par to a seq, so simply removed the block and just wrote:
import std.io;
public proc void writer(chan<int>.write out) {
out.write(42);
}
public proc void reader(chan<int>.read in) {
int value;
value = in.read();
println("The value is " + value);
}
public proc void main(string args[]) {
chan<int> c;
writer(c.write);
reader(c.read);
}
This code would deadlock. A deadlocked program is a program in which none of the processes can progress in their execution. Why does this program deadlock then? Since the body of main() is a sequential block the statements are executed one-by-one. The reader(c.read) statement will be executed once the writer(c.write) call has finished. However, in writer() we have a synchronous blocking call: out.write(42), which will never finish until the reader holding the reading end calls read() on it. This will never happen as this call is in the reader() procedure, which never gets called as writer() is blocked. This shows the important of parallelism or concurrency when dealing with synchronous blocking communication primitives (It is hard to imagine a synchrnous communication primitive that is not blocking!)
| ← Previous (Going Parallel) | Communicating Processes | Next (Selecting Input) → |