The real world contains actors that execute independently of, but communicate with, each other. In modeling the world, many parallel executions have to be composed and coordinated, and that's where the study of concurrency comes in. Consider:
Hardware examples:
Software examples:
$ sort | uniq | format | print
Even more examples:
-- Each of the K processors or execution units can work on the -- code below for a specific value for I, so the loop can -- be executed in ceil(N/K) "steps". for I in 1..N loop D[I] := A[I] + B[I] * C[I] end loop
I’m going to go out on a limb and suggest that there are three major styles of architecting a concurrent system.
Multiple independent processes with no shared memory, communicating only via message passing.
Multiple threads share memory, so require locks. Generally, a single process can have multiple threads.
One a single thread exists! A single loop reads from the event queue and invokes the handlers.
Probably some mixing can occur.
The “threads vs. events” issue is a big one. Which is better? Or when is one better than the other, if ever? Read this neutral analysis of both styles.
Generally speaking, threading requires that you use, locks, mutexes, countdowns, condition variables, semaphores, and similar things. But there do exist higher-level synchronization mechanisms like monitors and Ada-style protected objects.
In a distributed system, the processes can be considered to have a life of their own, in which case we sometimes call them actors or agents. High-level models for the agents include:
Communicate with each other via mailboxes
Communicate with each other via channels
Communicate by yielding to each other
As always, a theory (organized body of knowledge with predictive powers) helps use understand the state of the art and enables growth. People have created a number of process algebras and process calculi to describe concurrent systems mathematically.
As always, a working vocabulary helps.
Because reasons.
Naturally, concurrent programming is harder than sequential programming because concurrent programming is about writing a bunch of communicating sequential programs. You end up with a bunch of problems that don’t exist in sequential programs:
Single-threaded event-based systems have issues, too. You need to make sure each chunk of code (responses to events) always run quickly and leave everything in a consistent state.
The field of concurrent programming is concerned with:
A brief introduction of each follows.
We need a formal way to talk about concurrent programming so that we can analyze requirements and design and implement correct and efficient algorithms. One of the most useful models used in reasoning about concurrent programs is the non-realtime interleaved execution model. This is:
The study of interleaved execution sequences of atomic instructions, where each of the instructions execute in a completely arbitrary but finite amount of time.
In this model we can make no assumptions at all about the relative speeds of the individual instructions, or how malicious a scheduler might be. Since instructions take arbitrary time, there can be many possible interleavings.
Example: Suppose thread-1 has instruction sequence $[A,B,C]$ and thread-2 has sequence $[x,y]$. Then we have to consider:
[A B C x y] [A B x C y] [A B x y C] [A x B C y] [A x B y C] [A x y B C] [x A B C y] [x A B y C] [x A y B C] [x y A B C]
The number of interleavings for two threads, one with $m$ instructions and one with $n$ instructions is $m+n \choose n$ — which you probably know is $\frac{(n+m)!}{n!m!}$.
Systems can be classified by “where” the concurrency is expressed or implemented.
Most processors have several execution units and can execute several instructions at the same time. Good compilers can reorder instructions to maximize instruction throughput. Often the processor itself can do this.
inc ebx inc ecx inc edx mov esi,[x] mov eax,[ebx]
would be executed as follows:
Step U-pipe V-pipe ----------------------------------------- 0 inc ebx inc ecx 1 inc edx mov esi, [x] 2 mov eax, [ebx]
Modern processors even parallelize execution of micro-steps of instructions within the same pipe.
Many programming languages have syntactic forms to express that statements should execute in sequence or in parallel. Common notations:
Pascal-style | Occam-style | Algebraic |
---|---|---|
begin A; cobegin B; C; coend D; cobegin E; F; G; coend end |
SEQ a PAR b c d PAR e f g |
a ; b || c ; d ; e || f || g |
Many languages have a thread type (or class) and a means to run a function on a thread. These things might also be called tasks. Alternatively there might be a library call to spawn a thread and execute a function on the thread.
The operating system runs processes concurrently. And this is cool: Many programming languages give you the ability to spawn processes from your program.
In Java:
Runtime.getRuntime().exec(commandline);
In Python:
TODO
In C, using the Win32 API:
CreateProcess(security_attributes, stack_size, &function, param, activation, &thread_id);
execve
with fork
.)
Generally you'll have $M$ processors and $N$ threads. If $M < N$ you need a scheduler to interleave execution. A thread can be in one of several states; one example is:
The threads of control must communicate with each other. The two main ways to communicate:
Threads sometimes have to coordinate their activities. Here is the overused classic example: two threads $A$ and $B$ are trying to make a $100 deposit. They both execute the sequence:
If A executes its first statement and then $B$ executes its first statement before $A$ finishes its third statement, one of the deposits gets lost. There are dozens of programming paradigms to make sure threads can synchronize properly to avoid these problems. Roughly speaking, there are two approaches. The code to define this “critical region”; might use explicit acquisition and release of locks:
lock := acquire_a_lock() register := read_balance() register += 100 write_balance(register) release_lock()
or the synchronization may be more implicit:
critical_section do register := read_balance() register += 100 write_balance(register) end
If a system has temporal constraints, such as “this operation ”complete in 5 ms” it is called a real-time system. (Real-time constraints are common in embedded systems.)
Many have argued whether a language should have direct support for concurrency and distribution or whether such support should come from a library.
Library or O.S. based | Language-Intrinsic |
---|---|
. . . int t1, t2; . . . t1 = createThread(myFunction); . . . start(t1); // now t1 runs in parallel // with the function that // called start(). // Termination is // asynchronous, though // joining can be done with // a library call. |
procedure P is . . . task T; . . . begin -- T activated implicitly here . . . -- T and P execute in parallel . . end P; -- T and P terminate together |
Advantages:
|
Advantages:
|
What kind of patterns can you adopt that will help you to produce reliable concurrent code? There are too many to mention here, but here are some things to think about:
This is a big topic. You might like to read about Erlang.
Concurrent programs have to be correct for all possible interleavings. Natrually this makes testing more complicated, but it can be done.
We barely scratched the surface. There is so much more to learn.
How about some academic overviews of these fascinating topics.
This is an embarrassingly short and incomplete list:
Here are some questions useful for your spaced repetition learning. Many of the answers are not found on this page. Some will have popped up in lecture. Others will require you to do your own research.
We’ve covered: