|
Boost : |
From: Howard Hinnant (hinnant_at_[hidden])
Date: 2004-07-23 11:58:38
On Jul 23, 2004, at 8:10 AM, Peter Dimov wrote:
> The main reason I'm unconvinced about these "useful helper"
> constructors is
> that all of the use cases I see on the list are totally bogus. The
> only real
> use case for a try_lock (lock_both) does not need helper constructors.
> The
> only other real use case templated on a lock (cv::wait) accepts a lock
> variable and does not need helper constructors.
>
> I may be missing something, of course, but the conservative approach
> is to
> resist the temptation to provide the "but someone might find them
> useful!!~1" parts until we see how well they compare to the "no frills"
> approach in real code.
I sat down to write a realistic example of when a "try_lock"
constructor might be sufficiently desirable to warrant its existence:
Consider a program with a list a jobs that needs doing, in no
particular order (maybe updating various parts of a display - clock,
temperature, internet status, whatever). A reasonable strategy would
be to have a vector<Job> where each Job has a functor, a mutex, and a
bool indicating whether or not it needed running. You could throw
several threads at the function which executes each job in the
vector<Job>, and each thread could try to get the mutex, and if it
failed, just move on to the next job. Such a strategy might make good
use of multiple processors. Here's what I came up with:
template <class F, class Mutex>
struct job
{
typedef F Func;
typedef Mutex Mutex;
F f_;
Mutex* mut_;
bool needs_executing_;
};
template <class Job>
void
do_jobs(std::vector<Job>& v)
{
typedef typename Job::Mutex Mutex;
typedef typename Mutex::scoped_lock Lock;
for (typename std::vector<Job>::iterator i = v.begin(),
e = v.end(); i != e; ++i)
{
Lock lock(*i->mut_, try_lock);
if (lock && i->needs_executing_)
{
i->needs_executing_ = false;
i->f_();
}
}
}
Then I rewrote do_jobs for the case where I have no try-lock ctor:
template <class Job>
void
do_jobs2(std::vector<Job>& v)
{
using namespace Metrowerks;
typedef typename Job::Mutex Mutex;
typedef typename Mutex::scoped_lock Lock;
for (typename std::vector<Job>::iterator i = v.begin(),
e = v.end(); i != e; ++i)
{
Lock lock(*i->mut_, defer_lock);
if (lock.try_lock() && i->needs_executing_)
{
i->needs_executing_ = false;
i->f_();
}
}
}
You really have to squint to see the difference. ... And I just wrote
AND erased several paragraphs proposing that we eliminate the try and
timed constructors, but also how I did not feel strongly either way!
:-)
But I just noticed a difference (apparently I didn't squint enough the
first time ;-) ).
The loop in do_jobs is no-throw if i->f_() is no-throw. The same loop
in do_jobs2 should be no-throw as well, and it is, but the compiler
doesn't know it. That is, the statement:
lock.try_lock()
hides the test:
if (locked_)
throw lock_error();
This is an unnecessary run time inefficiency. And for a compiler
scanning the code to determine whether or not it might throw (a
reasonable optimization step), do_jobs may come up no-throw whereas
do_jobs2 probably won't (there's no way one can tag try_lock() as a
function that won't throw). The code size hit may be the more
important consideration (over the run time hit).
-Howard
Flipping like a fish on the dock
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk