代码之家  ›  专栏  ›  技术社区  ›  Daniel King

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

  •  4
  • Daniel King  · 技术社区  · 9 年前

    我相信我理解匿名函数的闭包是什么,并且熟悉传统的陷阱。涵盖此主题的好问题包括: here here 其目的不是从一般意义上理解为什么或如何工作,而是在依赖生成的闭包类引用的行为时找出我可能不知道的复杂性。明确地 在报告闭包中捕获的外部修改变量的行为时,存在哪些陷阱?

    实例

    我有一个长期运行的、大规模并发的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++;
        }        
    }
    

    关切

    我理解,在这种情况下,生成的闭包引用了 faults 变量(每当任何概念线程试图工作但无法工作时,该变量将递增)。我同样理解,这通常是不鼓励的,但根据我所知,这只是因为在编码时,它会导致意外行为,期望闭包捕获值。

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

    回答和解释

    我已经接受了下面的答案,通过将故障监视器具体化到自己的类中,重构代码以避免对闭包访问的需要。然而,由于这并不能直接回答问题,我将在这里为未来读者提供一个关于可靠行为的简要解释:

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

    • 您必须理解,变量将在闭包中充当引用,在外部作用域中修改变量时会改变其值。 闭包变量将始终包含外部范围变量的当前运行时值, 生成闭包时的值。
    • 您必须以这样的方式编写程序,以保证外部变量的生存期与匿名函数/闭包本身相同或更大。 如果垃圾收集外部变量,则引用将成为无效指针。
    2 回复  |  直到 9 年前
        1
  •  1
  •   JSteward    9 年前

    这里有一个快速的替代方案,可以避免您可能关心的一些问题。此外,正如@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);
                }
            }
        }
    } 
    
        2
  •  0
  •   VMAtm    9 年前

    我在您的解决方案中看到了一些问题:

    1. 你读/写 faults 非线程安全方式的变量值,因此理论上任何一个线程都可以使用它的旧值。你可以用 Interlocked 类用法,尤其是用于递增。
    2. 你的行动看起来不像是在处理 task 参数,那么为什么需要它作为 Action 接受任务?此外,在继续中,您没有检查令牌的取消标志,因此,从理论上讲,您可能会遇到代码运行平稳的情况,但仍然会收到错误电子邮件。
    3. 你开始这项漫长的任务时没有 long-running flag ,这对于任务调度器是不可靠的。
    4. 可以在中重写递归操作 while 而是循环,消除代码中不必要的开销。

    C#中的闭包是 implemented into a compiler generated class ,所以 GC 只要您正在循环重试代码,就不应该担心。