Boost logo

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