Chenyu Zheng CSCI 5828 - Spring 2010 Prof. Kenneth M. Anderson University of Colorado at Boulder
Chenyu Zheng CSCI 5828 - Spring 2010 Prof. Kenneth M. Anderson University of Colorado at Boulder
Actuality Introduction
Concurrency framework in the 2010 new C++ standard History of multi-threading in C++
Feature demonstration
Thread Management Data Sharing Synchronization of Concurrent Operations The New C++ Memory Model Operations on Atomic Types
11 years after the original C++ Standard (published in 1998), the C++ Standards committee is giving the language and its supporting library a major overhaul. The C++0x is due to be published at the end of 2010 and will bring with it a whole swathe of changes that will make working with C++ easier and more productive.
One of the most significant new features: the support of multi-threaded programs
allow us to write multi-threaded C++ programs without relying on platform-specific extensions => write portable multi-threaded code with guaranteed behavior
Does not acknowledge the existence of threads The memory model is not formally defined Can't write multi-threaded applications without compiler-specific extensions
=>
Solutions
compiler vendors add extensions to the language themselves C APIs led compiler vendors to support multi-threading with various platform specific extensions.
Accumulate sets of C++ classes that wrap the underlying platform specific APIs to provide higher level facilities for multi-threading that simplify the tasks
Application frameworks such as MFC General-purpose C++ libraries such as Boost and ACE
the use of the Resource Acquisition Is Initialization (RAII) idiom with locks to ensure that mutexes are unlocked when the relevant scope is exited
Lack of a formal multi-threading-aware memory model and standard library support means
Development has to
allow the use of the corresponding C API for the platform ensure the C++ runtime library works in the presence of multiple threads
a large number of multi-threaded C++ programs have been written and because of the development of the compilers and processor
managing threads protecting shared data synchronizing operations between threads low-level atomic operations => Provides both integrated high-level facilities and sufficient low-level facilities
Abstraction Penalty
Costs associated with using any high-level facilities compared to using the underlying low-level facilities directly
Not Really
Although sometimes the use of high-level facilities does comes with a performance cost due to the additional code that must be executed
in general the cost is no higher than would be incurred by writing equivalent functionality by hand the compiler may well inline much of the additional code anyway
Even if profiling does demonstrate that the bottleneck is in the C++ Standard Library facilities, it may be due to poor application design rather than a poor library implementation
the functions and classes for managing threads are declared in <thread> Initial Function
Every C++ program has at least one thread, which is started by the C++ runtime: initial function: main() std::thread object named t has the new function hello() as its initial function
Thread Management Data Sharing Synchronization of Concurrent Operations The New C++ Memory Model Operations on Atomic Types
Using function
Using class
Inserting a call to std::thread instance.join() before the to ensure that the thread was finished before the closing brace of the function body would therefore be sufficient function was exited, and thus before the local variables were destroyed
This code ensures that a thread with access to local state is finished before the function exits whether the function exits
joinable()
tests to see if the std::thread object is joinable before calling join(). join() can only be called once for a given thread of execution it would therefore be a mistake to do so if the thread had already been joined with
=delete
The copy constructor and copy-assignment operator are marked ensure they are not automatically provided by the compiler copyed or assigned objects probably would outlive the scope of the thread it was joining
the std::thread object is no longer associated with the actual thread of execution no longer joinable
Passing additional arguments to the std::thread constructor BY Passing arguments to the callable object or function
The arguments are copied into internal storage, where they can be accessed by the newly created thread of execution, even if the corresponding parameter in the function is expecting a reference.
This code will invoke my_x.do_lengthy_work() on the new thread the third argument to the std::thread constructor will be the first argument to the member function, and so forth
#1: a new thread is started and associated with t1 #2: ownership of some_function is transferred over to t2 when t2 is constructed, t1 no longer has an associated thread of execution #3: a new thread is started, and associated with t1
std::thread::hardware_concurrency()
Returns an indication of the number of threads that can truly run concurrently for a given execution of a program. On a multi-core system it might be the number of CPU cores. Only a hint: might return 0 if this information is not available
std::thread:: get_id()
Returns the identifier for a thread If the std::thread object doesn't have an associated thread of execution, returns a default-constructed std::thread::id object, which indicates not any thread.
std::thread::id a, b; a == b
True if: a and b represent the same thread True if: a and b are holding the not any thread value False if: a and b represent different threads False if: a or b represents a thread and the corresponding b or a is holding the not any thread value
std::lock_guard<std::mutex> guard(some_mutex) in add_to_list and list_contains functions means that the accesses in these functions are mutually exclusive
std::unique_lock
does not always own the mutex that it is associated with you can pass std::adopt_lock as a second argument to the constructor: have the lock object manage the lock on a mutex you can also pass std::defer_lock as the second argument to indicate that the mutex should remain unlocked on construction, The lock can then be acquired later by
calling lock() on the std::unique_lock object passing the std::unique_lock object itself to std::lock()
Usage
#1: The function can transfer ownership directly into its own process_data() std::unique_lock instance (#1), the call to do_something() can rely on the data being correctly prepared without another thread altering the data in the mean time.
Sometimes you don't just need to protect the data, but to synchronize actions on separate threads. Waiting for an Event: mutex + sleep
#1 Unlock the mutex whilst we sleep, so another thread can acquire it and set the flag #2 Sleep for 100ms #3 Lock the mutex again before we loop round to check the flag
[]{return !data_queue.empty();}: checks to see if the data_queue is not empty (lambda function) data_cond.wait method: checks the condition (by calling the lambda function), and returns if it is satisfied. If the condition is not satisfied, it unlocks the mutex and puts the thread in a waiting state
One-off Events
Suppose you're going on holiday abroad by plane, fundamentally you're just waiting for one thing: the signal that it's time to get on the plane. Not only that, but a given flight only goes once
Future
C++ model of the one-off event. A future may have data associated with it. A thread can poll the future to see if the event has occurred. Once an event has happened (and the future has become ready), then the future cannot be reset.
all the instances will of course become ready at the same time, they may all access any data associated with the event
All data in a C++ program is made up of objects The C++ Standard defines an object as a region of storage, though it goes on to assign properties to these objects, such as their type and lifetime Whatever its type, an object is stored in one or more memory locations. Each such memory location is either an object (or sub-object) of a scalar type, such as
unsigned short my_class* a sequence of adjacent bit-fields
Significance to Concurrency
If there is no enforced ordering between two accesses to a single memory location from separate threads, these accesses is not atomic, if one or both accesses is a write, this is a data race, and causes undefined behaviour
Example
Goals of the Standards committee is that there shall be no need for a lower-level language than C++
The Standard atomic types can be found in the <cstdatomic> header.
All operations on such types are atomic Only operations on these types are atomic in the sense of the language definition
As well as the basic atomic types, the C++ Standard Library also provides a set of typedefs for the atomic types corresponding to the various non-atomic Standard Library typedefs
Each of the operations on the atomic types has an optional memory ordering argument, three categories
store operations
can have memory_order_relaxed, memory_order_release or memory_order_seq_cst ordering; can have memory_order_relaxed, memory_order_consume, memory_order_acquire or memory_order_seq_cst ordering;
load operations
read-modify-write operations
Actuality Introduction
Concurrency framework in the 2010 new C++ standard History of multi-threading in C++
Feature demonstration
Thread Management Data Sharing Synchronization of Concurrent Operations The New C++ Memory Model Operations on Atomic Types
Thank you