为什么我们需要在工作线程退出时检查 IsIoPending?

Why we need to check the IsIoPending when the worker thread going to exit?

从win32threadpool.cpp我们知道,在工作线程通过检查20s超时退出之前,它需要通过IsIoPending()方法检查是否有任何IO挂起,根据我的理解:

1,当工作线程要退出时,它必须完成它的工作并且return到线程池。

2,根据上面的1,应该是没有IO pending的线程要退出了。

所以我的问题是为什么我们需要在线程即将死亡时检查 IO pending?或者,我们如何模拟上述情况发生?

RetryWaitForWork:
if (!WorkerSemaphore->Wait(AppX::IsAppXProcess() ? WorkerTimeoutAppX : WorkerTimeout))
{
    if (!IsIoPending())
    {




while (true)
{
RetryRetire:
    DWORD result = RetiredWorkerSemaphore->Wait(AppX::IsAppXProcess() ? WorkerTimeoutAppX : WorkerTimeout, FALSE);
    _ASSERTE(WAIT_OBJECT_0 == result || WAIT_TIMEOUT == result);

    if (WAIT_OBJECT_0 == result)
    {
        foundWork = true;
        counts = WorkerCounter.GetCleanCounts();
        FireEtwThreadPoolWorkerThreadRetirementStop(counts.NumActive, counts.NumRetired, GetClrInstanceId());
        goto Work;
    }

    if (!IsIoPending())
    {

https://github.com/dotnet/coreclr/blob/master/src/vm/win32threadpool.cpp

如果你搜索 IsIoPending(这是我遇到不熟悉的东西时首先要做的事情之一),你会看到更靠下一点的,它被称为它前面的以下评论:

// We can't exit a thread that has pending I/O - we'll "retire" it instead.

这几乎可以回答您的问题。为什么我们需要在允许它退出之前检查工作线程是否有任何 I/O 挂起?好吧,因为我们无法退出具有挂起 I/O.

的线程

唯一剩下的问题是,我想,为什么不?为什么我们不能退出具有挂起 I/O 的线程?为了对此进行调查,让我们看看 IsIoPending 实际上 做了什么 。进一步搜索文件,您会发现 its implementation:

// Returns true if there is pending io on the thread.
BOOL ThreadpoolMgr::IsIoPending()
{
    CONTRACTL
    {
        NOTHROW;         
        MODE_ANY;
        GC_NOTRIGGER;
    }
    CONTRACTL_END;

#ifndef FEATURE_PAL
    int Status;
    ULONG IsIoPending;

    if (g_pufnNtQueryInformationThread)
    {
        Status =(int) (*g_pufnNtQueryInformationThread)(GetCurrentThread(),
                                          ThreadIsIoPending,
                                          &IsIoPending,
                                          sizeof(IsIoPending),
                                          NULL);


        if ((Status < 0) || IsIoPending)
            return TRUE;
        else
            return FALSE;
    }
    return TRUE;
#else
    return FALSE;
#endif // !FEATURE_PAL
}

这里的评论并没有告诉我们很多信息,除了确认该函数的名称是否正确以及它是否按照我们的想法行事!但是它的实现呢?

好吧,您注意到的第一件事是大多数有趣的东西都被 #ifndef FEATURE_PAL 条件测试封锁了。那么什么是FEATURE_PAL呢? PAL代表Platform Adaptation Layer,它只是一种标记代码的方式只能在 Windows 上工作。如果定义了 FEATURE_PAL,那么框架将针对 OS other 而不是 Windows 进行编译,因此 Windows 特定代码需要被排除在外。这正是您在这里看到的 — 当定义 FEATURE_FAL 时,此 IsIoPending 函数只是 returns FALSE。只有当它 运行ning 在 Windows 之上时(当 FEATURE_PAL 定义时)才需要检查 I/O 是否是待办的。这非常强烈地表明上面关于无法退出具有挂起 I/O 的线程的评论是指 Windows 操作系统的规则。

如果我们 运行 在 Windows 上会发生什么?调用(间接地,通过全局函数指针)Windows API 函数 NtQueryInformationThread。第一个参数传递当前线程的句柄(GetCurrentThread()),第二个参数请求线程信息class ThreadIsIoPending,接下来的两个参数允许函数填写ULONG 变量 IsIoPending (他们传递它的大小和指向它的指针)。

如果您尝试阅读 NtQueryInformationThread 的文档,您会发现它是一个内部函数,建议应用程序:

Use the public function GetThreadIOPendingFlag instead to obtain this information.

.NET 源代码不遵循此建议,因为该函数 (GetThreadIOPendingFlag) 直到 Windows XP SP1 和 .NET 4(可能是旧版本,为此此代码是在 Windows 的下层版本上 运行 需要编写的)。所以他们只是调用了 Windows.

所有受支持版本上可用的内部函数

无论如何,GetThreadIOPendingFlag 的文档几乎证实了它做了我们所怀疑的事情:如果线程有任何 I/O 待处理请求,它 return 为真,或者否则为假。 .NET Framework 实现调用的内部函数将 return 相同的信息。

现在我想我们又回到了最初的问题:为什么一个线程是否有任何未决的 I/O 很重要?为什么我们在杀死它之前需要检查它?好吧,在 Windows 中,线程发出的 I/O 请求与该特定线程密不可分。无法将此 I/O 请求的所有权(或其结果数据)转移到另一个线程。换句话说,用户模式 ​​IRPs 不能超过最初创建它们的线程。如果线程退出,所有pending的I/O都会被毫不客气的取消,永远不会完成。因此,我们得到了原始评论中如此简洁地陈述的规则:如果一个线程有待处理的 I/O,它就不能退出(因为那样 I/O 将永远不会完成,并且会永远丢失)。

GetThreadIOPendingFlag 函数(或 NtQueryInformationThread 和 class ThreadIsIoPending)只是检查指定线程的活动 IRP 列表是否为空。如果没有 I/O 请求处于待处理状态,则退出线程是安全的。

工作线程可能有待处理的 I/O 请求的原因有很多,但最常见的情况是线程发出了 overlapped (asynchronous) I/O request。在这种情况下,超时可能会在发出 I/O 完成信号之前结束。异步 I/O 依赖于发布线程是 Win32 体系结构的一个基本限制,线程池的 .NET Framework 实现意识到了这一限制并解决了这一问题。

此检查 通常 return 可能是错误的,但为了安全起见,明确检查它是个好主意。这些是标准的防御性编程实践——对于在全球范围内发售的框架而言,这是一个非常重要的实践,运行 处于各种不同的条件下,并且需要尽可能稳健。