• No results found

Concurrent code is more difficult to produce than the functionally equivalent se- quential code. The first difficulty is trying to define the constituent processes. Once we have identified a set of processes we must identify all the dependences that exist between the processes and decide on the appropriate parallelization strategies (For example, the producer-consumer pattern [Downey, 2005]).

Dealing with the dependences between processes can result in contention and communication problems. In shared memory models of concurrency we can have data races[Breshears, 2009]. This is where one or more processes are reading data while simultaneously one or more other processes are updating that same data. In order to prevent this, some form of mutual exclusion is required. Mutual Exclusion is usually implemented by locking [Downey, 2005, Herlihy and Shavit, 2012]. A process can use a lock to guarantee exclusive access to data. A lock can only be held by one process at a time. We associate a particular lock with a specific piece of data and then we ensure that any process that wants access to that piece of data must hold that lock before they are allowed access to the data. Once a process has finished with the data it must relinquish the lock so as to allow other processes to acquire it. The most common form of lock is the semaphore. The semaphore was first introduced by Dijkstra [Dijkstra, nd] and has simple properties. A semaphore is an abstract data type that contains an integer value and has two operations:

Wait If the semaphore’s value is > 0 then subtract 1 from it otherwise suspend the calling process;

Signal If there are processes suspended on this semaphore wake up one of the suspended processes otherwise add 1 to the value held by the semaphore2.

1A speedup of more than p when using p processors. 2

Originally these two operations were called P and V , respectively, from the first letters of the Dutch words for Signal and Wait

When used incorrectly, locks can cause deadlock: where two or more tasks are waiting for each other to release a resource. Absence of deadlock is an important safetyproperty that must be shown for any program that uses locking. Deadlock can arise if all of the following conditions hold [Breshears, 2009]:

Mutual Exclusion One or more processes holds a resource in a non sharable mode (hold a lock on this resource);

Resource Holding A process holds one resource while simultaneously waiting to lock another resource;

No Preemption Locks can only be released by the processes holding them; Circular Wait A process must be waiting for a resource which is being held by

another process, which in turn is waiting for the first process to release the resource. In general, there is a set of waiting processes such that each process in the set is waiting for a resource held by another process in the set. A programmer implementing locks needs to show that at least one of the above properties cannot hold in order to prove that a program is deadlock free.

Another issue is Livelock which is similar to deadlock except the processes involved in livelock constantly change state with regard to one another but with no process progressing. Consider the example of two people trying to cross a rope bridge in opposite directions. When they meet in the middle they are blocked. Both go back the way they came but when they exit the bridge they both now see it is clear and both again try to cross, again meeting in the middle. As they continue this back and forth procedure both are doing work but neither is actually progressing.

We also need to show that fairness, in some form, exists within concurrent programs. Fairness specifies how contention is resolved. Generally we must ensure that it is resolved in such a way that everyone will eventually get access to the resources they need. There are different recognised types of fairness [Ben, 2006]: Weak fairness If a process continuously makes a request, eventually it will be

granted;

Strong fairness If a process makes a request infinitely often, eventually it will be granted;

Linear waiting If a process makes a request, it will be granted before any other process is granted the request more than one;

FIFO (first-in first-out) If a process makes a request, it will be granted before that of any process making a later request.

The ability to prove properties, such as fairness and absence of deadlock, of concurrent programs is both complex and difficult. Concurrent programmers must learn how to handle these new types of error that are just not present in sequential programming.

A final issue we face is that of debugging concurrent programs. Execution of concurrent code is generally non-deterministic. Each run, or execution, of the same code can, and generally will, result in a different scheduling or interleaving of the instructions within the processes. An error in the code may result in an incorrect output only under a certain scheduling. Since there is no reliable way of repeating the exact relative speed of processes on demand we can find ourselves unable to repeat the conditions that lead to specific errors. Without this repeatability the correction of errors in concurrent code is more difficult than that for sequential code.