C# 依赖于对修改的闭包的访问的实现是否不可取?

C# 依赖于对修改的闭包的访问的实现是否不可取?,c#,linq,closures,task-parallel-library,C#,Linq,Closures,Task Parallel Library,我相信我理解什么是匿名函数的闭包,并且熟悉传统的陷阱。涉及此主题的好问题包括和。其目的不是为了理解为什么或者如何在一般意义上工作,而是为了在依赖于生成的闭包类引用的行为时解决我可能不知道的复杂问题。具体来说,在报告闭包中捕获的外部修改变量的行为时,存在哪些陷阱? 例子 我有一个长期运行的、大规模并发的worker服务,只有一个错误案例——当它无法检索工作时。并发度(要使用的概念线程数)是可配置的。注意,概念线程通过TPL作为任务实现。因为当乘以未知的并发度时,服务不断地循环试图获得工作,这意味着

我相信我理解什么是匿名函数的闭包,并且熟悉传统的陷阱。涉及此主题的好问题包括和。其目的不是为了理解为什么或者如何在一般意义上工作,而是为了在依赖于生成的闭包类引用的行为时解决我可能不知道的复杂问题。具体来说,在报告闭包中捕获的外部修改变量的行为时,存在哪些陷阱?

例子 我有一个长期运行的、大规模并发的worker服务,只有一个错误案例——当它无法检索工作时。并发度(要使用的概念线程数)是可配置的。注意,概念线程通过TPL作为任务实现。因为当乘以未知的并发度时,服务不断地循环试图获得工作,这意味着每秒可能产生数千到上万个错误

因此,我需要一个有时间限制而非尝试限制的报告机制,该机制独立于自己的概念线程,并且是可取消的。为此,我设计了一个递归任务lambda,在尝试获得工作的基于主尝试的循环之外,每隔5分钟访问一次我的故障计数器:

var faults = 1;
Action<Task> reportDelay = null;
reportDelay =
    // 300000 is 5 min
    task => Task.Delay(300000, cancellationToken).ContinueWith(
        subsequentTask =>
        {
            // `faults` is modified outside the anon method
            Logger.Error(
                $"{faults} failed attempts to get work since the last known success.");
            reportDelay(subsequentTask);
        },
        cancellationToken);

// start the report task - runs concurrently with below
reportDelay.Invoke(Task.CompletedTask);

// example get work loop for context
while (true)
{
    object work = null;
    try
    {
        work = await GetWork();
        cancellationToken.Cancel();
        return work;
    }
    catch
    {
        faults++;
    }        
}
var故障=1;
Action reportDelay=null;
报告延迟=
//30万是5分钟
task=>task.Delay(300000,cancellationToken)。继续(
后续任务=>
{
//'faults'在anon方法之外修改
记录器。错误(
$“{faults}自上次已知成功后,尝试获取工作失败。”);
报告延迟(后续任务);
},
取消令牌);
//启动报告任务-与以下任务同时运行
reportDelay.Invoke(Task.CompletedTask);
//示例获取上下文的工作循环
while(true)
{
对象工作=空;
尝试
{
工作=等待获得工作();
cancellationToken.Cancel();
返回工作;
}
抓住
{
故障++;
}        
}
担心 我知道,在这种情况下,通过引用我的
faults
变量(每当任何概念线程试图获得工作但无法获得时,该变量都会递增)生成的带点闭包。我同样理解,这通常是不鼓励的,但从我所能说的来看,这只是因为当编码期望闭包捕获值时,它会导致意外行为

在这里,我希望并依赖于通过引用捕获
故障
变量的闭包。我想在调用continuation时报告变量的值(不必精确)。我有点担心
错误
过早地被GC'd,但我在退出词法作用域之前取消了循环,使我认为它应该是安全的还有什么我没想到的吗?在考虑基础价值的易变性之外的闭包访问时,有哪些危险?

答覆及解释 我已经接受了下面的答案,即通过将故障监视器具体化为自己的类来重构代码以避免闭包访问的需要。但是,由于这并不能直接回答问题,因此我将在这里为未来读者简要解释可靠行为:

只要闭合变量在闭合生命周期内保持在范围内,它就可以被视为真正的参考变量。从闭合中访问外部范围内修改的变量的危险如下:

  • 您必须了解,该变量将在闭包中作为引用,在外部范围中修改时会改变其值。闭包变量将始终包含外部作用域变量的当前运行时值,而不是生成闭包时的值
  • 编写程序时必须确保外部变量的生存期等于或大于匿名函数/闭包本身。如果对外部变量进行垃圾收集,则引用将成为无效指针

    • 这里有一个快速的替代方案,可以避免您可能关心的一些问题。另外,正如@Servy所提到的,只要调用一个sperate异步函数就可以了。
      ConcurrentStack
      只是使添加和清除变得容易,此外,还可以记录比计数更多的信息

      public class FaultCounter {
      
          private ConcurrentStack<Exception> faultsSinceLastSuccess;        
      
          public async void RunServiceCommand() {
              faultsSinceLastSuccess = new ConcurrentStack<Exception>();
              var faultCounter = StartFaultLogging(new CancellationTokenSource());
              var worker = DoWork(new CancellationTokenSource());
              await Task.WhenAll(faultCounter, worker);
              Console.WriteLine("Done.");
          }
      
          public async Task StartFaultLogging(CancellationTokenSource cts) {
              while (true && !cts.IsCancellationRequested) {
                  Logger.Error($"{faultsSinceLastSuccess.Count} failed attempts to get work since the last known success.");
                  faultsSinceLastSuccess.Clear();
                  await Task.Delay(300 * 1000);
              }
          }
      
          public async Task<object> DoWork(CancellationTokenSource cts) {            
              while (true) {
                  object work = null;
                  try {
                      work = await GetWork();
                      cts.Cancel();
                      return work;
                  }
                  catch (Exception ex) {
                      faultsSinceLastSuccess.Push(ex);
                  }
              }
          }
      } 
      
      公共类故障计数器{
      私有ConcurrentStack faultsSinceLastSuccess;
      公共异步void RunServiceCommand(){
      faultsSinceLastSuccess=新的ConcurrentStack();
      var faultCounter=StartFaultLogging(新的CancellationTokenSource());
      var worker=DoWork(新的CancellationTokenSource());
      等待任务。WhenAll(故障计数器、工作人员);
      控制台。WriteLine(“完成”);
      }
      公共异步任务启动故障日志记录(CancellationTokenSource cts){
      while(true&&!cts.IsCancellationRequested){
      Logger.Error($“{faultsSinceLastSuccess.Count}自上次已知成功以来,获取工作的尝试失败。”);
      faultsSinceLastSuccess.Clear();
      等待任务。延迟(300*1000);
      }
      }
      公共异步任务DoWork(CancellationTokenSource cts){
      while(true){
      对象工作=空;
      试一试{
      工作=等待获得工作();
      cts.Cancel();
      返回工作;
      }
      捕获(例外情况除外){
      faultsSinceLastSuccess.Push(ex);
      }
      }
      }
      } 
      
      我在您的解决方案中看到了一些问题:

    • 您以非线程安全的方式读取/写入
      faults
      变量值,因此在理论上,您的任何一个线程都可以使用它的旧值。您可以使用
      联锁的
      clas来修复此问题