
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