|
|
|
Sponsored Link •
|
|
Advertisement
|
The C++ Standard says not one word about threads. Nevertheless, C++ is routinely and widely used to write solid multithreaded code. If your application shares data across threads, do so safely:
Note that the above applies regardless of whether the type is some
kind of string type, or an STL container like a
vector, or any other type. (We note that some authors
have given advice that implies the standard containers are somehow
special. They are not; a container is just another object.) In
particular, if you want to use standard library components (e.g.,
string, containers) in a multithreaded program,
consult your standard library implementation's documentation to see
whether that is supported, as described earlier.
When authoring your own type that is intended to be usable in a multithreaded program, you must do the same two things: First, you must guarantee that different threads can use different objects of that type without locking (note: a type with modifiable static data typically can't guarantee this). Second, you must document what users need to do in order to safely use the same object in different threads; the fundamental design issue is how to distribute the responsibility of correct execution (race-and deadlock-free) between the class and its client. The main options are:
Push,
Pop). More generally, note that this option is
appropriate only when you know two things:
First, you must know up front that objects of the type will nearly always be shared across threads, otherwise you'll end up doing needless locking. Note that most types don't meet this condition; the vast majority of objects even in a heavily multithreaded program are never shared across threads (and this is good; see Item 10).
Second, you must know up front that per-member-function locking is at
the right granularity and will be sufficient for most callers. In
particular, the type's interface should be designed in favor of
coarse-grained, self-sufficient operations. If the caller typically needs
to lock several operations, rather than an op- eration, this is
inappropriate; individually locked functions can only be assembled into a
larger-scale locked unit of work by adding more (external) locking. For
example, consider a container type that returns an iterator that could
become invalid before you could use it, or provides a member algorithm
like find that can return a correct answer that could become the wrong
answer before you could use it, or has users who want to write
if( c.empty() ) c.push_back(x);. (See [Sutter02] for additional examples.)
In such cases, the caller needs to perform external locking anyway in
order to get a lock whose lifetime spans multiple individual member
function calls, and so internal locking of each member function is
needlessly wasteful.
So, internal locking is tied to the type's public interface: Internal
locking becomes appropriate when the type's individual operations are
complete in themselves; in other words, the type's level of abstraction is
raised and expressed and encapsulated more precisely (e.g., as a
producer-consumer queue rather than a plain vector).
Combining primitive operations together to form coarser common operations
is the approach needed to ensure meaningful but simple function calls.
Where combinations of primitives can be arbitrary and you cannot capture
the reasonable set of usage scenarios in one named operation, there are
two alternatives: a) use a callback-based model (i.e., have the caller
call a single member function, but pass in the task they want performed as
a command or function object; see Items 87 to 89); or b) expose locking in
the interface in some way.
Particularly if you are authoring a widely-used library, consider
making your objects safe to use in a multithreaded program as described
above, but without added overhead in a single-threaded program. For
example, if you are writing a library containing a type that uses
copy-on-write, and must therefore do at least some internal locking,
prefer to arrange for the locking to disappear in single-threaded builds
of your library (#ifdefs and no-op implementations are
common strategies).
When acquiring multiple locks, avoid deadlock situations by arranging for all code that acquires the same locks to acquire them in the same order. (Releasing the locks can be done in any order.) One solution is to acquire locks in increasing order by memory address; addresses provide a handy, unique, application-wide ordering.
|
Sponsored Links
|