Threads in C++ ISO_11

Draft Date: 20 MAR 2016

Background

These notes assume that you have taken and passed COP4610, Operating Systems, and as a consequence have general background knowledge of these concepts:

  • Operating system kernel
  • Job scheduling
  • Concurrent programming
  • Programming in C using posix threads
We will use terms such as mutex and semaphore without detailed explanation of their meaning.

C++ Thread Library

Thread support has come late to C++, and it is not hard to see why. Unlike Java, where execution threads run on a virtual (software) machine, the entire design philosophy of C++ is that performance should not be sacrificed to functionality. Particularly in low-level entities such as threads, C++ would not want to build a layer of indirection between the thread execution environment and the hardware as made available to the operating system.

On the other hand, operating systems provide their own support for threading, the best and most well-known example being posix threads.

The task for C++ is to design a thread support system that is as efficient as the native OS-provided library on the one hand but on the other hand is portable from one OS to another. This is not an easy mandate.

The C++ thread support library is new with C++11 and is still evolving, with new components being added and in at least one case a component undergoing changes from one version to the next. What follows here is a very abbreviated introduction to this system. The components are shown in the following table:

Intro Videos

Bo Qian (an Apple guy with a nack for clarity, friendliness, and good humor) has made some terrific videos introducing C++ threading:

As you watch these it may be helpful to have this document handy to click on the various entities as they are used. Of course it also helps to follow along with the code and compile/run on linprog. The following command-line compile macro is also handy:

#!/bin/sh
#
# c4610: a command line compile macro for seld-contained C++ thread programs
#
# usage: "c4610 $1" without .cpp suffix results in executable $1.x
#
echo "g++ -pthread -std=c++11 -Wall -Wextra -o./$1.x -I. $1.cpp"
g++ -pthread -std=c++11 -Wall -Wextra -o./$1.x -I. $1.cpp 

Just create the file and make it executable. Put it in ~/.bin for ubiquitous access.

 

C++ Thread Support Library (C++11 and later)
Topic Category Items Library Header
Threads class
 thread
<thread>
this_thread namespace template <class Clock, class Duration>
function
 sleep_for
 sleep_until
<thread>
  function
 yield
 get_id
<thread>
Mutual exclusion class
 mutex
 recursive_mutex  
 timed_mutex
 recursive_timed_mutex
 shared_timed_mutex (C++14)
 shared_mutex (C++17)
<mutex>
Lock management template <class Mutex>
class
 lock_guard
 unique_lock
 shared_lock (C++14)
<mutex>
Lock management types, instances, & flags
 defer_lock_t
  defer_lock
 try_to_lock_t
  try_to_lock
 adopt_lock_t
  adopt_lock
 once_flag
<mutex>
Lock management template<Lock1,Lock2,...>
function
 lock
 try_lock
 call_once
<mutex>
Condition variables class
 condition_variable 
 condition_variable_any
 cv_status
<condition_variable>
Condition variables function
 notify_all_at_thread_exit
<condition_variable>
Condition variables enumerated type
 cv_status
<condition_variable>
Futures template<ReturnType>
class
 promise
 future
 shared_future
 packaged_task
 launch
 future_status
 future_error
 future_errc
<future>
Futures function template
 async
 future_category
 swap
<future>

We'll take a look at a few of these components and illustrate their use (and the concept of RAII as it applies to threads). A comprehensive study is far beyond what we can accomplish. But with the library at hand, the Internet available, and a small startup experience, you can become as expert as you wish to be.

What is RAII?

Resource Allocation Is Initialization [RAII] is the term invented by Stroustrup to describe the idea that resource de-allocation can and should be handled by class destructors, so that resource allocations are automatically de-allocated and otherwise cleaned up when an object goes out of scope. This is expecially handy in situations where there may be "side exits" caused by such things as exception throws.

The use of RAII should be very familiar to students in the context of dynamic memory allocation: container classes such as List, Vector, Deque, Set, Map all have destructors that de-allocate memory when they are called, which is an automatic call as an object's scope of existence comes to an end.

RAII applies in multithreading in the area of lock management (among other places):

