Task.ConfigureAwait behavior after UI cross-thread

2019-02-18 00:49发布

I was playing with Task.ConfigureAwait in order to better understand what is going beyond the hood. So i got this strange behavior while combining some UI access stuff with the ConfigureAwait.

Below is the sample app using a simple windows form, with 1 Button followed by the test results:

private async void btnDoWork_Click(object sender, EventArgs e)
{
    List<int> Results = await SomeLongRunningMethodAsync().ConfigureAwait(false);

    int retry = 0;
    while(retry < RETRY_COUNT)
    {
        try
        {
            // commented on test #1 & #3 and not in test #2
            //if(retry == 0)
                //throw new InvalidOperationException("Manually thrown Exception");

            btnDoWork.Text = "Async Work Done";
            Logger.Log("Control Text Changed", logDestination);
            return;
        }
        catch(InvalidOperationException ex)
        {
            Logger.Log(ex.Message, logDestination);
        }

        retry++;
    }
}

Now after button Click:

Test 1 Log results : (Exactly as the above code)

1. Cross-thread operation not valid: Control 'btnDoWork' accessed from a thread other than the thread it was created on.
2. Control Text Changed

Test 2 Log results : (Manual exception throw uncommented)

1.  Manually thrown Exception
2.  Cross-thread operation not valid: Control 'btnDoWork' accessed from a thread other than the thread it was created on.
3.  Control Text Changed

Test 3 Log results : (Same as 1 but without a debugger)

1. Control Text Changed

So the questions are:

  1. Why does the first UI Access (Cross-Thread Operation) have the next iteration of the loop execute on the Main Thread ?

  2. Why doesn't the manual exception lead to the same behavior ?

  3. Why does executing the above sample without a debugger attached (directly from exe) doesn't show the same behavior ?

1条回答
该账号已被封号
2楼-- · 2019-02-18 01:26

This one got me scratching my head a bit, but finally found the trick.

The code of the setter of the Button.Text property is:

  set
  {
    if (value == null)
      value = "";
    if (value == this.Text)
      return;
    if (this.CacheTextInternal)
      this.text = value;
    this.WindowText = value;
    this.OnTextChanged(EventArgs.Empty);
    if (!this.IsMnemonicsListenerAxSourced)
      return;
    for (Control control = this; control != null; control = control.ParentInternal)
    {
      Control.ActiveXImpl activeXimpl = (Control.ActiveXImpl) control.Properties.GetObject(Control.PropActiveXImpl);
      if (activeXimpl != null)
      {
        activeXimpl.UpdateAccelTable();
        break;
      }
    }
  }

The line throwing the exception is this.WindowText = value; (because it internally tries to access the Handle property of the button). The trick is that, right before, it sets the text property in some kind of cache:

if (this.CacheTextInternal)
   this.text = value;

I'll be honest, I have no clue how this cache works, or when it is activated or not (turns out, it seems to be activated in this precise case). But because of this, the text is set even though the exception was thrown.

On further iterations of the loop, nothing happens because the property has a special check to make sure you don't set the same text twice:

if (value == this.Text)
  return;

If you change your loop to set a different text every time, then you'll see that the exception is thrown consistently at each iteration.

查看更多
登录 后发表回答