如果您搜索IsIoPending
(这是我遇到不熟悉的东西时所做的第一件事),你会看到再往下看,它被调用,前面有以下注释:
// We can't exit a thread that has pending I/O - we'll "retire" it instead. https://github.com/dotnet/coreclr/blob/master/src/vm/win32threadpool.cpp#L3500
这几乎回答了你的问题。为什么在允许工作线程退出之前我们需要检查它是否有任何待处理的 I/O?嗯,因为我们无法退出具有挂起 I/O 的线程。
我想剩下的唯一问题是why not?为什么我们不能退出有待处理 I/O 的线程?为了调查这个问题,让我们看看是什么IsIoPending
实际上does。在文件中进一步搜索,您会发现其实施 https://github.com/dotnet/coreclr/blob/master/src/vm/win32threadpool.cpp#L3916:
// 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 A适应Layer,它只是一种标记代码只能在 Windows 上运行的方法。如果FEATURE_PAL
已定义,然后正在为操作系统编译框架other比 Windows 更重要,因此需要排除 Windows 特定的代码。这正是你在这里看到的——当FEATURE_FAL
被定义,这IsIoPending
函数只返回FALSE
。仅当它运行在 Windows 之上时(当FEATURE_PAL
is not已定义)是否需要检查 I/O 是否处于挂起状态。这非常强烈地表明,上面关于无法退出具有挂起 I/O 的线程的评论是指 Windows 操作系统的规则。
如果我们在 Windows 上运行会发生什么?对 Windows API 函数进行调用(通过全局函数指针间接调用)NtQueryInformationThread https://msdn.microsoft.com/en-us/library/windows/desktop/ms684283.aspx。第一个参数传递当前线程的句柄(GetCurrentThread()
),第二个参数请求线程信息类ThreadIsIoPending
,接下来的两个参数允许函数填写ULONG
多变的IsIoPending
(他们传递它的大小和指向它的指针)。
如果您尝试阅读文档NtQueryInformationThread
,您会看到它是一个内部函数,建议应用程序:
使用公共函数GetThreadIOPendingFlag https://msdn.microsoft.com/en-us/library/windows/desktop/ms683234(v=vs.85).aspx而是获取此信息。
.NET 源代码不遵循此建议,因为该函数 (GetThreadIOPendingFlag
)直到 Windows XP SP1 才被引入,并且 .NET 4(可能还有为该代码编写的旧版本)需要在 Windows 的低版本上运行。因此,他们只是调用了所有受支持的 Windows 版本上可用的内部函数。
无论如何,文档GetThreadIOPendingFlag
几乎证实了它做了我们怀疑的事情:如果线程有任何待处理的 I/O 请求,则返回 true,否则返回 false。 .NET Framework 实现调用的内部函数将返回相同的信息。
现在我想我们又回到了最初的问题:为什么线程是否有挂起的 I/O 很重要?为什么我们需要在杀死它之前检查它?在 Windows 中,线程发出的 I/O 请求与该特定线程有着千丝万缕的联系。无法将此 I/O 请求或其结果数据的所有权移至另一个线程。换句话说,用户模式IRPs https://msdn.microsoft.com/en-us/library/windows/hardware/ff550694(v=vs.85).aspx不能比最初创建它们的线程寿命更长。如果线程退出,所有挂起的 I/O 将被毫不客气地取消,并且永远不会完成。因此,我们得到了原始注释中如此简洁地表述的规则:如果一个线程有挂起的 I/O,则它不能退出(因为那样 I/O 将永远不会完成并且将永远丢失)。
The GetThreadIOPendingFlag
函数(或NtQueryInformationThread
与类ThreadIsIoPending
) 只是检查指定线程的活动 IRP 列表,看看它是否为空。如果没有待处理的 I/O 请求,则可以安全地退出线程。
工作线程可能有挂起的 I/O 请求的原因有很多,但最常见的情况是如果重叠(异步)I/O 请求 https://msdn.microsoft.com/en-us/library/windows/desktop/ms686358(v=vs.85).aspx由线程发出。在这种情况下,超时可能会在发出 I/O 完成信号之前就过去了。依赖于发出线程的异步 I/O 是 Win32 体系结构的一个基本限制,.NET Framework 的线程池实现意识到了这一限制并对其进行了解释。
可能是这个检查通常返回 false,但为了安全起见,最好明确检查它。这些是标准的防御性编程实践——对于在全球范围内运行、在各种不同条件下运行并且需要尽可能健壮的框架来说,这是非常重要的实践。