问题
Just to put the questions upfront (please no comments about the bad architecture or how to revise - suppose this is what it is):
- How does the "lock" statement apply when using Invoke/BeginInvoke
- Could the following code result in a deadlock?
Suppose I have the following BindingList that I need to update on the GUI Thread:
var AllItems = new BindingList<Item>();
I want to make sure that all updates to it are synchronized. Suppose I have the following subroutine to do some calculations and then insert a new entry into the BindingList:
private void MyFunc() {
lock(locker) {
... //do some calculations with AllItems
AddToArray(new Item(pos.ItemNo));
... //update some other structures with the contents of AllItems
}
}
And AddToArray looks like:
private void AddToArray (Item pitem)
{
DoInGuiThread(() =>
{
lock (locker)
{
AllItems.Add(pitem);
}
});
}
And DoInGuiThread looks like:
private void DoInGuiThread(Action action) {
if(InvokeRequired) {
BeginInvoke(action);
} else {
action.Invoke();
}
}
回答1:
The lock is held till you leave the lock
block, your current code does not cause a deadlock however it also does not work correctly either.
Here is the sequence of events:
- On a background thread you call
MyFunc
.- A lock is taken for the background thread for the object
locker
- The background thread will "do some calculations with AllItems"
- The background thread calls
AddToArray
fromMyFunc
passing inpitem
- The background thread calls
DoInGuiThread
fromAddToArray
The background thread calls
BeginInvoke
fromDoInGuiThread
, the thread does not block, I will useA
to signify the background thread andB
to signify the UI thread, both of these are happening at the same time.A)
BeginInvoke
returns from it's call because it is non blocking.
B) The UI hitslock (locker)
and blocks because the lock is held by the background thread.- A)
DoInGuiThread
returns.
B) The UI is still locked up, waiting for the background thread to release the lock.- A)
AddToArray
returns.
B) The UI is still locked up, waiting for the background thread to release the lock.- A) The background thread will "update some other structures with the contents of AllItems" (note,
pitem
has not yet been added toAllItems
)
B) The UI is still locked up, waiting for the background thread to release the lock.- A) The background thread releases the lock for the object
locker
B) The UI thread takes the lock for the objectlocker
- A)
MyFunc
returns.
B)pitem
is added toAllItems
- A) Whoever called
MyFunc
continues to run code
B) The UI thread releases the lock for the objectlocker
- A) Whoever called
MyFunc
continues to run code
B) The UI thread returns to the message pump to process new messages and no longer appears to be "locked up" by the user.
Do you see the issue? AddToArray
returns but the object is not added to the array until the end of MyFunc
so your code after AddToArray
will not have the item in the array.
The "usual" way to solve this is you use Invoke
instead of BeginInvoke
however that causes a deadlock to happen. This is the sequence of events, Steps up to 6 are the same and will be skipped.
- The background thread calls
Invoke
fromDoInGuiThread
- A)
Invoke
waits for B to return to the message pump.
B) The UI hitslock (locker)
and blocks because the lock is held by the background thread.- A)
Invoke
waits for B to return to the message pump.
B) The UI is still locked up, waiting for the background thread to release the lock.- A)
Invoke
waits for B to return to the message pump.
B) The UI is still locked up, waiting for the background thread to release the lock.- A)
Invoke
waits for B to return to the message pump.
B) The UI is still locked up, waiting for the background thread to release the lock.(This repeats forever)
回答2:
There's two different ways this is likely to go down.
- You're doing all of this on the GUI thread
- You're starting this call chain on some other thread
Let's deal with the first first.
In this case there won't be a problem. You take the lock in MyFunc
, you call AddToArray
which calls DoInGuiThread
passing in a delegate. DoInGuiThread
will notice that invoking is not required and call the delegate. The delegate, executing on the same thread that now holds the lock, is allowed to enter the lock again, before calling AllItems.Add
.
So no problem here.
Now, the second case, you start this call chain on some other thread.
MyFunc
starts by taking the lock, call AddToArray
which calls DoInGuiThread
passing the delegate. Since DoInGuiThread
now detects that it needs to invoke it calls BeginInvoke
passing in the delegate.
This delegate is queued on the GUI thread by ways of a message. Here's where things again diverge. Let's say the GUI thread is currently busy, so it won't be able to process messages for a short while (which in this context means "enough to let the rest of this explanation unfold").
DoInGuiThread
, having done its job, returns. The message is not yet processed. DoInGuiThread
returned back to AddToArray
which now returns back to MyFunc
which releases the lock.
When the message is finally processed, nobody owns the lock, so the delegate being called is allowed to enter the lock.
Now, if the message ended up being processed before the other thread managed to return all the way out of the lock, the delegate now executing on the GUI thread would simply have to wait.
In other words, the GUI thread would block inside the delegate, waiting for the lock to be released so it could be entered by the code in the delegate.
来源:https://stackoverflow.com/questions/34275816/what-happens-to-a-lock-during-an-invoke-begininvoke-event-dispatching