Re: MFC and Worker Thread issue
- From: Joseph M. Newcomer <newcomer@xxxxxxxxxxxx>
- Date: Mon, 07 Sep 2009 11:38:41 -0400
See below...
On Mon, 7 Sep 2009 01:20:39 -0700 (PDT), Goran <goran.pusic@xxxxxxxxx> wrote:
On Sep 5, 5:38 pm, Joseph M. Newcomer <newco...@xxxxxxxxxxxx> wrote:****
Why shouldn't a thread know about an HWND? It doesn't have to know about a specific C++
subclass of CWnd, but part of the abstract interface spec of the thread can safely be
stated as "from time to time the thread will notify a selected window, via PostMessage, of
events of interest, such as..." And that facility, which is running in the thread, must
be able to assume that the CWnd* actually has a valid HWND; otherwise, you get into
situations such as the thread has terminated before the window was created but has no way
to notify the "owner" that it has terminated. Requiring an HWND in a CWnd * is a valid
specification! Requiring a specific *type* of CWnd* is not. Yes, this means you can't
run the thread in a console app without some extra effort, but this is not a killer
limitation. The complexity of handling thread notifications if the window does not exist
is far more fragile, difficult to test, and likely to fail. Simple limitation: There is a
CWnd* which is the message sink and it is created before the thread (with its HWND value)
and will not be destroyed until after the thread finishes. I see nothing wrong with
building that requirement in as part of the thread specification.
****
Sure, and that's how I was doing it for a long time. It has, however,
always irked me that I have to pass some sort of the "thread context"
to dialog class and to OnInitDialog, so that there I can create and
start the thread. But OnInitDialog is quite far away from my starting
point. I "know" that it will be called, but... how do I know that?
Because it is specified that you will be called; it is not optional or negotiable!
****
And****
__why__ do I have to go that far just to start the thread?
Because it isn't very far at all; it is just the right place. Why do you think this poses
any conceptual problem?
****
On top of****
that, as I said up there, I might better avoid flicker of a dialog
being quickly shown if I let the thread run first.
That's yet a different question. I would create a modeless dialog and delay the
ShowWindow of the dialog to hit on a WM_TIMER. Other messages could be handled in that
interval. In OnInitDialog, I would GetParent()->EnableWindow(FALSE) to simulate the modal
dialog. Solves the problem without the race condition of testing the thread-is-running
boolean.
****
So I said, oh screw****
that, just start the thread right away and make it work in the dialog.
So dialog only knows that there's a thread to wait on, and that it can
be called with "progress" and "terminated" functions from another
thread (and so it does PostMessage(WM_APP+X, ...) to itself).
Consequence of that inversion is that thread lifetime spans that of
the dialog, and is in the hands of a function that started the thread
(so dialog dtor/OnDestroy don't WFSO on the thread, but it's just the
same as they did).
You are of course right that not limiting thread lifetime to
OnInitDialog/Ondestroy is, ahem... gratuitous complexity. And I kinda
regret to have written my code up here with ctor/dtor. (I see that
now), giving impression to original poster of ctor/dtor being a
solution is a bad idea, as he seems poorly versed in the matter.
^e.g. code must set "thread done" flag...
****
This adds gratuitous complexity to what should be a simple task
****
Yes (on second thought, there's no flag there, but rather a
GetExitCodeThread call).
Which is not actually reliable, because one of the valid exit codes is also the code that
indicates the thread is still running. If I were doing this, I would do a
WFSO(threadhandle, 0) which is robust and cannot be confused by GetExitCode values.
****
****
Same goes for the destructor versus OnDestroy, really.
****
There are three interesting points of closing a window
OnClose/OnOK/OnCancel: the closing can be aborted or delayed if necessary
OnDestroy: The HWND exists but will not upon completion, and the operation cannot be
delayed without blocking the thread.
Destructor: Who knows what is valid? Nothing can be delayed without blocking the thread.
The destructor is the most fragile point, because it can introduce a surprising hang if
the thread has not finished. Furthermore, there is no capability of notifying the user
that the thread is not terminating, because the delay has to require blocking the main GUI
thread *invisibly* (that is, no explicit code on the part of the programmer) and therefore
no notifications can be posted.
True. To the defense of what I actually did: I do not finish the
dialog before thread exits. I disable closing it and wait for
"terminated" callback from the thread. That way, user sees that he's
waiting for "cleanup". Sadly, that has to be done either way if thread
and dialog lifetime are tied together. I really wish I could overcome
that, and one day I'll god damn put up some code to do it!
Spawning a thread and waiting for it is just a clumsy implementation of a subroutine call.
Yes, of course, but you seem to have forgotten that the thread is
there because we want to keep the UI alive and inform user about the
progress ;-)
But you said you were spawning the thread and waiting for it to complete; this is not the
same as saying you were spawning the thread and continuing to run while waiting for an
asynchronous completion notification.
****
****
****>Since it's used for operations of varying duration, I wanted
not to even show the dialog if background is quick. So what I did was
to start the thread before the dialog instance even exists. I continue
with the modal dialog, but don't show it immediately. I check
"terminated" in OnInitDialog, too. I have aforementioned "interface"
that thread uses, which does never see PostMessage or HWND. Instead,
there are ::Progress and ::ThreadDone "callbacks". I wrapped all this
in one function, where I pass an "executable" object. From there I
built stuff up to passing a class instance and a member function to
execute in the background.
*****
Callbacks add their own problems; I try to avoid them if at all possible. I just finished
a project that uses callbacks, but that is because the entire library is written in pure
C. Otherwise, I would have done virtual methods. I like positive-acknowledgement
protocols where there are no possible "race" conditions. They are very insensitive to
accidents of timing.
Yes, callbacks are like that. I have two of them: "terminated" and
"progress". Again, to my defense, if HWND is alive, callbacks go to
PostMessage. If not, "progress" stores received params that
OnInitDialog picks up^^^. "terminated" is just lost (but
GetExitCodeThread picks thread termination up).
There are still some race conditions, e.g.,
callback: main thread
PostMessage DestroyWindow
there is no possible test you can execute in the callback that tells you it is valid to
PostMessage. If you test a boolean that says the window exists, it tells you that the
window exists at time T, but the PostMessage occurs at time T+n for some time value n, and
therefore by the time the PostMessage occurs, the window is invalid. Simplistically
if(::IsWindow(wnd->m_hWnd))
wnd->PostMessage(...);
it can be that the sequence is
if(::IsWindow(wnd->m_hWnd))
**** Thread hits end of time slice****
DestroyWindow()
HWND rendered invalid
*** Thread hits end of time slice ***
wnd->PostMessage(...);
In a dual processor system, this is even worse. The two threads can interlace execution.
Essentially, there is no way a thread can test to see if the window is valid before
posting the message, which is why I avoid this particular model.
****
****
^^^"progress" has to store params because otherwise they have to go on
the heap; once there, they may never get delivered through
PostMessage, and leaked (regardless of HWND being alive for the
lifetime of the thread or not).
And here and now, I just spotted a flaw in what I did: non-stable
m_hWnd is accessed from another thread without proper synchronization.
The way it is done, thread code sees NULL at first, NON-NULL later and
doesn't get to see NULL again. It does not matter if I miss any
message, but what if my code sees bad non-NULL HWND? E.g. perhaps it
can see only 32 bits of it in a 64-bit build (not that we have 64)?
No, it's better that HWND is stable for the lifetime of the thread.
Crap... Seems like "gratuitous complexity", plus this issue, is reason
enough to go back to the old AfxBeginThread in OnInitDialog that I
dislike so much ;-).
If you PostMessage to an invalid handle, the message just disappears (actually,
PostMessage would return FALSE). If it had a pointer in it that had to be released by the
recipient, you have a storage leak.
It would be impossible to see only 32 bits of a 64-bit window handle, because in a 64-bit
build the DLL would be a 64-bit DLL.
joe
****
Joseph M. Newcomer [MVP]
Goran.
email: newcomer@xxxxxxxxxxxx
Web: http://www.flounder.com
MVP Tips: http://www.flounder.com/mvp_tips.htm
.
- References:
- MFC and Worker Thread issue
- From: DewPrism09
- Re: MFC and Worker Thread issue
- From: Goran
- Re: MFC and Worker Thread issue
- From: Joseph M . Newcomer
- Re: MFC and Worker Thread issue
- From: Goran
- Re: MFC and Worker Thread issue
- From: Joseph M . Newcomer
- Re: MFC and Worker Thread issue
- From: Goran
- MFC and Worker Thread issue
- Prev by Date: Re: exe error, works on some pcs but doesnt on others
- Next by Date: Re: How to serialize Mfc template dialog class ?
- Previous by thread: Re: MFC and Worker Thread issue
- Next by thread: Re: MFC and Worker Thread issue
- Index(es):
Relevant Pages
|