1

跟进:

如何并行遍历动态值字典?

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

为什么会这样?

简单示例:

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代码的调用时,它总是会完美执行。

4

1 回答 1

6

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

问题与您的计时器有关。

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

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

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

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

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

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_永远不会结束(底部的无限循环)。

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

您可以使用此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 优化开关

你会得到这个输出:

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 构建(优化)中完成的,因此生产程序的行为可能与正在开发的程序不同。

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

于 2013-07-15T19:53:11.797 回答