为什么SynchronizationContext.Current为空?

13

错误: 对象引用未设置为对象实例。

下面的算法是有效的。 我尝试了它,然后将Winform项目移动到另一个目录,导致SynchronizationContext.Current为空。 为什么?

SynchronizationContext uiCtx = SynchronizationContext.Current;  

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    int[] makeSelfMoves = new int[4];

    lock (replay)
    {
        // count should be more than 2
        foreach (KeyValuePair<int, int[]> item in replay)
        {              
            makeSelfMoves = replay[item.Key];
            codeFile.ExecuteAll(makeSelfMoves[0],
              makeSelfMoves[1], makeSelfMoves[2], makeSelfMoves[3]);

            // i get the error here. uictx is null
            uiCtx.Post(o =>
            {
                PrintPieces(codeFile.PieceState());
            }, null);                               

            System.Threading.Thread.Sleep(1000);
        }
    }
}

3
那你是在什么上下文中运行这段代码?控制台应用程序吗? - Jon Skeet
我想知道是否仅需要延迟截取。 - Marc Gravell
该实例是在运行应用程序时创建的。帖子将通过按钮按下触发,并应显示棋盘游戏的回复。 - Dmitry Makovetskiyd
马克,你是指使用这种方法:SetWaitNotificationRequired吗? - Dmitry Makovetskiyd
它在后台工作器上。 - Dmitry Makovetskiyd
显示剩余2条评论
2个回答

27

你的代码严重依赖于你的类构造函数何时以及在哪里运行。当以下情况发生时,SynchronizationContext.Current将为null:

  • 你的类对象创建得太早,在你的代码创建Form类实例或在Main()函数中调用Application.Run()之前。此时,Current成员将设置为WindowsFormsSynchronizationContext的一个实例,该类知道如何使用消息循环进行调度。通过将对象实例化代码移动到主表单构造函数中来修复此问题。

  • 你的类对象是在主UI线程以外的任何线程上创建的。只有Winforms应用程序中的UI线程才能进行调度。通过向你的类添加一个构造函数并包含此语句来诊断此问题:

      Console.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId);
    

还需要在Program.cs的Main()方法中添加这一行代码。如果输出窗口中显示的值不同,它将无法正常工作。通过将对象实例化代码移动到主窗体构造函数中,以确保在UI线程上运行,从而解决此问题。


好的,我会记住你的建议。谢谢帮助。 - Dmitry Makovetskiyd
这是一个旧答案,但值得注意的是,Application.Run不是WinForms安装其SynchronizationContext的唯一时间。我发现创建一个新的Form也会导致WindowsFormsSynchronizationContext被安装,并且其他Control子类的构造函数可能会产生相同的效果,尽管我没有测试过。 - Drake
1
我有一个应用程序,其中我只有new Control();作为一个独立的语句,并注释说明这是因为我需要比它本来创建的更早的同步上下文。 - user743382
1
是Control类构造函数调用WindowsFormsSynchronizationContext.InstallIfNeeded。不太确定我是否想要记录这个 :) - Hans Passant

1

在我的测试框架中,通过依赖注入创建WinForms时,我遇到了这个问题。最初我会在构造函数中捕获SynchronizationContext.Current:

private readonly SynchronizationContext UISyncCtxt;

public MyWinFormViewModel ()
{
    UISyncCtxt = SynchronizationContext.Current;
    ...
}

如果在应用程序已经运行时创建了MyWinFormViewModel,那么这将很好地工作,但是在测试工具中创建依赖关系图时并不一定是这种情况。当由测试工具创建时,SynchronizationContext.Current将为null,并且稍后会出现空引用异常。

我的解决方案是按需进行“惰性”评估,如下所示:

    private SynchronizationContext _uisyncctxt;

    private SynchronizationContext UISyncCtxt => 
        _uisyncctxt ??= SynchronizationContext.Current;

当我实际需要上下文(更新表单上的控件)时,它肯定存在(因为表单已经被实例化)。

编辑:Peter Duniho提出了一个关于任意抓取同步上下文的有效观点。我的原始答案也使得这个类对其依赖关系不诚实,因为它依赖于这个上下文,但没有通过构造函数或其他可注入方法请求它。由于这个类使用DI,我添加了一个名为IUISyncContext的依赖项,它具有以下签名:

public interface IUISyncContext
{
    SynchronizationContext UISyncContext { get; }
}

...以及我的视图模型构造函数:

private readonly SynchronizationContext UISyncCtxt;

public MyWinFormViewModel (IUISyncContext syncContext)
{        
    UISyncCtxt = syncContext.UISyncContext;
    ...
}

感谢您的反馈,Peter。


除非现在您仍然需要记住仅在上下文存在后访问它,并且您必须确保仅从关心它的线程访问它。 这并没有改善已接受的答案,事实上更糟。 - Peter Duniho
有趣的观点。但是,如果你处于一种情况下,尝试获取UI线程上下文并且没有机会获取它,那么你可能会遇到更大的问题。我曾经考虑过编写一个可注入服务来捕获上下文,但认为这似乎是一个过于笨重的解决方案。你对这个问题有什么推荐的解决方案吗? - Vince G.
在进一步考虑之后,我确信使用可注入的服务会是更好的解决方案,因为从技术上讲,这是该类所需要的依赖。通过以这种方式随意获取它,该类并没有真正公开它的依赖关系。我会编辑我的回答。 - Vince G.
“你对这个问题有什么推荐的解决方案?”-- 没有一种适用于所有情况的解决方案。在现代API中,同步上下文几乎不需要显式获取。通过绑定引擎和异步/等待处理自动处理这些内容,这不是一个问题。我不建议将DI作为“通用”方法,因为大多数代码甚至不使用DI,并且试图向那些不知道如何处理其同步上下文的人解释它是一场艰苦的战斗,更不用说从中产生第二个问题了。 - Peter Duniho
1
说实话,如果你觉得你遇到了一个与同步上下文相关的新问题,需要一个新的答案,那么在这里发布答案并不是正确的方法。你没有为这里提出的问题添加任何有用的信息。相反,考虑发布自己的问题,概述你正在处理的具体和新颖的情况,并发布自我回答。当然,确保问题确实是新颖的...可能已经有一个或多个问题在网站上专门处理你的类型的情况(即使用 DI)。 - Peter Duniho
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接