c# - 为什么我的动态值字典迭代(并行)丢弃了我的本地范围变量?

标签 c#

跟进:

How do I iterate through a dynamically valued dictionary in parallel?

如果我使用本地范围的变量运行函数,我的程序最终只会丢弃该值 - 这让我完全困惑。

为什么会这样?

简单示例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data;

namespace ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            Dictionary<string, object> Dict = new Dictionary<string, object>() {
                { "1",  new Dictionary<string, object>() { 
                    {"Columns",new object[1]{ "name"} }
                }},
                 { "2",  new Dictionary<string, object>() { 
                    {"Columns",new object[1]{ "name"} }
                }}

            };
            int i = 0;
            foreach (KeyValuePair<string, object> record in Dict)
            {

                var _recordStored = record; //THIS IS(was) SUPPOSED TO FIX MY WOES!!!
                new System.Threading.Timer(_ => {
                    i++;
                    //if i comment the next line, the code "works" ( albeit doing nothing really" )
                    processFile((Dictionary<string, object>)_recordStored.Value, new List<object>{});
                    Console.WriteLine("Val: " + _recordStored.Key + " ThreadID: " + System.Threading.Thread.CurrentThread.ManagedThreadId+ " "+ i);
                }, null, TimeSpan.Zero, new TimeSpan(0, 0,0,0,1));
            }

            for (; ; )
            {
                System.Threading.Thread.Sleep(10000);
            }
        }

        public static void processFile(Dictionary<string, dynamic> record, List<object> fileData)
        {

            DataTable dt = new DataTable("table");
            foreach (string column in record["Columns"])
            {
                dt.Columns.Add(column, typeof(string));
            }
        }
    }
}

哪个有输出:
Val: 1 ThreadID: 12
Val: 2 ThreadID: 11
Val: 1 ThreadID: 11
Val: 2 ThreadID: 12
Val: 2 ThreadID: 12
Val: 1 ThreadID: 11
Val: 2 ThreadID: 12

但最终(大约 880 次之后)迭代会开始打印
Val: 2 ThreadID: 10
Val: 2 ThreadID: 12
Val: 2 ThreadID: 10
Val: 2 ThreadID: 12
Val: 2 ThreadID: 10
Val: 2 ThreadID: 12

这一切最奇怪的是,当我删除对 processFile 的调用时代码将始终完美执行。

最佳答案

这是工作中的垃圾收集,正确地做它的工作请注意。

问题与您的计时器有关。

在进一步阅读之前,请注意您使用局部范围变量的技巧是完全正确的。这不是你遇到的问题。所以继续这样做,如果你没有这样做,你会有其他/更多的问题。

好的,所以,您构建了两个计时器,因为您的字典中有两个值,并且它们“永远”运行,直到它们被收集。

您没有保留对计时器的引用,因此最终垃圾收集器将收集其中一个或两个。

当它这样做时,它将在收集它之前运行计时器的终结器,这将停止它的执行。

解决方案是保留您的计时器,或者根本不使用计时器,但让我们继续使用计时器,解决方案很容易修复:

int i = 0;
var timers = new List<System.Threading.Timer>();
foreach (KeyValuePair<string, object> record in Dict)
{
    var _recordStored = record; //THIS IS(was) SUPPOSED TO FIX MY WOES!!!
    timers.Add(new System.Threading.Timer(_ => {
       ... rest of your timer code here
    }, null, TimeSpan.Zero, new TimeSpan(0, 0,0,0,1))); // extra end parenthesis here
}
for (; ; )
{
    System.Threading.Thread.Sleep(10000);
}
GC.KeepAlive(timers); // prevent the list from being collected which would ...
                      // end up making the timers eligible for collection (again)

现在 : 这是一个会让你绊倒的问题。如果您在调试器或 DEBUG 构建中运行它,则它是第一个值消失的事实是完全正常的,同时第二个值永远不会消失是完全正常的。

为什么?因为所有变量的生命周期都已延长到方法的持续时间。在循环中,当您简单地说 new System.Threading.Timer(...) ,编译器默默地将其重新定义为:
var temp = `new System.Threading.Timer(...)`

