C#中的Task.WaitAll()在.Net和Mono中的应用

3
为什么这段代码在Windows和Linux(使用Mono)上表现不同?
static void Main(string[] args)
{
    Stopwatch stopwatch = Stopwatch.StartNew();
    Task[] tasks = new Task[1];

    tasks[0] = Task.Run(() =>
    {
        IPHostEntry iphe = Dns.GetHostEntry("8.8.8.8.dnsrbl.org");
    });
    Task.WaitAll(tasks, 2000);
    Console.WriteLine("Done in " + stopwatch.ElapsedMilliseconds + " ms");
}
8.8.8.8.dnsrbl.ru是一个最终会超时的查询示例。我认为没有工作的DNS服务器(或其防火墙正在阻止我)。

无论如何,关键不在于从DNS服务器获取结果,而在于当等待包含对Dns.GetHostEntry()的调用的任务时,Task.WaitAll()在Windows和Mono上的行为如何。

在Windows上,当查询在超时期限(2s)内未返回任何结果时,程序运行需要大约2秒钟。也就是说,带有超时的Task.WaitAll似乎起作用。在使用Mono在Linux上运行此程序需要2秒才能获得输出,但是该程序直到任务退出后才终止。为什么会这样呢?

无论是否使用带有超时的Time.WaitAll,我似乎都得到相同的执行时间。

Dns.GetHostEntry()的关键在于,如果我启动具有Thread.Sleep()的任务来模拟长时间运行的任务,则Task.WaitAll()按预期工作。下面是可以正常工作的示例代码:

tasks[0] = Task.Run(() => Thread.Sleep(10000));

有没有一种方法可以强制Task.WaitAll(Task [] tasks,int millisecondsTimeout)在Mono中运行时实际超时?

编辑:Task.WaitAll()确实在超时后返回,但是在Mono中运行时程序不会终止(直到Dns.GetHostEntry 超时)。

这不是编译器的问题。无论我使用Visual Studio还是Mono C#编译器编译,都会得到相同的结果。


但是你说过 Task.WaitAll 即使在Mono上也会正确超时(输出显示为2秒,这意味着 Task.WaitAll 在2秒内返回)。 - Evk
是的,我的错。抱歉。程序似乎在等待。我会编辑问题。 - Jeff Merlin
Evk,当删除Task.WaitAll时,在两个环境中都立即完成(如预期)。 - Jeff Merlin
1
如果您开始一个新任务,然后放置 Thread.Sleep(2000) 并退出呢? - Evk
1
这意味着(很可能)在Linux上使用Dns.GetHostEntry会启动新的非后台线程。程序无法完成,直到所有非后台线程都完成。确认这一点很困难,因为在Mono中,Dns.GetHostEntry最终会调用一些难以分析(甚至难以找到)源代码的本地函数。 - Evk
显示剩余6条评论
1个回答

3

虽然功劳应该归于指导我正确方向的 Evk(谢谢你,伙计!),但我会回答自己的问题。

这个问题的主题可以说是糟糕的。问题与Task.WaitAll无关,而是与Mono实现的Dns.GetHostEntry有关。正如Evk在评论中所说:

这意味着(很可能)在Linux上,Dns.GetHostEntry启动了新的非后台线程。程序无法完成,直到所有非后台线程都完成。

GetHostEntry()方法位于源文件Dns.cs中,当使用字符串调用它时,它调用GetHostByName,然后调用GetHostByName_internal,后者是位于w32socket.c中的外部C函数。最后调用mono_get_address_info(在networking-posix.c中),进入libc函数getaddrinfo。哇!

我看不到任何新的非后台线程被启动,但我发现了这个:

MONO_ENTER_GC_SAFE;
ret = getaddrinfo (hostname, service_name, &hints, &info);
MONO_EXIT_GC_SAFE;

MONO_ENTER_GC_SAFEMONO_EXIT_GC_SAFE是在mono-threads-api.h中定义的宏。

#define MONO_ENTER_GC_SAFE  \
    do {    \
        gpointer __gc_safe_dummy;   \
        gpointer __gc_safe_cookie = mono_threads_enter_gc_safe_region (&__gc_safe_dummy)

#define MONO_EXIT_GC_SAFE   \
        mono_threads_exit_gc_safe_region (__gc_safe_cookie, &__gc_safe_dummy);  \
    } while (0)

我没有深入探究,但我相信Evk是正确的。

所以,回答我的问题:在Mono中无法终止或取消Dns.GetHostEntry()。调用此方法的程序将一直等到所有查询已处理完毕或超时为止。就是这样。我的猜测是这与垃圾收集器(GC)有关,它可能在非后台线程中运行,因此无法被取消/终止。

编辑:(几天后)当我查阅getaddrinfo的手册时,原因显而易见。此函数返回结果的链接列表。当然,此列表在堆上分配,并且必须在某个时间点释放以避免内存泄漏。该函数是freeaddrinfo

无论如何,再次感谢Evk!

那么我们如何使用Mono并行发出多个DNS查询并设置超时呢?很简单!可以在Task中调用此函数,并乐意执行带有超时的WaitAll:

private static string host(string query)
{
    ProcessStartInfo psi = new ProcessStartInfo("host", query);
    psi.UseShellExecute = false;
    psi.RedirectStandardOutput = true;
    Process p = Process.Start(psi);
    return p.StandardOutput.ReadToEnd();
}

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