template < class Mutex >
class Locker
{
  private:
   Mutex& mu_;
  public:
    Locker  (Mutex m) : mu_(m) { mu_.lock(); }
    ~Locker ()                 { mu_.unlock(); }
}; 

This captures the essence of the idea. In client code such as this fragment:

{
  ...
  std::mutex          mu;
  Locker<std::mutex>  locker (mu); // locks mu
  ...
} // mu is automatically unlocked here (normal) or whenever an exception is thrown (abnormal)

the client thread does not make explicit calls to lock/unlock a mutex, but relies on RAII to manage locking and (the important part) unlocking.

No Semaphores??

The lack of a class named Semaphore is mildly controversial. Some consider this omission a defect (it is not) and some consider it a disrespectful act toward this classic concept that goes all the way back to Dijkstra.

In practice, the roles played by semaphores in, say, the pthread library, are all subsumed by other features of the C++11 support library, notably condition variables (which are objects of class condition_variable). The fact that a class named "semaphore" might have some behaviors that would be subject to arguments like "that is not part of the traditional semaphore" probably plays a role, as does the obvious fact that OS semaphores must be used in the implementation of the library, so that there would be two different technical definitions for the term.

 

  <thread>   Back to Table↑   Back to Top↑

Library Summary <thread>

namespace std
{
    class thread;
 
    void swap(thread& x, thread& y) noexcept;
 
    bool operator== (thread::id x, thread::id y) noexcept;
    bool operator!= (thread::id x, thread::id y) noexcept;
 
    bool operator<  (thread::id x, thread::id y) noexcept;
    bool operator<= (thread::id x, thread::id y) noexcept;
    bool operator>  (thread::id x, thread::id y) noexcept;
    bool operator>= (thread::id x, thread::id y) noexcept;
 
    template<class CharT, class Traits>
    basic_ostream<CharT, Traits>& operator<< (basic_ostream<CharT, Traits>& out, thread::id id);
 
    template <class T> struct hash;
    template <>        struct hash<thread::id>;
 
    namespace this_thread
    {
        thread::id get_id() noexcept;

        void yield() noexcept;
        template <class Clock, class Duration>
        void sleep_until(const chrono::time_point<Clock, Duration>& abs_time);

        template <class Rep, class Period>
        void sleep_for(const chrono::duration<Rep, Period>& rel_time);
    }
}

Class std::thread

class thread
{
 public:
    // types:
    class id;
    typedef /*implementation-defined*/ native_handle_type;
 
    // construct/copy/destroy:
    thread() noexcept;
    template <class F, class ...Args> explicit thread(F&& f, Args&&... args);
    ~thread();
    thread(const thread&) = delete;
    thread(thread&&) noexcept;
    thread& operator=(const thread&) = delete;
    thread& operator=(thread&&) noexcept;
 
    // members:
    void swap(thread&) noexcept;
    bool joinable() const noexcept;
    void join();
    void detach();
    id get_id() const noexcept;
    native_handle_type native_handle();
 
    // static members:
    static unsigned hardware_concurrency() noexcept;
};

Class std::thread::id

class thread::id
{
 public:
    id() noexcept;
};

 

  <mutex>   Back to Table↑   Back to Top↑

Library Summary <mutex>

namespace std
{
    class mutex;
    class recursive_mutex;
    class timed_mutex;
    class recursive_timed_mutex;
 
    struct defer_lock_t { };
    constexpr defer_lock_t defer_lock { };
 
    struct try_to_lock_t { };
    constexpr try_to_lock_t try_to_lock { };
 
    struct adopt_lock_t { };
    constexpr adopt_lock_t adopt_lock { };
 
    template <class Mutex> class lock_guard;
    template <class Mutex> class unique_lock;
 
    template <class Mutex>
    void swap(unique_lock<Mutex>& x, unique_lock<Mutex>& y) noexcept;
 
    template <class L1, class L2, class... L3>
    int try_lock(L1&, L2&, L3&...);
 
    template <class L1, class L2, class... L3>
    void lock(L1&, L2&, L3&...);
 
    struct once_flag;
 
    template<class Callable, class ...Args>
    void call_once(once_flag& flag, Callable func, Args&&... args);
}

Class std::mutex