这个变量( temp )在每次循环迭代时被覆盖,实际上保持分配给它的最后一个值,并且只要您在调试器或 DEBUG 构建中运行它,这个计时器就永远不会被收集,因为该方法定义它永远不会结束(底部的无限循环)。

然而,第一个计时器可以自由收集,因为不再有任何东西保持对它的引用。

你可以用这个 LINQPad 来验证。程序:
void Main()
{
    for (int index = 1; index <= 5; index++)
    {
        new NoisyObject(index.ToString());
    }
    for (;;)
    {
        // do something that will provoke the garbage collector
        AllocateSomeMemory();
    }
}

private static void AllocateSomeMemory()
{
    GC.KeepAlive(new byte[1024]);
}

public class NoisyObject
{
    private readonly string _Name;

    public NoisyObject(string name)
    {
        _Name = name;
        Debug.WriteLine(name + " constructed");
    }

    ~NoisyObject()
    {
        Debug.WriteLine(_Name + " finalized");
    }
}

当你在 LINQPad 中运行它时,确保窗口右侧的小按钮切换到“/o-”:

LINQPad optimization switch

你会得到这个输出:
1 constructed
2 constructed
3 constructed
4 constructed
5 constructed
4 finalized
3 finalized
2 finalized
1 finalized

请注意 5 是如何从未最终确定的?

现在通过单击“/o-”按钮打开优化并将其变为“/o+”以启用优化(RELEASE build):
1 constructed
2 constructed
3 constructed
4 constructed
5 constructed
5 finalized
4 finalized
3 finalized
2 finalized
1 finalized

现在5也定稿了。

备注 : 我所说的关于引入临时变量的事情并没有真正发生,但效果是一样的。

如果把上面的LINQPad程序的main方法改成这样:
void Main()
{
    new NoisyObject("Not finalized when /o-");
    for (;;)
    {
        // do something that will provoke the garbage collector
        AllocateSomeMemory();
    }
}

您可以验证在“/o-”下运行它永远不会最终确定对象(嗯,从来没有这么强的词,反正在我等待它的时候也不是),它会在/“o+”下运行。

奖金问题 : 如果你放弃了 GC.KeepAlive(timer);从我上面提出的解决方案中,我将它存储在变量中的事实不会改变行为吗?

并不真地。问题是变量的生命周期。编译器和 JITter 使用有关变量在何处使用的信息来确定哪些变量在任何时间点被认为是“事件的”。

例如这段代码:
void Main()
{
    var obj = new SomeObject();
    CallSomeMethodThatNeverReturns();
}

这不会保留obj无限期地活着?不。在调用该方法时,不再使用该变量,没有代码可以/将访问它,因此垃圾收集器可以自由地收集它。

这就是我所说的变量的生命周期。在 DEBUG 构建(优化关闭)中,或者在调试器下运行时,上面的代码如下所示:
void Main()
{
    var obj = new SomeObject();
    CallSomeMethodThatNeverReturns();
    GC.KeepAlive(obj);
}

从本质上防止对象被收集。 GC.KeepAlive本质上是一种无操作方法,编译器和 JITter 无法“优化掉”,因此似乎会使用相关对象,从而延长其生命周期。

这不是在 RELEASE 构建(优化)中完成的,因此生产程序的行为可能与正在开发的程序不同。

结论 :在您的客户将获得的相同类型的构建上进行测试。

关于c# - 为什么我的动态值字典迭代(并行)丢弃了我的本地范围变量?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17662400/

相关文章:

c# - WCF 和 IIS 中的神秘问题?

c# - 如何从 MS Bot Framework、Skype For Business 获取除 "conversationUpdate"和 "endOfConversation"之外的其他值

javascript - JS .hover 可见性不起作用(ASP.NET、VS2013、Web 表单)

c# - 何时使用 Visual Studio C# "Validated"/"Validating"事件

c# - HttpContext,替代方法 Context.Request.Unvalidated for pre .Net 4.5 servers

c# - 当数组表示为具有 "item#XXX"属性的对象时读取 JSON

c# - 复选框命令在最初未选中时不触发

c# - 共享资源访问的优化同步

c# - 将 Dictionary<Key, Value> 转换为 IReadOnlyDictionary<Key, IValue>?

c# - .NET vScrollBar 问题