Concurrency is running code/ accomplishing tasks simultaneously within a process. This is achieved by threads. Prior to C++11, it was not supported by the language and needed platform specific API's. Thus, such code could not be ported to different platfoms.
One of the prominent improvement to the C++11 standard is the introduction of concurrency/ multi threading built into the language.
Before jumping into the language specific's, let us take this opportunity to see how threads are useful.
Extensions C++11 has added to the standard library to support multi-threading are
<thread>
,
<mutex>
,
<condition_variable>
and
<future>
.
1. <thread>: An example thread that say's Hello!
void Hello(std::string who)
{
std::cout << "Hello " << who << "!" << std::endl;
}
int main()
{
std::string who("world");
std::thread myThread(Hello, who); // create myThread to run Hello with parameter who
// while myThread is busy saying Hello. I shall finish other tasks.
myThread.join(); // wait for myThread to complete
std::cout << "Thread finished saying Hello!";
return 0;
}
main
spawns a new thread with std::thread
and runs function Hello
with the parameter who
. Both the main thread and myThread
now run concurrently. main thread then waits to make sure myThread
finishes execution.
Now that the tasks are running concurrently, there is great performance and scalability achieved. But, this achievement hits a bottleneck when the threads need to co-ordinate or SYNCHRONIZE for various reasons. One of the main reasons being contending for a shared scarce resource like memory, files, database connections etc.
Consider two threads had to read from and write to a memory location. If the memory is not synchronized between the threads, the data would be corrupted. This is called the data race. Again, when the threads run in an non-deterministic order, the software could produce undefined behavior. This is called the race condition.
C++11 has
<mutex>
for synchronizing the threads.2. <mutex>: A mutex - MUTual EXclusion - allows only one thread to access a shared resource at a given time. For simplicity, let us synchronize a global variable between the main thread and the worker thread.
std::mutex protector; // mutex to be locked by the threads that contend for who.
std::string who; // shared data/ memory
void Hello()
{
protector.lock(); // lock the data so that main is not using it.
std::cout << "Hello " << who << "!" << std::endl;
protector.unlock(); // unlock the data so that main can continue using it.
}
int main()
{
protector.lock(); // lock the data so that Hello thread is not using it.
std::thread myThread(Hello); // create myThread to run Hello
who = "world";
protector.unlock(); // Unlock the data so that Hello thread can continue using it.
// while myThread is busy saying Hello. I shall finish other tasks.
myThread.join(); // wait for myThread to complete
std::cout << "Thread finished saying Hello!";
return 0;
}
when two threads are using the same data, it must be assured that only one thread must access the data for a defined behavior. In the above code, main thread locks the mutex protector so that no other thread will use it. It releases the mutex when it is no longer using the data. Meanwhile,
Hello
thread tries to lock the mutex and since it is being used by the main thread, it waits untill freed by main thread and then acquires the lock. This way, mutexes assure data integrity between the threads.Mutexes can also be used to resolve race conditions by protecting the entire code sections that need to be executed in a definite order.
Here is an analogy to understand mutex better.
A public restroom is used by many, a shared resource. So, there must be a synchronization mechanism that lets others know when it is being used by one. The physical lock indicates the status of the restroom and keeps others out. Similarly, when two threads are used, they are synchronised using mutex and lock. The thread that is using the resource, locks the corresponding mutex and other threads keep waiting untill released by the thread.
Hence, Mutexes helps in synchronising the threads.
Care must be taken to avoid Deadlock's. Deadlock is a state when a thread is stuck infinitely waiting for a resource which could be accquired by another thread indefinitely. It can happen so that two threads accquire resources required by each other and wait for the resource accquired by the other.
Most common measures taken to prevent such deadlocks are,
- The threads accquire resources in the same order irrespective of the order of its usage.
- If the mutexes are used to avoid data races, They must be held for as less time as possible.
- Whenever possible, design to acquire only one mutex at a given time.
What does not require synchronization is the thread local storage. The data, local to a particular thread. C++11 has introduced an additional storage class specifier
thread_local
which creates one instance of the variable for each of the lifetime of the thread's.<mutex> header also defines classes for scope based locking of mutexes.
3. <condition_variable> : When multiple threads are used for various reasons, sometimes there is a need for one or more threads to wait for a particular thread to finish it's job. This can be demonstrated with a simple producer-consumer problem. The producer notifies the consumer whenever it produces data to be consumed. Thus, the consumer waits untill there is data produced. Also, the producer waits to be notified by the consumer to produce more data. The condition variables are used for this purpose. The waiting threads are blocked untill notified/ signalled. Once the condition variable is signaled, the thread is resumed accquiring the lock.
Sequence of operations performed by the waiting threads,
- Accquire the mutex.
- Wait to be notified.
- Check the notification variable and continue excution, when woke up my the notifier.
- Release the mutex.
Sequence of operations performed by the notifier,
- Perform the operations that other threads are waiting for.
- Accquire the mutex.
- Modify the notification variable.
- [optional] Release the mutex.
- Notify the waiting threads.
Now, why do we need a notification variable afterall!? It is because of spurious wake up's. It means the waiting threads could be woke up even if they weren't signalled.
A non realistic example to see how to use condition variables.
std::mutex protector;
std::string who; // shared data
std::condition_variable conditionCheck;
bool isComplete = false;
void worker_thread()
{
// Wait until main() finishes its task.
protector.lock();
// Beware of spurious wake up. Check the notification variable to make sure the condition variable is signalled.
while(!isComplete) // If it is a spurious wake up, wait till main signals.
conditionCheck.wait(protector); // give up lock and wait for main to signal
// lock is automatically re accquired
std::cout << "Hello " << who << "!" << std::endl;
protector.unlock();
}
int main()
{
std::thread myThread(worker_thread);
protector.lock();
who = "world";
isComplete = true;
protector.unlock();
conditionCheck.notify_one();
myThread.join(); // wait for myThread to complete
std::cout << "Thread finished saying Hello!";
return 0;
}
4. <future> : When one thread (say A) returns a value to be used by another thread (say B), It can be done so safely with future and promise. A sets the value in a promise and B reads the value from the corresponding future.
void setVal(std::promise<int>& prom) {
prom.set_value (100);
}
int main ()
{
// Make a promise.
std::promise<int> prom;
// Promise to be fulfilled in the future.
std::future<int> fut = prom.get_future();
// My thread will fulfil the promise in the future.
std::thread myThread(setVal, std::ref(prom)); // Pass by reference so that it can set the value in the future obtained.
// Meanwhile, do other stuff.
// Get the value set by the promise or wait till it is set.
int val = fut.get(); // future is not even ref type, How does it have the value set by the promise!!!!
myThread.join();
return 0;
}
The future can be retrieved only once from the promise. That is because it is moved to the instance when it is first retrieved. Use a
std::shared_future
if multiple threads needs the value from the future.The standard library provides a much cleaner API that hides the thread creation and promises, the
std::async
API.// Execute setVal asynchronously and return future for the return value. std::future<int> fut = std::async(std::launch::async, setVal); // How do we know the future type!! It is the same as the type as std::result_of setVal functon





No comments:
Post a Comment