Atomicity and Interlocked Operations
Consider the following scenario: An application has multiple threads executing in parallel, with each thread having write access to some shared integer variable. Each thread simply increments that variable by 1, using ++value
. That looks harmless enough; after all, this looks like an atomic operation, and on many systems, it is at least from the point of view of a machine instruction. However, C++/CLI's execution environment does not universally guarantee this for all integer types.
To demonstrate this, the program in Listing Four has three threads, each concurrently incrementing a shared 64-bit integer variable 10 million times. It then displays that variable's final value, which, in theory, should be 30 million. The resulting application can be run in one of two modes: the default mode is unsynchronized and uses the ++
operator; the alternate mode, indicated by using a command-line argument of Y
or y
, uses a synchronized library increment function instead.
Listing Four
using namespace System; using namespace System::Threading; static bool interlocked = false; const int maxCount = 10000000; /*1*/ static long long value = 0; void TMain() { if (interlocked) { for (int i = 1; i <= maxCount; ++i) { /*2*/ Interlocked::Increment(value); } } else { for (int i = 1; i <= maxCount; ++i) { /*3*/ ++value; } } } int main(array<String^>^ argv) { if (argv->Length == 1) { if (argv[0]->Equals("Y") || argv[0]->Equals("y")) { interlocked = true; } } /*4*/ Thread^ t1 = gcnew Thread(gcnew ThreadStart(&TMain)); Thread^ t2 = gcnew Thread(gcnew ThreadStart(&TMain)); Thread^ t3 = gcnew Thread(gcnew ThreadStart(&TMain)); t1->Start(); t2->Start(); t3->Start(); t1->Join(); t2->Join(); t3->Join(); Console::WriteLine("After {0} operations, value = {1}", 3 * maxCount, value); }
When the standard ++
operator is used, five consecutive executions of the application resulted in the output shown in Figure 3. As we can see, the reported total falls far short of the correct answer. Simply stated, between 17 and 50 percent of the increments went unreported. When the same program was run in synchronized mode that is, using Interlocked
's Increment
instead, all 30 million increments are done and reported correctly.
Figure 3: Output of Listing Four.
Output using the ++ operator After 30000000 operations, value = 14323443 After 30000000 operations, value = 24521969 After 30000000 operations, value = 20000000 After 30000000 operations, value = 24245882 After 30000000 operations, value = 25404963 Output using Interlocked's Increment After 30000000 operations, value = 30000000
Class Interlocked
also has a Decrement
function.
Exercises
To reinforce the material we've covered, perform the following activities:
- In Listing Four, change the type of the shared variable,
value
, and run the application with and without synchronization. - In your implementation's documentation, carefully read the description of the
Increment
andDecrement
functions in classInterlocked
. Note that there are two sets, one forint
and one forlong long
. Note also that there is no support for arguments of typeunsigned int
orunsigned long long
. - Carefully read the description of the other functions in
Interlocked
, especiallyAdd
,Exchange
, andCompareExchange
. - The class
Queue
in Listing Five (available online, see link at the beginning of this article) implements a queue of strings. Modify this class so that it is thread safe; that is, provide support for multiple threads adding and/or removing nodes from the same queue at the same time. The class has two public functions: RemoveNode
must not return to its caller until it has something to return; that is, it must wait indefinitely, if necessary, for a node to be added.- In some other assembly, write a
Main
that creates three threads that each adds some fixed number of nodes, and one thread that removes nodes, all running asynchronously. Once all the adder threads have finished, have the main thread add one last string with the value "END" and wait for the remover thread to shut itself down, which it does when it sees this node. Hint: You will need to use theWait
,Pulse
, andJoin
functions, and you might find it useful to useSleep
as well, to stagger the adder threads' actions.
void AddNode(String^ s); String^ RemoveNode();
Rex Jaeschke is an independent consultant, author, and seminar leader. He serves as editor of the Standards for C++/CLI, CLI, and C#. Rex can be reached at http://www.RexJaeschke.com/.