winforms 什么机制使一个basec按钮点击处理程序为每次调用维护单独的局部变量?

5w9g7ksd  于 7个月前  发布在  其他
关注(0)|答案(3)|浏览(57)

这段代码做了您所期望的事情:快速连续两次单击按钮将启动两个任务,并打印两条具有两个不同局部变量值的消息。

private bool _dummy;
        
private async void button1_Click(object sender, EventArgs e)
{
  int a;
  if (_dummy == false)
  { 
    a = 1; 
    _dummy = true; 
  }
  else
  { 
    a = 99;
    _dummy = false;
  }

  Debug.WriteLine($"{DateTime.Now:HH:mm:ss:fff} {Thread.CurrentThread.ManagedThreadId} {a} click pre delay");
  Task dtask = Task.Delay(2000);
  await dtask;
  Debug.WriteLine($"{DateTime.Now:HH:mm:ss:fff} {Thread.CurrentThread.ManagedThreadId} {a} {dtask.Id} click post delay");
}

字符串
结果消息:

15:21:52:111 1 1 click pre delay
15:21:52:595 1 99 click pre delay
15:21:54:120 1 1 7 click post delay
15:21:54:615 1 99 9 click post delay


问题是:方法的第一次post-await恢复如何为本地值维护一个单独的堆栈,并与第二次post-await恢复保持分离**?
我知道(或者至少有一个模糊的概念)线程有独立的堆栈,但在这种情况下,所有内容都在单个GUI线程上(通过打印ManagedThreadId进行验证)。
这个问题类似于C# async: How does a thread remember its local variables?,但我不知道我是否会认为它们是真正的重复,我的问题是关于解开时间上重叠的调用。

db2dz4w8

db2dz4w81#

在内部,async调用通过创建一个状态机来工作。各种状态机(每个调用一个)独立运行,并具有各自的独立变量。
看看你的例子是什么降低到例如在“这里”

gev0vcfq

gev0vcfq2#

c#编译器为每个async方法创建一个实现状态机的内部类。方法中的局部变量被转换为类的字段。
每次调用该方法都会创建该内部类的一个新示例。这就是方法的重叠执行如何使用不同的局部变量集:局部变量由内部生成的StateMachine类的不同示例的字段表示。
您可以在https://sharplab.io/#gist:b0bda78721855033b6423b6cd872d05e上看到编译器生成的代码示例

wgmfuz8q

wgmfuz8q3#

我知道(或者至少有一个模糊的概念)线程有独立的堆栈,但在这种情况下,所有内容都在单个GUI线程上(通过打印ManagedThreadId进行验证)。
这是真的,但button1_Click事件处理程序实际上是一个超越线程的异步执行流。如果执行流是同步的,例如将await Task.Delay(2000)替换为Thread.Sleep(2000),您甚至无法单击按钮两次,因为第一次单击后UI将冻结。
在后台,Task.Delay任务在ThreadPool线程上完成。此时,await之后的继续通过透明但复杂的机制重新调度到UI线程,该机制涉及WindowsFormsSynchronizationContext类和Control.BeginInvoke方法(源代码)。您可以通过使用ConfigureAwait方法配置await来防止此重定向:

await dtask.ConfigureAwait(false); // Don't marshal the continuation back
                                   // to the captured synchronization context.

字符串
这样,await之后的continuation将直接在完成Task.Delay任务的线程上运行,这将是一个ThreadPool线程,因为Task.Delay方法是如何在内部实现的。更多细节请参见此答案。

相关问题