class mutex
{
 public:
    constexpr mutex () noexcept;
             ~mutex ();
    mutex(const mutex&) = delete;
    mutex& operator=(const mutex&) = delete;
 
    void lock     ();
    bool try_lock ();
    void unlock   ();
    typedef /*implementation-defined*/ native_handle_type;
    native_handle_type native_handle();
};

Class std::recursive_mutex

class recursive_mutex
{
 public:
    recursive_mutex();
    ~recursive_mutex();
    recursive_mutex(const recursive_mutex&) = delete;
    recursive_mutex& operator=(const recursive_mutex&) = delete;
 
    void lock();
    bool try_lock() noexcept;
    void unlock();
    typedef /*implementation-defined*/ native_handle_type;
    native_handle_type native_handle();
};

Class std::timed_mutex

class timed_mutex
{
 public:
    timed_mutex   ();
    ~timed_mutex  ();
    timed_mutex   (const timed_mutex&) = delete;
    timed_mutex& operator=(const timed_mutex&) = delete;
 
    void lock     ();
    bool try_lock ();

    template <class Rep, class Period>
    bool try_lock_for (const chrono::duration<Rep, Period>& rel_time);

    template <class Clock, class Duration>
    bool try_lock_until (const chrono::time_point<Clock, Duration>& abs_time);
    void unlock();
    typedef /*implementation-defined*/ native_handle_type;
    native_handle_type native_handle ();
};

Class std::recursive_timed_mutex

class recursive_timed_mutex
{
 public:
    recursive_timed_mutex   ();
    ~recursive_timed_mutex  ();
    recursive_timed_mutex(const recursive_timed_mutex&) = delete;
    recursive_timed_mutex& operator=(const recursive_timed_mutex&) = delete;
 
    void lock     ();
    void unlock   ();
    bool try_lock () noexcept;

    template <class Rep, class Period>
    bool try_lock_for(const chrono::duration<Rep, Period>& rel_time);

    template <class Clock, class Duration>
    bool try_lock_until(const chrono::time_point<Clock, Duration>& abs_time);

    typedef /*implementation-defined*/ native_handle_type;
    native_handle_type native_handle();
};

Class std::lock_guard

template <class Mutex>
class lock_guard
{
 public:
    typedef Mutex    mutex_type;
    explicit lock_guard (mutex_type& m);
    lock_guard          (mutex_type& m, adopt_lock_t);
    ~lock_guard         ();
    lock_guard          (lock_guard const&) = delete;
    lock_guard& operator=(lock_guard const&) = delete;
 private:
    mutex_type& pm; // exposition only
};

Class std::unique_lock

template <class Mutex>
class unique_lock
{
 public:
    typedef Mutex mutex_type;
 
    // construct/copy/destroy:
    unique_lock          () noexcept;
    explicit unique_lock (mutex_type& m);
    unique_lock          (mutex_type& m, defer_lock_t) noexcept;
    unique_lock          (mutex_type& m, try_to_lock_t);
    unique_lock          (mutex_type& m, adopt_lock_t);

    template <class Clock, class Duration>
    unique_lock          (mutex_type& m, const chrono::time_point<Clock, Duration>& abs_time);

    template <class Rep, class Period>
    unique_lock          (mutex_type& m, const chrono::duration<Rep, Period>& rel_time);

    ~unique_lock         ();
    unique_lock(unique_lock const&) = delete;
    unique_lock& operator=(unique_lock const&) = delete;
    unique_lock(unique_lock&& u) noexcept;
    unique_lock& operator=(unique_lock&& u) noexcept;
 
    // locking:
    void lock      ();
    bool try_lock  ();

    template <class Rep, class Period>
    bool try_lock_for(const chrono::duration<Rep, Period>& rel_time);

    template <class Clock, class Duration>
    bool try_lock_until(const chrono::time_point<Clock, Duration>& abs_time);

    void unlock();
 
    // modifiers:
    void swap(unique_lock& u) noexcept;
    mutex_type * release() noexcept;
 
    // observers:
    bool        owns_lock     () const noexcept;
    explicit    operator bool () const noexcept;
    mutex_type* mutex         () const noexcept;
 
