c# – 实现是否取决于对不受欢迎的修改关闭的访问?

我相信我理解匿名函数的闭包是什么,并且熟悉传统的陷阱.关于这个主题的好问题是
here
here.目的不是为了理解为什么或如何在一般意义上起作用,而是要根据生成的闭包类引用的行为来理解我可能不知道的错综复杂.具体来说,在报告闭包中捕获的外部修改变量的行为时会出现什么陷阱?

我有一个长期运行,大规模并发的工作服务,只有一个错误情况 – 当它无法检索工作时.并发度(可使用的概念线程数)是可配置的.注意,概念线程被实现为任务<>通过TPL.因为服务不断循环尝试在乘以未知的并发度时工作,这可能意味着每秒可能生成数千到数万个错误.

因此,我需要一个有时间限制而不是尝试绑定的报告机制,它与其自己的概念线程隔离,并且是可取消的.为此,我设计了一个递归的Task 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++;
    }        
}

关注

我理解,在这种情况下,生成的闭包通过引用我的fault变量(每当任何概念线程尝试工作但不能增加时递增).我同样理解这通常是不鼓励的,但是从我所能说的只是因为当编码期望闭包捕获值时它会导致意外的行为.

在这里,我希望并依赖于通过引用捕获故障变量的闭包.我想在调用continuation的时候报告变量的值(它不必是精确的).我有点担心过早出现GC故障但我在退出该词汇范围之前取消循环使我认为它应该是安全的.还有什么我没想到的吗?在考虑基础价值的可变性之外的关闭访问时会有什么危险?

答案和解释

我接受了下面的答案,通过将故障监视器重新编写到自己的类中,重构代码以避免需要关闭访问.但是,由于这不能直接回答这个问题,我将在此处为未来读者提供一个可靠行为的简要说明:

只要闭合变量保持在闭包寿命的范围内,就可以依赖它作为真正的参考变量.从闭包内访问外部作用域中修改的变量的危险是:

>您必须了解变量将在闭包内表现为一个引用,在外部作用域中修改它的值.闭包变量将始终包含外部作用域变量的当前运行时值,而不是生成闭包时的值.
>您必须以这样的方式编写程序,以便外部变量的生命周期与匿名函数/闭包本身相同或更大.如果你垃圾收集外部变量,那么引用将成为无效指针.

最佳答案 这是一个快速的替代方案,可以避免您可能关注的一些问题.另外,正如@Servy所提到的那样,只需调用sperate async函数即可. 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);
            }
        }
    }
} 
点赞