[API Proposal]: Expose ThreadPool.NotifyThreadBlocked()
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 25/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Stale
- Tech stack
- csharp
- Domain
- backend-api-design
Research direction
Start by reviewing the existing internal NotifyThreadBlocked and NotifyThreadUnblocked methods, plus the Task.Wait change in .NET 6 referenced by the proposal. Evaluate the proposed ThreadPool.NotifyThreadBlocked() and ThreadBlockedScope API across ThreadPool implementations; done means the public API design, behavior, and platform implications are resolved.
Written by the indexing model from the issue text.
Description
Background and motivation
The ThreadPool can be inappropriately starved of threads if many ThreadPool threads block. The ideal solution to that problem is for applications to write async code to avoid blocking threads. However, sometimes it is impossible for applications to write async code because of a requirement to call a sync API that has no async equivalent.
The particular scenario that I am dealing with is that my ASP.NET app frequently opens handles to files on network storage. Sometimes, the network storage appliance does not open file handles in a timely manner, and so my thread pool threads become blocked for non-trivial amounts of time, which causes the ThreadPool to starve, which causes other requests to not be served in a timely manner. There is no way to open a file handle asynchronously, so I cannot solve this problem by switching to async code.
In .NET 6, PR https://github.com/dotnet/runtime/pull/53471 modified Task.Wait() to notify the ThreadPool that it is blocking a thread, which informs the ThreadPool to inject new threads more aggressively, My goal with this proposal is to enable application developers to similarly notify the ThreadPool of a thread blocking for reasons other than waiting on a Task.
API Proposal
namespace System.Threading;
public static partial class ThreadPool
{
public static ThreadBlockedScope NotifyThreadBlocked();
}
public ref struct ThreadBlockedScope : IDisposable
{
public void Dispose();
}
The NotifyThreadBlocked() method is modeled after the existing internal method of the same name, but it returns a new ThreadBlockedScope ref struct that notifies the runtime of thread unblock when it is disposed, whereas the pre-existing internal method returns a bool that indicates whether a separate NotifyThreadUnblocked() method should be called upon unblock.
This aspect of the proposal exists to protect against potential misuse that could occur if the existing internal methods were exposed directly, such as erroneous additional or missing calls to NotifyThreadUnblocked().
Calling NotifyThreadBlocked() will be taken to indicate that the current thread is about to block, as opposed to incrementing a process-wide counter of the number of threads blocked. This means that nested calls on the same thread will be ignored, which is a valuable behavior to allow future .NET versions to decide to add calls to NotifyThreadBlocked() within an existing APIs (e.g., Thread.Sleep()) without concern that the blocked thread will be double counted due to application code already calling NotifyThreadBlocked() before calling that existing API. Since ThreadBlockedScope is a ref struct, disposal is guaranteed to occur on the same thread that called NotifyThreadBlocked(), similar to the recently introduced Lock.Scope type.
API Usage
public SafeFileHandle OpenNetworkFile(string networkFilePath)
{
using (ThreadBlockedScope blockScope = ThreadPool.NotifyThreadBlocked())
{
return NativeMethods.CreateFileW(networkFilePath, ...);
}
}
This example uses a call to the Win32 API CreateFileW() to open a file handle instead of using File.OpenHandle() to open a file handle. I wrote the example this way to sidestep any discussion about whether the problem is better solved by having File.OpenHandle() internally call the existing internal ThreadPool APIs. For the record, I think that File.OpenHandle() probably shouldn't call them, since most files opened by .NET applications in general are not in network storage, so making every caller pay the overhead of notifying the thread pool may not be worth it. Regardless, my ASP.NET app uses native code to open the handle to the network file, so modifying File.OpenHandle() to call the existing internal ThreadPool APIs would not solve my problem.
Alternative Designs
-
As discussed above, relevant .NET runtime APIs such as File.OpenHandle() or Thread.Sleep() could be modified to call the existing internal ThreadPool APIs instead of exposing the ThreadPool APIs publicly for applications to call. However, this does not solve my case of opening a file handle via native code unless LibraryImportAttribute is enhanced to support calling these APIs, which I'm guessing would an undesirable entanglement of layers. Also, it's not clear that the .NET runtime can always determine when its APIs, such as File.OpenHandle(), are sufficiently likely to block to justify notifying the ThreadPool that the thread is potentially blocking.
-
Instead of depending on apps to manually indicate with these new ThreadPool APIs that a thread is about to block, perhaps there is some way to solve thread exhaustion with changes internal to the ThreadPool thread injection algorithm, such as with automated detection of blocked threads. I don't have any practical suggestion about how this could be accomplished, though.
-
For my particular concern about opening files always being synchronous, perhaps it is possible to open files asynchronously on Linux with io_uring. My ASP.NET app runs on Windows, though, where I believe asynchronous file opens to be impossible. Maybe the Windows team could be contacted to request this feature though :).
Risks
-
For some ThreadPool implementations, there may be no useful implementation for these APIs, such as when the native Windows thread pool is used. Therefore, use of these APIs may increase behavioral differences between the same application running under different deployment scenarios, or different versions of .NET as ThreadPool implementations are modified. This does not seem a significant concern because the APIs are only performance hints, not guaranteeing specific behavior. Also, the behavioral differences have already been introduced via Task.Wait() in .NET 6+.
-
It may be unclear to application developers when it is or isn't beneficial for them to call ThreadPool.NotifyThreadBlocked(). For example, Task.Wait() already calls it internally, so applications do not need to call it in that case, but the only way to know that is to read the .NET runtime source code. This could be mitigated with docs for ThreadPool.NotifyThreadBlocked() that list all the runtime APIs that already call it internally, if that list remains sufficiently small. In any case, the impact of an application adding erroneous additional calls to these APIs is probably not significant.
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dotnet/runtime
-
agentic-workflows untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
area-System.Reflection blocking-clean-ci-optional Known Build Error os-mac-os-x untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
area-CodeGen-coreclr untriaged
Difficulty 1/5 Under an hour Newbie friendliness 92/100
-
agentic-workflows untriaged
Difficulty 1/5 Under an hour Newbie friendliness 78/100
-
area-VM-meta-mono untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
:watch: Not Triaged 11.0 fundamentals/subsvc
Difficulty 2/5 1-3 hours Newbie friendliness 92/100
dotnet/AspNetCore.Docs#37699 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
SubtitleEdit/subtitleedit#15108 · 1 comment ·
-
area/docs-content Bug pulumi/docs
Difficulty 1/5 1-3 hours Newbie friendliness 94/100
-
Create parent directories only after the containment check in InstallHelper.TryExtractToDirectory Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
PowerShell/PSResourceGet#2056 ·