 private:
    mutex_type * pm;   // exposition only
    bool         owns; // exposition only
};

Class std::once_flag

struct once_flag
{
    constexpr once_flag() noexcept;
    once_flag(const once_flag&) = delete;
    once_flag& operator=(const once_flag&) = delete;
};

 

  <condition_variable>   Back to Table↑   Back to Top↑

Library Summary <condition_variable>

namespace std
{
    class condition_variable;
    class condition_variable_any;
 
    void notify_all_at_thread_exit(condition_variable& cond, unique_lock<mutex> lk);
 
    enum class cv_status
    { no_timeout,  timeout };
}

Class std::condition_variable

class condition_variable
{
 public:
    condition_variable();
    ~condition_variable();
    condition_variable(const condition_variable&) = delete;
    condition_variable& operator=(const condition_variable&) = delete;
 
    void notify_one() noexcept;
    void notify_all() noexcept;
    void wait(unique_lock<mutex>& lock);

    template <class Predicate>
        void wait(unique_lock<mutex>& lock, Predicate pred);

    template <class Clock, class Duration>
    cv_status wait_until(unique_lock<mutex>& lock, const chrono::time_point<Clock, Duration>& abs_time);

    template <class Clock, class Duration, class Predicate>
    bool wait_until(unique_lock<mutex>& lock, const chrono::time_point<Clock, Duration>& abs_time, Predicate pred);

    template <class Rep, class Period>
    cv_status wait_for(unique_lock<mutex>& lock, const chrono::duration<Rep, Period>& rel_time);

    template <class Rep, class Period, class Predicate>
    bool wait_for(unique_lock<mutex>& lock, const chrono::duration<Rep, Period>& rel_time, Predicate pred);
 
    typedef implementation-defined native_handle_type;
    native_handle_type native_handle();
};

Class std::condition_variable_any

class condition_variable_any
{
 public:
    condition_variable_any();
    ~condition_variable_any();
    condition_variable_any(const condition_variable_any&) = delete;
    condition_variable_any& operator=(const condition_variable_any&) = delete;
 
    void notify_one() noexcept;
    void notify_all() noexcept;

    template <class Lock>
    void wait(Lock& lock);

    template <class Lock, class Predicate>
    void wait(Lock& lock, Predicate pred);

    template <class Lock, class Clock, class Duration>
    cv_status wait_until(Lock& lock, const chrono::time_point<Clock, Duration>& abs_time);

    template <class Lock, class Clock, class Duration, class Predicate>
    bool wait_until(Lock& lock, const chrono::time_point<Clock, Duration>& abs_time, Predicate pred);

    template <class Lock, class Rep, class Period>
    cv_status wait_for(Lock& lock, const chrono::duration<Rep, Period>& rel_time);

    template <class Lock, class Rep, class Period, class Predicate>
    bool wait_for(Lock& lock, const chrono::duration<Rep, Period>& rel_time, Predicate pred);
};

 

  <future>   Back to Table↑   Back to Top↑

Library Summary <future>

namespace std
{
    enum class future_errc
    {
        broken_promise = /*implementation-defined*/,
        future_already_retrieved = /*implementation-defined*/,
        promise_already_satisfied = /*implementation-defined*/,
        no_state = /*implementation-defined*/
    };
 
    enum class launch : /*unspecified*/
    {
        async = /*unspecified*/,
        deferred = /*unspecified*/,
        /*implementation-defined*/
    };
 
    enum class future_status
    {
        ready,
        timeout,
        deferred
    };
 
    template <>
    struct is_error_code_enum<future_errc> : public true_type { };
 
    error_code             make_error_code(future_errc e) noexcept;
    error_condition        make_error_condition(future_errc e) noexcept;
    const error_category&  future_category() noexcept;
 
    class future_error;
 
    template <class R> class promise;
    template <class R> class promise<R&>;
    template <> class promise<void>;
 
    template <class R>
    void swap(promise<R>& x, promise<R>& y) noexcept;
 
    template <class R, class Alloc>
    struct uses_allocator<promise<R>, Alloc> : true_type {};
 
    template <class R> class future;
    template <class R> class future<R&>;
    template <> class future<void>;
 
