|
Boost : |
From: Howard Hinnant (hinnant_at_[hidden])
Date: 2007-03-26 12:06:01
On Mar 25, 2007, at 12:03 AM, Emil Dotchevski wrote:
>
>> In the sole-ownership view (value semantics), there is only one
>> owner of a thread. Only the owner can do non-const things with it.
>> Even the thread does not own itself unless it has been passed its
>> own sole-ownership handle. This simplifies (imho) reasoning about
>> non-const operations on the thread.
>
> It simplifies the implementation and/or the documentation, and yes,
> simpler is better -- but only if the functionality that's taken away
> is not needed in practice. I don't think the boost::thread
> documentation claim that "it isn't needed very often" is acceptable.
> If even one valid use case exists that requires shared handles, then
> we either need shared handles or we need to show that the use case
> is in fact invalid.
I was specifically trying to indicate a simplified (and safer)
interface.
I picture threads starting child threads (as an implementation detail)
which start grand-child threads, etc. All of this can get very
complicated to reason about fairly quickly. Especially if some of
those threads encounter exceptional situations (such as being canceled
or whatever).
We essentially have a directed graph with each node in the graph
indicating a thread and each edge indicating ownership of one thread
over another. The main thread is typically the root node. In the
simplest view this graph would be a tree: each node has a single
parent. In a more complex view, the graph is a lattice with children
having multiple parents (owners). In still more complicated
situations one may get into cyclic ownership patterns.
The sole-ownership model, by virtue of its interface and semantics,
naturally restricts the types of graphs which can be created. Such a
restriction could be viewed as either unnecessarily (or unacceptably)
constraining. Or it could be viewed as a safety net which the
compiler helps enforce (at compile time). I have the latter view.
However I can imagine the need for more complicated graphs than the
sole-ownership model allows. I think this functionality is best
delivered in a separate package, perhaps with a warning label or two.
I.e. I don't even believe future should have the shared ownership
model. I would much prefer:
thread sole ownership
future sole ownership
shared_future shared ownership
Here's a prototype HelloWorld I wrote which simulates a tree of
threads (62 of them I believe) simulating work with
this_thread::sleep. Then the main thread decides it needs to cancel
everything (it sends cancels only to the roots of the trees):
#include <iostream>
#include <tr1/functional>
#include <tr1/memory>
#include "thread"
std::mutex cout_mutex;
std::mutex id_mutex;
int get_id()
{
std::exclusive_lock<std::mutex> lk(id_mutex);
static int id = 0;
return ++id;
};
void f(char task, int id, int level)
{
try
{
if (level > 0)
{
--level;
std::thread t1(std::tr1::bind(f, task, get_id(), level));
std::thread t2(std::tr1::bind(f, task, get_id(), level));
t1.join();
t2.join();
}
else
{
while (true)
{
{
std::exclusive_lock<std::mutex> lk(cout_mutex);
std::cout << "thread " << id << " working on task "
<< task << "\n";
}
std::this_thread::sleep(1);
}
}
}
catch (std::thread_canceled&)
{
std::exclusive_lock<std::mutex> lk(cout_mutex);
std::cout << "canceling thread " << id << '\n';
throw;
}
catch (std::exception& e)
{
std::exclusive_lock<std::mutex> lk(cout_mutex);
std::cout << "error in thread " << id << ' ' << e.what() <<
'\n';
throw;
}
catch (...)
{
std::exclusive_lock<std::mutex> lk(cout_mutex);
std::cout << "unknown error in thread " << id << '\n';
throw;
}
std::exclusive_lock<std::mutex> lk(cout_mutex);
std::cout << "normal exit thread " << id << '\n';
}
int main()
{
std::thread a(std::tr1::bind(f, 'A', get_id(), 4));
std::thread b(std::tr1::bind(f, 'B', get_id(), 4));
std::this_thread::sleep(5);
a.cancel();
a.join();
std::this_thread::sleep(1);
b.cancel();
b.join();
std::this_thread::sleep(1);
std::cout << "done\n";
}
This all worked nicely. Despite having to reason about over 60
threads in the system, everything just worked. Everyone has a single
owner. And if that owner dies unexpectedly, that information is
delivered to the child thread via it's destructor which cancels and
then deteaches.
...
thread 26 working on task B
thread 32 working on task B
thread 46 working on task B
thread 48 working on task B
thread 50 working on task B
thread 29 working on task B
thread 31 working on task B
thread 52 working on task B
canceling thread 44
canceling thread 58
canceling thread 54
canceling thread 60
canceling thread 45
thread 33 working on task B
thread 47 working on task B
thread 51 working on task B
thread 61 working on task B
thread 62 working on task B
canceling thread 3
canceling thread 8
canceling thread 4
canceling thread 13
canceling thread 10
canceling thread 11
canceling thread 6
...
canceling thread 47
canceling thread 62
canceling thread 61
done
I introduced some shared ownership into this model with:
if (level > 0)
{
--level;
std::thread t1(std::tr1::bind(f, task, get_id(), level));
std::tr1::shared_ptr<std::thread> p(&t1,
std::tr1::bind(&std::thread::join, &t1));
std::thread t2(std::tr1::bind(f, task, get_id(), level));
t1.join();
t2.join();
}
I now have a much more complicated structure to reason about. What
does this do under cancellation?
Do we want such sharing *implicitly*? By default? In the
*foundation* class?
Btw, it created an infinite loop. I couldn't cancel from main.
When I say:
> This simplifies (imho) reasoning about non-const operations on the
> thread.
this is what I'm trying to convey.
-Howard
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk