天天看点

C++11 多线程编程《C++ Concurrency in Action》读书笔记(2)-Managing Threads

1.1     Basicthread management

Every C++ program has at least one thread,which is started by the C++ runtime:the thread runningmain(). Your program can then launch additional threads that have anotherfunction as the entry point.

If the thread is detached, then you needto ensure that the data accessed by the thread is valid until the thread hasfinished with it.If the thread isjoined, thatmeaning we waiting for the thread to complete.

The act of calling join()also cleans up any storageassociated with the thread, so the std::thread object is no longer associatedwith the now finished  thread;  it  isn’t  associated with  any  thread. This  means  that you  can  call join() only  once for  a  given thread;  once  you’ve called  join(),  the std::thread object is no longer joinable,and joinable()will return false.

To avoid your application being terminated when an exception is thrown,you therefore need to make a decision on what to do in this case. In general,if you were intending to call join()in the non-exceptional case, you also needto call join()in the presence of an exception to avoid accidental lifetimeproblems.

One way of doing this is to use the standard ResourceAcquisition Is Initialization (RAII) idiom and provide a class that doesthe join()in its destructor, as in the following listing.

class thread_guard

{

       std::thread& t;

public:

       explicit thread_guard(std::thread&t_) :

              t(t_)

       {}

       ~thread_guard()

       {

              if (t.joinable())

              {

                     t.join();

              }

       }

       thread_guard(thread_guard const&) = delete;

       thread_guard& operator=(thread_guardconst&) = delete;

};

struct func;

void f()

{

       int some_local_state = 0;

       func my_func(some_local_state);

       std::thread t(my_func);

       thread_guard g(t);

       do_something_in_current_thread();

}

Runningthreads in the background

Calling detach() on a std::thread object leaves  the thread  to  run in  the  background, with no direct means ofcommunicating with it. It’s no longer possible to wait for that thread tocomplete; if a thread becomes detached, it isn’t possible to obtain a std::threadobjectthat references it, so it can no longer be joined. Detached threads truly runin the background; ownership and control are passed over to the C++ RuntimeLibrary, which ensures that the resources associated with the thread arecorrectly reclaimed when the thread exits.

Detached threads are often called daemon threads after the UNIX conceptof a daemon processthat runs in the background without any explicit userinterface. Such threads are typically long-running; they may well run foralmost the entire lifetime of the application, performing a background tasksuch as monitoring the filesystem, clearing unused entries out of objectcaches, or optimizing data structures. At the other extreme, it may make senseto use a detached thread where there’s another mechanism  for identifying when the thread has completedor where the thread is used for a “fire and forget” task.

1.2     Passingarguments to a thread function

It’s important to bear in mind that by default thearguments are copiedinto internal storage, where  they  can be  accessed  by the  newly  created thread  of  execution,even if the corresponding parameterin the function is expecting a reference.

void f(int i, std::stringconst& s);

void oops(int some_param)

{

       char buffer[1024];

       sprintf(buffer, "%i", some_param);

       std::thread t(f, 3, buffer);

       t.detach();

}

In this case, it’s the pointer to the local variable buffer B that’spassed through to the new thread c, and there’s a significant chance that thefunction oopswill exit before the buffer has been converted to a std::string onthe new thread, thus leading to undefined behavior. The solution is to cast tostd::string before passing the buffer to the std::thread constructor:

void f(int i, std::stringconst& s);

void not_oops(int some_param)

{

       char buffer[1024];

       sprintf(buffer, "%i",some_param);

       std::thread t(f, 3, std::string(buffer));

       t.detach();

}

It’s also possible to get the reverse scenario: the object is copied,and what you wanted was a reference. This might happen if the thread isupdating a data structure that’s passed in by reference, for example:

void update_data_for_widget(widget_id w, widget_data&data);

void oops_again(widget_id w)

{

       widget_data data;

       std::thread t(update_data_for_widget, w,data);

       display_status();

       t.join();

       process_widget_data(data);

}

Although update_data_for_widget B expects the second parameter to bepassed by reference,  the std::threadconstructor  c doesn’t  know that;  it’s  oblivious to  the types of the argumentsexpected by the function and blindly copies the supplied values.  When it  calls update_data_for_widget,  it will  end  up passing  a  reference to the internal copy of dataand not a reference to dataitself.Consequently, when the thread finishes, these updates will be discarded as theinternal copies of the supplied arguments are  destroyed,  and process_widget_data will  be passed  an  unchanged data d rather  than a  correctly  updated version.  For  those of  you  familiar with std::bind, the solution will be readily apparent: you need to wrap the arguments that really need to bereferences in std::ref. In this case, if you change the threadinvocation to

std::thread t(update_data_for_widget,w,std::ref(data));

and then update_data_for_widgetwill be correctly passed a reference todatarather than a reference to a copy of data.

You can pass a member function pointer as thefunction, provided you supply a suitable object pointer as the first argument:

class X

{

public:

       void do_lengthy_work();

};

X my_x;

std::thread t(&X::do_lengthy_work, &my_x);

voidprocess_big_object(std::unique_ptr<big_object>);

std::unique_ptr<big_object>p(new big_object);

p->prepare_data(42);

std::threadt(process_big_object, std::move(p));

By specifyingstd::move(p) in the std::thread constructor, the  ownership of  the big_object is transferredfirst into internal storage for the newly created thread and then intoprocess_big_object.

1.3   Transferringownership of a thread

Many resource-owning  types in  the  C++ Standard  Library  such as std::ifstream and std::unique_ptr are movablebut not copyable, andstd::thread is one of them.

1.4    Choosing the number of threads at runtime

One feature of the C++ Standard Library thathelps here is std::thread::hardware_ concurrency().Thisfunction returns an indication of the number of threads that can truly runconcurrently for a given execution of a program. On a multicore system it mightbe the number of CPU cores, for example. This is only a hint, and the function mightreturn 0 if this information is not available, but it can be a useful guide forsplitting a task among threads.

1.5    Identifying threads

Thread identifiers are of type std::thread::id and can be retrieved in two ways:

First, the identifier for a thread can beobtained from its associated std::thread object by calling theget_id() member function. If the std::thread objectdoesn’t have an associated  thread of execution,the call toget_id() returns a default constructedstd::thread::id object, which indicates “not anythread.”

Alternatively, the identifier for the currentthread can be obtained by callingstd::this_thread::get_id(),which is also defined in the <thread> header.