Visual Studio 2010在Socket.BeginReceive()回调函数中遇到未处理的异常时为什么不停止程序?

10

通常情况下,当调试器附加时,即使异常对话框中的“抛出”列没有勾选异常类型,Visual Studio 2010也会在未处理异常处停止。关键字是未处理,上述对话框仅涉及已处理异常。

然而,在以下最小化的示例中,尽管它在立即窗口中作为第一次机会异常出现,但 Visual Studio 2010 对我来说并未停止异常:

编辑: 我发布的第一个最小化示例已由我收到的第一个答案修复,但不幸的是,以下示例仍存在问题:

using System;
using System.Net.Sockets;

namespace SocketTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var listener = new TcpListener(8080);
            listener.Start();
            AsyncCallback accepter = null;
            accepter = ar =>
            {
                var socket = listener.EndAcceptSocket(ar);
                var buffer = new byte[65536];
                AsyncCallback receiver = null;
                receiver = ar2 =>
                {
                    var bytesRead = socket.EndReceive(ar2);
                    throw new InvalidOperationException();
                    socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, receiver, null);
                };
                socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, receiver, null);
                listener.BeginAcceptSocket(accepter, null);
            };
            listener.BeginAcceptSocket(accepter, null);

            Console.WriteLine("Feel free to connect to port 8080 now.");
            Console.ReadLine();
        }
    }
}
如果你运行这个程序,在命令行中输入telnet localhost 8080,然后在telnet中键入任何字符,希望你能看到我看到的东西:程序会默默地中止。

为什么Visual Studio会吞掉这个异常?我怎么才能让它像通常一样在异常处中断?

(有趣的是,在BeginAcceptSocket回调中抛出异常确实被捕获了,使用Thread.Start启动的普通线程中也捕获了异常。我只能通过在BeginReceive回调中抛出异常来复现此问题。)


1
你是否已经附加到新线程? - Hogan
@Hogan:你是什么意思?我知道如何附加到一个进程,但是线程呢? - Timwi
这仍然是一个问题... 我正在考虑提供赏金。有人感兴趣吗? - Timwi
我刚刚尝试了你的设置,但它仍然会让我进入调试器... - Jon Skeet
你能否在帖子下面的选项->调试中更新设置?此外,在调试->异常中,你勾选了哪些(如果有)异常? - Attila
3个回答

13

由于您在异常设置中只看到“已抛出”复选框,我的怀疑是您的调试设置中启用了“.Net Framework源代码调试”。如果您启用“仅限我的代码”(禁用“.Net Framework源代码调试”),则应该会在异常设置对话框中看到“未处理的用户”和“已抛出”复选框,并且您还将在调试器中捕获异常。

胜利之截屏:

屏幕截图显示调试器按预期停在异常处


@Timwi - 我没有任何支持信息,但我猜测这与调试器无法理解用户代码和非用户代码之间的区别有关。无论是什么问题导致了调试器团队的困扰,“安全”的选择是完全禁用捕获用户未处理的异常。 - Ritch Melton
很抱歉,我不得不再次撤回我的勾选标记,因为它仍然无法正常工作。我将编辑问题,包括一个更大的测试用例,在其中我仍然遇到了静默终止和没有调试器中断。 - Timwi
那么就必须有另一个相关的选项,我们设置了不同的选项。或者这是一种竞争条件,取决于硬件。或者其他什么原因。 - Timwi
@Timwi - 我不知怎么地截断了我的最后一篇帖子,但我可以使用你的新代码捕获抛出的异常,但它并不被视为用户未处理。 - Ritch Melton
① 我也可以捕获异常。我不想捕获它,我希望Visual Studio在这里中断。② 当然,这不是生产代码。这只是一个最简示例来演示问题。真正的程序当然是一个涉及TCP的复杂系统,在其中连接处理程序可能会抛出异常,而我想要知道它(而不是程序突然消失)。 - Timwi
显示剩余5条评论

13

这是CLR 4版本中已知的一个bug。反馈文章在这里。一个可能的解决方法是将框架目标更改为3.5版本。我只引用反馈响应的相关部分:

 

我们调查了这个问题,并确定了CLR v4.0中存在一个bug导致了这个问题。进程确实会抛出异常(例如,您可以使用catch处理程序捕获它),但是调试器没有正确地通知未处理的异常。这使得进程似乎在没有任何来自调试器的指示的情况下退出。我们调查了潜在的修复方案,但所有的修复方案都有破坏其他功能的风险。因为它在产品周期的最后阶段,我们决定不修复此问题并冒着创建新错误的风险。我们将继续在下一个发布周期中跟踪此问题。

    

该问题仅限于从某些特定API调用创建线程的托管代码逃逸的异常:
  new System.Threading.Timer()
  ThreadPool.UnsafeQueueNativeOverloapped
  ThreadPool.BindHandle
  ThreadPool.RegisterWaitForSingleObject。

在这个特定的情况下,它是RegisterWaitForSingleObject()。


1
有趣。在发布这篇文章之前,我已经想到了将回调包装在new Thread(() => { ... }).Start();中的解决方法,但对于我实际遇到问题的“真实”代码,这只修复了问题代码的例子,而不是我的“真实”代码。结果发现我错误地应用了这个修复方法;我应该查看调用堆栈 :) 谢谢!您是否愿意将此解决方法添加到您的答案中?那会更好。 - Timwi
很遗憾你不更新你的答案。我猜我得自己动手了。 - Timwi

1

目前为止,这是我发现的:

  1. 如果在Debug->Exceptions对话框中没有明确检查任何异常,并且Options->Debugging->"Enable Just My Code"(EJMC)未被选中,则在任何回调函数中抛出的异常都不会以First Chance Exception(FCE)中断。

  2. 如果在Debug->Exceptions对话框中没有明确检查任何异常并且已选中EJMC,则在TCPListener的回调函数中抛出的异常将以FCE中断,但在Socket的回调函数中不会以FCE中断。

    a. 即使调用了TCPListener的阻塞AcceptSocket()(因此监听器没有回调),在Socket的回调函数中抛出的异常也不会以FCE中断。

  3. 如果在Debug->Exceptions中检查了System.InvalidOperationException(或System),则在任何回调函数中抛出的适当异常都将以FCE中断,无论是否选中了EJMC。

  4. 以上内容适用于将回调函数指定为lambda表达式或显式用户函数。

我不知道为什么,在调试->异常中未明确检查异常的情况下,VS在套接字回调中不会与FCE一起中断(这使得套接字回调与监听器回调有所不同)。


我能想到的一个(有些笨拙的)解决方法是,在回调函数的主体部分中包装一个try/catch块,它将重新抛出异常并在重新抛出时设置断点。无论异常设置如何,断点似乎都可以正常工作。 - Attila

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