    template <class R> class shared_future;
    template <class R> class shared_future<R&>;
    template <> class shared_future<void>;
 
    template <class> class packaged_task; // undefined
    template <class R, class... ArgTypes>
    class packaged_task<R(ArgTypes...)>;
 
    template <class R>
    void swap(packaged_task<R(ArgTypes...)>&, packaged_task<R(ArgTypes...)>&) noexcept;
 
    template <class R, class Alloc>
    struct uses_allocator<packaged_task<R>, Alloc> : true_type {};
 
    template <class F, class... Args>
     future<result_of_t<decay_t<F>(decay_t<Args>...)>> async(F&& f, Args&&... args);
 
    template <class F, class... Args>
     future<result_of_t<decay_t<F>(decay_t<Args>...)>>  async(launch policy, F&& f, Args&&... args);
}

Class std::future_error

class future_error : public logic_error
{
 public:
    future_error(error_code ec); // exposition only
    const error_code& code() const noexcept;
    const char* what() const noexcept;
};

Class std::promise

template <class R>
class promise
{
 public:
    promise();
    template <class Allocator>
    promise(allocator_arg_t, const Allocator& a);
    promise(promise&& rhs) noexcept;
    promise(const promise& rhs) = delete;
    ~promise();
 
    // assignment
    promise& operator=(promise&& rhs) noexcept;
    promise& operator=(const promise& rhs) = delete;
    void swap(promise& other) noexcept;
 
    // retrieving the result
    future<R> get_future();
 
    // setting the result
    void set_value(/*see description*/);
    void set_exception(exception_ptr p);
 
    // setting the result with deferred notification
    void set_value_at_thread_exit(/*see description*/);
    void set_exception_at_thread_exit(exception_ptr p);
};

Class std::future

template <class R>
class future
{
 public:
    future() noexcept;
    future(future &&) noexcept;
    future(const future& rhs) = delete;
    ~future();
    future& operator=(const future& rhs) = delete;
    future& operator=(future&&) noexcept;
    shared_future<R> share();
 
    // retrieving the value
    /*see description*/ get();
 
    // functions to check state
    bool valid() const noexcept;
    void wait() const;
    template <class Rep, class Period>
    future_status wait_for(const chrono::duration<Rep, Period>& rel_time) const;
    template <class Clock, class Duration>
    future_status wait_until(const chrono::time_point<Clock, Duration>&
    abs_time) const;
};

Class std::shared_future

template <class R>
class shared_future
{
 public:
    shared_future() noexcept;
    shared_future(const shared_future& rhs);
    shared_future(future<R>&&) noexcept;
    shared_future(shared_future&& rhs) noexcept;
    ~shared_future();
    shared_future& operator=(const shared_future& rhs);
    shared_future& operator=(shared_future&& rhs) noexcept;
 
    // retrieving the value
    /*see description*/ get() const;
 
    // functions to check state
    bool valid() const noexcept;
    void wait() const;
    template <class Rep, class Period>
    future_status wait_for(const chrono::duration<Rep, Period>& rel_time) const;
    template <class Clock, class Duration>
    future_status wait_until(const chrono::time_point<Clock, Duration>&
    abs_time) const;
};

Class std::packaged_task

template<class> class packaged_task; // undefined
 
template<class R, class... ArgTypes>
class packaged_task<R(ArgTypes...)>
{
 public:
    // construction and destruction
    packaged_task() noexcept;
    template <class F>
    explicit packaged_task(F&& f);
    template <class F, class Allocator>
    explicit packaged_task(allocator_arg_t, const Allocator& a, F&& f);
    ~packaged_task();
 
    // no copy
    packaged_task(const packaged_task&) = delete;
    packaged_task& operator=(const packaged_task&) = delete;
 
    // move support
    packaged_task(packaged_task&& rhs) noexcept;
    packaged_task& operator=(packaged_task&& rhs) noexcept;
 
    void swap(packaged_task& other) noexcept;
    bool valid() const noexcept;
 
    // result retrieval
    future<R> get_future();
 
    // execution
    void operator()(ArgTypes... );
    void make_ready_at_thread_exit(ArgTypes...);
 
    void reset();
};