[cpp-threads] High-level concurrency issues

jimmaureenrogers at att.net jimmaureenrogers at att.net
Thu Oct 27 00:21:39 BST 2005


 -------------- Original message ----------------------
From: "Peter A. Buhr" <pabuhr at plg.uwaterloo.ca>
>    Date: Mon, 17 Oct 2005 08:29:31 -0700
>    From: "Herb Sutter" <hsutter at microsoft.com>
> 
>    Hopefully we can get together sometime with a whiteboard handy -- it's hard
>    to have these conversations in email alone. :-) Maybe at C++ Connections?
> 
> I agree, but I'll put down a few thoughts for others because I think there are
> some misconceptions about concurrency models and what I said in my paper. Yes,
> I'm hoping to talk to you and others at C++ Connections, and it should involve
> beer and a whiteboard of almost infinite size. ;-)
> 
>    Just some quick feedback, here's one basic point I don't agree with: You
>    rule out asynchronous calls as being interesting
> 
> I quote from my paper "Both synchronous and asynchronous communication are
> needed in a concurrent system." so clearly I think asynchronous call is
> interesting.  To explain some of my comments in the paper, I need the following
> simple concurrency model:
> 
>       client side      communication      server side
>       -----------      -------------      -----------
>        thread(s) <---> buffer(s) <---> thread(s)/buffer(s)
> 
> If a concurrency model can't paint this picture, then I'll argue the model is
> inadequate.  Once this model can be constructed, you can decorate it with many
> additional styles and motifs, as long as a user can adequately get access to
> the basic model.
> 
> Some definitions:
> 
>   sync-call : a client thread calls a server thread to perform an operation and
>   possibly return a result. The client is going to use the result or action
>   performed by the server immediately so it waits for the call to complete.
>   This scenario occurs a *LOT* in client code.  Because the client waits, no
>   buffering of arguments is needed to perform the call and no protocol is
>   needed to return a result. The advantage is a simple mechanism to express the
>   call/return, which matches with normal routine call in C++, and no buffering
>   of the caller's arguments.
> 
>   async-call : client does a sync-call to store its arguments into a buffer so
>   it can continue execution concurrently with the server. The server retrieves
>   the arguments from a buffer, performs an operation on the arguments, and
>   through some mechanism may return a result to the client.  The advantage is
>   the potential overlap in execution between the client and server, which
>   assumes there is sufficient latency between the start of the call and
>   acquiring the return value to justify any overhead costs.  Note, the
>   buffering of arguments and return values can be done in a variety of ways,
>   such as implicit buffers created by the language or explicit buffers created
>   by the client and/or server.
> 
> For both calls, some locking has to occur, and the server is free to handle
> other requests while performing client operations or farm the operations to
> worker threads, i.e., the server is not required to handle client requests in
> FIFO order, which may require additional buffering in the server.
> 
> That's it folks. There is no other special magic about either sync or async
> call. You can build a lot of specialized concurrent patterns on top of this
> model (design-pattern fodder), but this model is the core/root pattern
> explaining the fundamental concepts.

What about the equivalent to condition variables?

In a sync communication the server thread my execute the call only when a
particular internal state is achieved. Similarly, async communications can
depend upon the internal state of the shared buffer.

The ability to define boundary conditions for the execution of a sync or
async call does require additional buffering. The calls waiting for the
boundary condition to evaluate to TRUE must be buffered in some manner.
For high reliability the queuing model should be well defined, such as 
FIFO or thread priority.


> Nevertheless, async-call and its protocol management only addresses client-side
> issues.  The server-side must also be addressed. One of the reasons that Jim
> and I keep mentioning the language that starts with "A" is because it actively
> addresses server-side issues with a programming model based on the notion of
> active objects (task). (So do other concurrent languages with active objects.)
> Without this notion and its associated concepts, it is difficult or impossible
> to build powerful server-side concurrency, like the Administrator model. If you
> have only done concurrency in Java or C#, then it is difficult to understand or
> appreciate the capabilities of active objects for developing complex servers.
> As Jim has pointed out, both the notion of a monitor and task are useful, in
> general, and especially useful on the server side.

While I must agree, I also want to add that the Ada model also defines a 
clear synchronous communication protocol through the rendezvous model.
The 1983 version of Ada only provided the rendezvous (sycnhronous) model.
Asynchronous communication could be created using extra intermetiate
communication tasks. The abstract complexity was too high. The overhead
of using an active object to simulate a passive object was also too high.
The 1995 Ada standard added passive objects Ada calls protected objects.
They are protected from inappropriate simultaneous access. Note that this
does not mean all simultaneous access is prevented. Read-only actions
by several tasks are allowed, avoiding locking when it is unnecessary.

> 
>    Well, message queues are simple and well understood. Lots of libraries and
>    systems for this exist. (Also, while garbage collection isn't necessary for
>    this, I should note that GC is also being proposed for C++; GC can make some
>    of the implementation choices easier.)
> 
> True, but none of these models fit with any existing notion in C++. The more
> notions you put into the language, the more difficult it is to program. I'm
> suggesting extensions to existing notions whenever possible to provide new
> capabilities that simplify programming.
> 
> Finally, Herb says:
> 
>    I don't buy into that part, because that makes blocking the default, and
>    implicit blocking is one of the big problems with today's concurrent
>    programming models that we need to overcome.
> 
> and in a number of followup messages Herb pounds home this idea. Unfortunately,
> I'm not getting the point of it all. Locking and blocking are fundamental to
> concurrency. The notion of atomicity cannot be removed from concurrency, and
> mutual exclusion, which provides atomicity, requires locking involving
> spinning/blocking.  Clearly, locking/blocking are complex and cause problems; I
> am in total agreement with this part of the argument. But unless you can show
> me a form of concurrency without atomicity, I don't understand the point. There
> is nothing magic about lock-free or transactional memory; all the problems are
> still there in some form or another. You can't sweep locking under the carpet
> and say all problems are solved. Basically, I don't think denial is going to
> get you anywhere.
> 
>    But if you mean that 'doing the right locking' is the answer, I would
>    disagree with that because locks are known to be inherently
>    inappropriate for building modern software -- they aren't composable.
>    One of the key things we need to do in a next-generation programming
>    model for concurrency is to get better isolation to reduce or eliminate
>    mutable shared state (and thereby reduce or eliminate the need for
>    locks), just to get back composability.
> 
> Again, I'm not getting the point. Lots of things are not composable. I can't
> compose a Honda using Toyota parts; I can't even reuse Honda parts on a
> Honda. I think reusability is a good parallel issue. For many reasons, people
> do not write reusable code even when a language provides mechanisms to do
> it. But if you want to spend the extra time and effort, you can write very
> reusable code. Similarly, if you take some time and effort, you can write some
> composable concurrent software, up to some fundamental limits.  But you can't
> magically solve problems like deadlock or wish away mutable shared state. If we
> can figure out ways to mitigate these issues, that's great, but they are always
> going to be there. It's the nature of the beast.

Reuse of active and passive concurrent components does not necessarily have to
be intractable. 

Both kinds of components can be defined in C++ classes, or even through
templates. Ada provides the ability to defined task types and protected types.
Much reuse is possible with this simple capability already available in C++.

I don't expect that anybody needs examples, but I will provide examples in 
Ada upon request.

--
Jim Rogers
Colorado Springs, Colorado
U.S.A.



More information about the cpp-threads mailing list