Re: CMutex /CEvent (multiple threads)
On Sep 25, 4:20 pm, Joseph M. Newcomer <newco...@flounder.com> wrote:
Comment below...
As a fan of exceptions, I must disagree. CSyncObject::Lock should IMO
work like this:
1. when lock is acquired, returns true
2. when timeout expires, returns false
3. anything else provokes an exception.
****
Actually, this solves the problem of out-of-band communication I alluded =
to. Now, if the
CMutex class been done right, we wouldn't be having this debate. So he=
re's a design for a
CMutex class that makes sense (alas, this is what SHOULD have been done i=
n the original
design, had the original designer not be handicapped by being (a) clueles=
s and (b) having
a really-badly-broken C++ compiler at the time this design was done [exce=
ptions didn't
work well in the 4.x compilers and earlier])
The implementation could be
class CSyncException : public CException {
public:
CSyncException(DWORD r) { reason = r; }
DWORD GetExceptionReason() { return reason; }
protected:
DWORD reason;
};
class CSyncFailed : public CSyncException {
public:
CSyncFailed : CSyncException(WAIT_FAILED) {}
};
class CSyncAbandoned : public CSyncException {
public
CSyncAbandoned : CSyncException(WAIT_ABANDONED) {}
};
(and, frankly, I don't see why the function should not be
void Lock()
and we throw a CSyncTimeout exception on timeout!)
class CSyncTImeout : public CSyncException {
public:
CSyncTimeout : CSyncException(WAIT_TIMEOUT) {}
};
and in the Lock() code it would look something like
DWORD result = WFSO(mutex, timeout);
switch(result)
{
case WAIT_TIMEOUT:
throw new CSyncTimeout
case WAIT_OBJECT_0:
return;
case WAIT_FAILED:
throw new CSyncFailed;
case WAIT_ABANDONED:
throw new CSyncAbandoned;
default:
throw new CSyncException(result);
}
Yes, that is better than what MFC offers, I can't disagree. I am
ambivalent on throw upon timeout. I quite liked your "if you have a
timeout, you've got to have a plan" - I am eradicating timeouts
without a plan for years now in code I have here; to my defense, I
didn't write it, curiously enough, I prefer to hang, lie you ;-)). If
there's a timeout, an "if" is obligatory.
But a try/catch(TimeoutException) will work without a doubt, too.
So now my code looks like
{
CMutex lock;
... stuff
lock.Lock();
...synchronized stuff
// unlock not needed}
A nitpick: you mixed two related concepts: a mutex and a lock. Lock is
scoped, mutex is shared between threads.
note I have no explicit Unlock because I rely on the destructor CMutex::~=
CMutex to unlock.
So I can legitimately write things like
if(condition)
return;
inside the body of the ...synchronized stuff and the mutex willl be unloc=
ked. Sounds
clean and elegant. But it has nasty implications.
* If, at the point of return, the data structure I was locking is consist=
ent, I win.
* If, at the point of return, the data structure I was locking is inconsi=
stent, it is
unlocked anyway and I lose big, because everyone who is waiting eventuall=
y gets a "you
have acquired the lock successfully" notification.
* If the ...synchronized stuff calls some method which throws an exceptio=
n, I *might* be
left in an inconsistent state, but fail to notice, because I wasn't catch=
ing the exception
locally and therefore did not detect that it had happened.
Absolutely. What we are looking for here is what wikipedia calls
"Commit or rollback semantics" (http://en.wikipedia.org/wiki/
Exception_handling). It's quite probable that shared data needs that
level of exception safety. That has to be done anyhow.
In a way, you are forgetting rule 0 of exceptions in C++: everything
throws, except code specifically crafted not to (e.g. primitive type
assignments, non-throwing-swap calls and raw C calls). One has to
write code with two factors in mind: for any given block, there is an
exception safety level attached, and no-throw blocks are extremely
rare and hopefully visible through some annotation, a comment etc.
OK, so I have to do
CMutex lock;
try { /* lock */
lock.Lock();
try { /* synchronized section */
...open transaction
...synchronized stuff
...commit transaction;
return;
} /* synchronized section */
catch(CSyncException * e)
{ /* sync problem */
...roll back transaction
e->Delete();
} /* sync problem */
catch(...)
{
...roll back transaction
throw;
}
} /* lock */
catch(CSyncTImeout * e)
{ /* timeout */
...do something if appropriate
e->Delete();
...probably tell our caller we failed
} /* timeout */
catch(CSyncFailed * e)
{ /* failed */
...do something if appropriate
e->Delete();
...probably tell our caller we failed
} /* failed */
catch(CSyncAbandoned * e)
{ /* abandoned */
...roll back transaction
e->Delete();
...probably tell our caller we failed
} /* abandoned */
catch(CSyncException * e)
{ /* other sync error */
...do something if appropriate
e->Delete();
....probably tell our caller we failed
} /* other sync error */
catch(...)
{ /* anything else */
...roll back transaction
throw;
} /* anything else */
And actually, that is the correct code for doing this. Quite a distanc=
e from the current
mythos that
CMutex lock;
if(lock.Lock())
...synchronized stuff
is correct and sufficient
Yes, OK. There's another path: do your stuff in these steps:
1. (locked) read stuff you need of shared part (can throw)
2. work on a copy, produce results (can throw)
3. (locked) "commit" (MUSTN'T throw)
Now look at the "management problem" here. If I fail to understand the=
classes of
exceptions I can get from called functions, I can have a disaster. The=
Java approach was
to recognize this and FORCE the programmer to handle exceptions, either b=
y writing a
handler or stating that their function could pass an unknown exception ba=
ck. This
meticulousness interferes with the ability of a programmer to dash off
quick-and-dirty-and-incorrect code. Instead, they are forced to write =
correct code. Wow!
(I have taught Java-based courses and written enough Java to appreciate t=
his. Based on my
experience, I find the notion that the compiler forces me to be correct t=
o be refreshing)
We'll continue to disagree here. Java approach suffers from "have to
handle it here" problem: for the most part down the call stack, code
does not need to deal with exception, bar it's own state handling in
Java that being try/finally blocks (e.g. close a file etc). But
exception specifications meddle in a big way. It's not by accident
that a myriad of try/catch(exception) {/*crap!*/} is a bane of low-
quality Java code.
IOW, places where code __does__ care what error happened and has to do
something are statistically rare; that is the reason why exceptions
work well. That is also the reason why no-exceptions code in e.g.
straight C is best written like so (metacode):
result f(params)
{
null-init local resources
result=success;
if (result=fail(op1(params)))
goto error;
if (result=fail(op2(params)))
goto error;
...
error:
clean-up local resources (and perhaps others)
return result;
}
(e.g. Linux kernel uses that approach)
Which frankly, is poor man's exceptions. I like to call that Basic-
style __local__ exceptions (it's equivalent to Basic's on "error goto
error_label").
Goran.