(All articles in this series.)
This is the beginning of a series of articles on multithreaded programming in C++. In this first article we will look at pthreads, which are generally considered to be the "assembly language of threading." We will start at the bottom and work our way up to higher concepts.
Pthreads represent the lowest level multithreaded concept that is available on every major platform. On some platforms, pthreads are a wrapper for the operating system specific threads. On other platforms pthreads are native to the OS.
First, a couple of definitions to get us started.
The pthread library is written in C. As such, we need a C compatible way of calling the library from inside of C++. The following example represents the common way to do this:
class threaded_class { public: threaded_class() : m_stoprequested(false), m_running(false) { pthread_mutex_init(&m_mutex); } ~threaded_class() { pthread_mutex_destroy(&m_mutex); } // Create the thread and start work // Note 1 void go() { assert(m_running == false); m_running = true; pthread_create(&m_thread, 0, &threaded_class::start_thread, this); } void stop() // Note 2 { assert(m_running == true); m_running = false; m_stoprequested = true; pthread_join(&m_thread, 0); } int get_fibonacci_value(int which) { pthread_mutex_lock(&m_mutex); // Note 3 int value = m_fibonacci_values.get(which); // Note 4 pthread_mutex_unlock(&m_mutex); return value; } private: volatile bool m_stoprequested; // Note 5 volatile bool m_running; std::vector<int> m_fibonacci_values; // This is the static class function that serves as a C style function pointer // for the pthread_create call static void start_thread(void *obj) { //All we do here is call the do_work() function reinterpret_cast<threaded_class *>(obj)->do_work(); } int fibonacci_number(int num) { switch(num) { case 0: case 1: return 1; default: return fib(num-2) + fib(num-1); }; } // Compute and save fibonacci numbers as fast as possible void do_work() { int iteration = 0; while (!m_stoprequested) { int value = fibonacci_number(iteration); pthread_mutex_lock(&m_mutex); m_fibonacci_values.push_back(value); pthread_mutex_unlock(&m_mutex); // Note 6 } } };
While this code works and is an all too common way of using threads in C++, it has several disadvantages. Note markers are made in the code and will be explained here.
pthread_create function we end up with at least 2 (and in our case 3) functions to actually kick off the thread.
go() (the public interface for starting the work) calls pthread_create() which calls start_thread() (the C style interface for starting the thread) which finally callsdo_work() the object level private entry into the thread.stop() shuts down the thread and calls pthread_join() which does not return until the thread has been fully shutdown. What happens here if we forget to call stop? We may end up with a runaway thread that we have no way of shutting down in the best case. In the worst case we get a crash. The crash occurs when the threaded_class gets destroyed by the main thread of execution but the do_work thread is still running and is now trying to work on destructed data.pthread_mutex_lock() is used to get an exclusive lock on the variable m_mutex. The locks in both get_fibonacci_number and do_work ensure that a user trying to get a fibonacci value will not try to access m_fibonacci_values at the same time that it is being updated by do_work.
If an update and a read were to occur at the same time a crash is likely to happen. This is because the std::vector class resizes itself when new data is added to it.
int value = m_fibonacci_values.get(which); throws an exception? If it were to, the function pthread_mutex_unlock immediately below it would never get executed. If the lock is never unlocked a condition known as a deadlock occurs. When a deadlock occurs one or more threads are stopped and cannot do any work because they cannot acquire the resources they need (in our case a mutex lock).
In fact, this code is susceptible to an exception being thrown if the requested fibonacci value has not yet been calculated.
m_stoprequested must be defined volatile for code correctness. There is a chance it would work if it were not, but without volatile it is possible to create a situation where the thread would never exit because it would not know that m_stoprequested had changed to true
See the volatile article for more details.
pthread_mutex_unlock(&m_mutex); a deadlock would be created just as it would have if an exception were thrown in "Note 4."Coming up next, boost::threads, the "C" of multithreaded programming.
Recent comments
12 hours 23 min ago
17 hours 27 min ago
17 hours 56 min ago
1 day 10 hours ago
1 day 17 hours ago
1 week 6 days ago
3 weeks 11 hours ago
3 weeks 1 day ago
3 weeks 3 days ago
3 weeks 4 days ago