lecture-05
lecture-05
Mike Spertus
[email protected]
Next week’s lecture
⚫ I will be at the C++ Standards Meeting, so
your TA Patrick Shriwise will be lecturing
⚫ As an extra credit HW problem, please
submit and also email directly to me a
question to ask the standards committee
⚫ I will choose one or two of them to be
answered by someone specializing in that
area
Resource management best
practices
⚫ Always use a smart pointer (e.g., unique_ptr
or shared_ptr) to manage the lifetime of an
object
⚫ Use make_shared or make_unique rather
than new to create the object
⚫ Likewise, use RAII to ensure other resources
get reliably released as well
⚫ We will see this shortly with scoped_lock
Review on unique_ptr
⚫ Using smart pointers like unique_ptr is
unquestionably tricky
⚫ However, in combination with move
semantics and RAII (also tricky), memory and
resource management become non-issues in
most C++ programs
⚫ Since memory management is a nightmare in
C and C++98 (tricky doesn’t even begin to
cover it), learning these still makes
programming simpler
unique_ptr
⚫ To get a raw pointer to the unique_ptr’s target,
call get()
⚫ void f() {
unique_ptr<A> ap = make_unique<A>(1, 2, 3);
g(ap.get());
} // the A object is destroyed on leaving scope
⚫ To get a raw pointer to the unique_ptr’s target
and stop managing the object, call release()
⚫ void f() {
unique_ptr<A> ap = make_unique<A>(1, 2, 3);
g(ap.release());
} // the A object is not destroyed on leaving scope
Moving objects
⚫ Suppose I have two overloaded functions
void f(B const &) { /* … */ } // #1
void f(B &&) { /* … */ } // #2
B g() { /* … */ } // Will use shortly
⚫ Which one is called when?
⚫ B b;
f(b); // #1 - can’t mess with b
f(g()); // #2 Moves from unnamed temporary
f(move(b)); // #2 – Said we don’t need b anymore
Move and unique_ptr
⚫ unique_ptrs can’t be copied because then there would
be two owners for the same object
⚫ unique_ptr<A> ap = make_unique<A>();
unique_ptr<B> bp = ap; // Ill-formed. Not unique ownership
⚫ What if we want to transfer ownership from one
unique_ptr to another?
⚫ Unique_ptr can move ownership
⚫ unique_ptr<A> f();
unique_ptr<A> ap = f(); // OK. Can move from temporary
unique_ptr<B> bp = move(ap); // Moves from ap
⚫ See our btree and animal_game code for practical
examples of this
Why did our btree copy
constructor look so weird
⚫ btree &operator=(btree &&other) noexcept {
swap(root, other.root);
return *this;
}
⚫ Well actually,
btree &operator=(btree &&other) = default;
would have been ok
⚫ But for a more complicated class, the full version
would be better
⚫ To understand the reason, we have to
understand exceptions better
What if release() threw an
exception
void hello_threads() {
std::cout<<"Hello Concurrent World\n";
}
int
main(){
// Print in a different thread
std::thread t(hello_threads);
t.join(); // Wait for that thread to complete
}
What happened?
⚫ Constructing an object of type std::thread
immediately launches a new thread, running the
function given as a constructor argument (in this
case, hello_threads).
⚫ We’ll talk about passing arguments to the thread
function in a bit.
⚫ Joining on the thread, waits until the thread
completes
⚫ Be sure to join all of your threads before ending the
program
⚫ Exception: Later we will discuss detached threads,
which don’t need to be joined
Locks
⚫ The simplest way to protect shared data is
with a std::mutex.
⚫ How can we make sure we release the mutex
when we are done no matter what?
⚫ RAII!
⚫ C++11 includes a handy RAII class
std::lock_guard for just this purpose.
⚫ C++17 has replaced it with
std::scoped_lock
Locks
#include <list>
#include <mutex>
#include <algorithm>
void
add_to_list(int new_value)
{
std::lock_guard<std::mutex> guard(some_mutex); // Since I am going to access the shared data struct, acquire the lock
some_list.push_back(new_value); // Now it is safe to use some_list. RAII automatically releases lock at end of function
}
bool
list_contains(int value_to_find)
{
std::lock_guard<std::mutex> guard(some_mutex); // Must get lock every time I access some_list
return
std::find
(some_list.begin(),some_list.end(),value_to_find)
!= some_list.end();
}
Locks: C++11
#include <list>
#include <mutex>
#include <algorithm>
void
add_to_list(int new_value)
{
std::lock_guard<std::mutex> guard(some_mutex); // Since I am going to access the shared data struct, acquire the lock
some_list.push_back(new_value); // Now it is safe to use some_list. RAII automatically releases lock at end of function
}
bool
list_contains(int value_to_find)
{
std::lock_guard<std::mutex> guard(some_mutex); // Must get lock every time I access some_list
return
std::find
(some_list.begin(),some_list.end(),value_to_find)
!= some_list.end();
}
Locks: C++17
Class Template Argument Ded
#include <list>
#include <mutex>
#include <algorithm>
void
add_to_list(int new_value)
{
std::scoped_lock guard(some_mutex); // Template arguments are deduced as of C++17
some_list.push_back(new_value); // Now it is safe to use some_list. RAII automatically releases lock at end of function
}
bool
list_contains(int value_to_find)
{
std::scoped_lock guard(some_mutex); // Must get lock every time I access some_list
return
std::find
(some_list.begin(),some_list.end(),value_to_find)
!= some_list.end();
}
shared_mutex
⚫ A shared_mutex can be acquired either in shared
ownership mode or unique ownership mode
⚫ shared_mutex sm;
// RAII for acquiring shared ownership
shared_lock<shared_mutex> sl(sm);
// RAII for acquiring sole ownership
unique_lock ul(sm);
⚫ Motivating use case: reader-writer lock
⚫ Multiple threads can read from a data structure at the same
time as long as no thread is modifying it
⚫ If a thread is modifying the data structure, it should acquire
sole ownership of the object so no thread sees the data
structure in a partially modified state
Locks: C++17
Shared mutex
#include <list>
#include <mutex>
#include <algorithm>
void
add_to_list(int new_value)
{
std::unique_lock guard(some_mutex); // Unique writer access
some_list.push_back(new_value); // Now it is safe to use some_list. RAII automatically releases lock at end of function
}
bool
list_contains(int value_to_find)
{
std::shared_lock guard(some_mutex); // Shared reader access
return
std::find
(some_list.begin(),some_list.end(),value_to_find)
!= some_list.end();
}
Not so basic: Thread
arguments
⚫ You can add arguments to be passed to the
new thread when you construct the
std::thread object as in the next slide
⚫ But there are some surprising and important
gotchas that make passing arguments to
thread function different from passing
arguments to ordinary functions, so read on
Passing arguments to a thread
#include <iostream>
#include <thread>
#include <string>
#include <vector>
#include <mutex>
using namespace std;
mutex io_mutex;
int
main(){ // No parens after thread function name:
vector<string> names = { "John", "Paul"};
vector<thread> threads;
for(auto name : names) {
threads.push_back(thread(hello, name));
}
for(auto it = threads.begin(), it != threads.end(); it++) {
it->join();
}
}
Deceptively simple
⚫ A different notation is used from arbitrary
function calls, but otherwise fairly
straightforward looking:
⚫ void f(int i);
f(7); // Ordinary call
thread(f, 7);// f used as a thread function
Gotcha: Passing pointers and
references
⚫ Be very careful about passing pointers or references to
local variables into thread functions unless you are sure the
local variables won’t go away during thread execution
⚫ Example (based on Boehm)
void f() {
int i;
thread t(h, &i);
bar(); // What if bar throws an exception?
t.join(); // This join is skipped
} // h keeps running with a pointer
// to a variable that no longer exists
// Undefined (but certainly bad) behavior
⚫ Use try/catch or better yet, a RAII class that joins like the
thread_guard class in Concurrency In Action book
Gotcha: Signatures of thread
functions silently “change”
⚫ What does the following print?
void f(int &i) { i = 5; }
int main() {
int i = 2;
std::thread t(f, i);
t.join();
cout << i << endl;
return 0;
}
A compile error
⚫ Of course, 5 was intended
⚫ Unfortunately, thread arguments are
not interpreted exactly the same way
as just calling the thread function with
the same arguments
⚫ This means that even an application
programmer using threads needs to
understand something subtle about
templates
What went wrong, continued
⚫ Imagine std::thread’s constructor looks like the following
struct thread { ...
// 0 arg thrfunc constructor
template<typename func>
thread(func f);
// 1 arg thrfunc constructor
template<typename func, typename arg>
thread(func f, arg a);
...
};
...
// Deduces thread::thread<void(*)(int), int)
std::thread t(f, i);
...
⚫ In fact, thread constructors use “variadic argument lists,”
which we haven’t (yet) covered
IOW, Templates don’t know f
takes its argument by reference
⚫ To do this, we will use the “ref” wrapper in
<functional>
⚫ void f(int &i) { i = 5; }
int main() {
int i = 2;
std::thread t(f, std::ref(i));
t.join();
cout << i << endl;
return 0;
}
Does thread’s constructor
really look like that?
⚫ No, C++11 has “variadic templates” that can take
any number of arguments, so we don’t need to
separate 0-arg, 1-arg, etc. constructors:
struct thread {
template
<typename F, typename... argtypes>
thread(F f, argtypes... a);
...};
⚫ We’ll learn about these later
Review the code
⚫ Most of the content of this lecture is
illustrated in the code on Canvas, especially
the different implementations of distributed
counters. Carefully review those code
samples in conjunction with the lecture notes
below
Thread References
⚫ C++ Concurrency in Action Book
⚫ http://www.manning.com/williams/
⚫ If you buy from Manning rather than Amazon, you can download a
preprint right now without waiting for the official publication
⚫ The author Anthony Williams is one of the lead architects of
C++11 threads, the maintainer of Boost::Thread, and the author
of just::thread
⚫ Anthony’s Multithreading in C++0x blog
⚫ http://www.justsoftwaresolutions.co.uk/threading/multithreading-
in-c++0x-part-1-starting-threads.html
⚫ Free with concise coverage of all the main constructs
⚫ The standard, of course
⚫ Also look at the papers on the WG21 site
Lock ordering
⚫ If you want to avoid deadlocks, you want to
acquire locks in the same order!
⚫ Suppose thread 1 acquires lock A and then lock B
⚫ Suppose thread 2 acquires lock B and then lock A
⚫ There is a window where we could deadlock with
thread 1 owning lock A and waiting for lock B while
thread 2 owns lock B and is waiting for lock A forever
⚫ The usual best practice is to document an order
on your locks and always acquire them
consistent with that order
⚫ See
http://www.ddj.com/hpc-high-performance-computing/204801163
Sometimes it is hard to fix a
lock order
⚫ From http://www.justsoftwaresolutions.co.uk/threading/multithreading-
in-c++0x-part-7-locking-multiple-mutexes.html
⚫ Consider
class account {
mutex m;
currency_value balance;
public:
friend void transfer(account& from,account& to,
currency_value amount) {
lock_guard<mutex> lock_from(from.m);
lock_guard<mutex> lock_to(to.m);
from.balance -= amount;
to.balance += amount;
}
};
⚫ If one thread transfers from account A to account B at the same time as
another thread is transferring from account B to account A: Deadlock!
C++ provides a solution
⚫ std::lock(…) allows you to acquire multiple locks “at the same time”
and guarantees there will be no deadlock,
⚫ Like magic! (Actually, it will try releasing locks and then acquiring in different
orders until no deadlock occurs)
⚫ class account {
mutex m;
currency_value balance;
public:
friend void transfer(account& from,account& to,
currency_value amount) {
scoped_lock lck(from.m, to.m);
from.balance -= amount;
to.balance += amount;
}
};
⚫ After acquiring the lock, “adopt” it into the lock_guard to manage the
lifetime of the lock.
C++ provides a solution
⚫ As of C++17, it is a lot easier
⚫ class account {
mutex m;
currency_value balance;
public:
friend void transfer(account& from,account&
to,
currency_value amount)
{
scoped_lock lck(from.m, to.m);
from.balance -= amount;
to.balance += amount;
}
};
shared_ptr and lock ordering
⚫ shared_ptrs silently delete an object when the reference
count goes to zero
⚫ The programmer probably doesn’t know when that will be (or
wouldn’t have needed a shared_ptr to begin with!)
⚫ Recall that locks need to be acquired in a specified order
⚫ The problem! If an object that is managed by a shared_ptr’s
destructor acquires locks, we won’t know which locks are held
and won’t know if we are respecting the lock ordering constraints
⚫ Best practice
⚫ Do not acquire (non-leaf) locks in the destructor of any class that
may be managed by a shared_ptr
⚫ If necessary, do the portion of destruction that requires
synchronization in a separate thread (like Java finalizers)
HW 5-1: Extra credit
⚫ Email me a question to ask the standards
committee during next week’s C++ standards
meeting
⚫ I will choose one or two for personal answers
by the appropriate experts
HW 5-2
⚫ The purpose of this problem is to ensure that you can write
basic multithreaded code on your system. Use the C++
threads
⚫ Some compilers require special flags to build multithreaded code, so
consult your compiler documentation if necessary. For example,
g++ foo.cpp –pthread
⚫ Write a program that creates 3 threads that each count up to
100 and output lines like:
Thread 3 has been called 4 times
⚫ To get a thread number, use
std::this_thread.get_id()
⚫ Make sure you use synchronization to keep different threads
from garbling lines like the above.
⚫ There are correct solution with locks and without locks. Full credit for
either. Extra credit for both
⚫ Submit the output from your program. What does it tell you
about how threads are actually scheduled on your system?
HW 5-3
⚫ Write a thread-safe stack using locks
⚫ The only required operations are push and pop
⚫ E.g., multiple threads can concurrently do things
like the following without corrupting the stack
⚫ mpcs50144::stack<int> s;
s.push(7);
s.push(5);
cout << s.pop();
⚫ For extra credit, add additional useful
functionality (e.g., initializer list constructor, etc.)