Skip to content

Commit 914c10f

Browse files
Add analyzer for async delegates with LongRunning tasks (#311)
Introduces EPC36, which warns when async delegates are used with Task.Factory.StartNew and TaskCreationOptions.LongRunning. Adds analyzer implementation, tests, documentation, and diagnostic descriptor. This helps prevent inefficient use of threads and clarifies intent when working with async code.
1 parent bcb8439 commit 914c10f

File tree

5 files changed

+571
-1
lines changed

5 files changed

+571
-1
lines changed

docs/Rules/EPC36.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# EPC36 - Do not use async delegates with Task.Factory.StartNew and TaskCreationOptions.LongRunning
2+
3+
This analyzer warns when an async delegate is used with `Task.Factory.StartNew` and `TaskCreationOptions.LongRunning`.
4+
5+
## Description
6+
7+
`TaskCreationOptions.LongRunning` is intended for long-running synchronous work that would otherwise block a thread pool thread for an extended period. When used with async delegates, it defeats the purpose of async programming by:
8+
9+
1. **Wasting thread pool threads**: The LongRunning option creates a dedicated thread that will be used only before the first `await` in the async delegate (or async method).
10+
3. **Creating confusion**: The intent of the code becomes unclear - is it meant to be long-running synchronous work or efficient async work? Is it really the case that the synchronous block of the async method is taking a long time, or it's just a mistake?
11+
12+
## Code that triggers the analyzer
13+
14+
**Bad** - Using async lambda with LongRunning:
15+
```csharp
16+
using System;
17+
using System.Threading.Tasks;
18+
19+
class Example
20+
{
21+
void BadExamples()
22+
{
23+
// ❌ EPC36: Async lambda with LongRunning
24+
Task.Factory.StartNew(async () =>
25+
{
26+
await SomeAsyncOperation();
27+
}, TaskCreationOptions.LongRunning);
28+
29+
// ❌ EPC36: Async delegate with LongRunning
30+
Task.Factory.StartNew(async delegate()
31+
{
32+
await SomeAsyncOperation();
33+
}, TaskCreationOptions.LongRunning);
34+
35+
// ❌ EPC36: Async method reference with LongRunning
36+
Task.Factory.StartNew(SomeAsyncMethod, TaskCreationOptions.LongRunning);
37+
38+
// ❌ EPC36: Combined options including LongRunning
39+
Task.Factory.StartNew(async () =>
40+
{
41+
await SomeAsyncOperation();
42+
}, TaskCreationOptions.LongRunning | TaskCreationOptions.AttachedToParent);
43+
}
44+
45+
async Task SomeAsyncMethod() => await Task.Delay(100);
46+
async Task SomeAsyncOperation() => await Task.Delay(1000);
47+
}
48+
```
49+
50+
## How to fix
51+
52+
Choose the appropriate approach based on your intent:
53+
54+
**Good** - Use `Task.Run` for async delegates:
55+
```csharp
56+
using System;
57+
using System.Threading.Tasks;
58+
59+
class Example
60+
{
61+
void GoodExamples()
62+
{
63+
// ✅ Correct: Use Task.Run for async work
64+
Task.Run(async () =>
65+
{
66+
await SomeAsyncOperation();
67+
});
68+
69+
// ✅ Correct: Task.Run with async method reference
70+
Task.Run(SomeAsyncMethod);
71+
72+
// ✅ Correct: If you need specific task creation options (except LongRunning)
73+
Task.Factory.StartNew(async () =>
74+
{
75+
await SomeAsyncOperation();
76+
}, TaskCreationOptions.AttachedToParent);
77+
}
78+
79+
async Task SomeAsyncMethod() => await Task.Delay(100);
80+
async Task SomeAsyncOperation() => await Task.Delay(1000);
81+
}
82+
```
83+
84+
**Good** - Use LongRunning only for truly long-running synchronous work:
85+
```csharp
86+
using System;
87+
using System.Threading.Tasks;
88+
89+
class Example
90+
{
91+
void LongRunningSyncWork()
92+
{
93+
// ✅ Correct: LongRunning with synchronous work
94+
Task.Factory.StartNew(() =>
95+
{
96+
// Long-running CPU-intensive or blocking synchronous operation
97+
for (int i = 0; i < 1_000_000_000; i++)
98+
{
99+
// Some heavy computation
100+
ProcessData(i);
101+
}
102+
}, TaskCreationOptions.LongRunning);
103+
104+
// ✅ Correct: LongRunning with blocking I/O that can't be made async
105+
Task.Factory.StartNew(() =>
106+
{
107+
// Legacy blocking operation that can't be easily made async
108+
LegacyBlockingOperation();
109+
}, TaskCreationOptions.LongRunning);
110+
}
111+
112+
void ProcessData(int value) { /* CPU work */ }
113+
void LegacyBlockingOperation() { /* Blocking I/O */ }
114+
}
115+
```
116+
117+
## When to use each approach
118+
119+
- **`Task.Run`**: For async operations or when you need to run async code on the thread pool
120+
- **`Task.Factory.StartNew` without LongRunning**: When you need specific task creation options but still want efficient async execution
121+
- **`Task.Factory.StartNew` with LongRunning**: Only for long-running synchronous operations that would otherwise tie up thread pool threads
122+
123+
## Performance impact
124+
125+
Using async delegates with LongRunning can lead to:
126+
- **Thread pool starvation**: Each LongRunning task consumes a dedicated thread
127+
- **Increased memory usage**: Each thread consumes approximately 1MB of virtual memory for its stack
128+
- **Reduced scalability**: The application cannot efficiently handle many concurrent operations
129+
130+
## Examples in context
131+
132+
**Bad** - Common anti-pattern:
133+
```csharp
134+
// This creates a dedicated thread that just waits for async operations
135+
var tasks = new List<Task>();
136+
for (int i = 0; i < 100; i++)
137+
{
138+
tasks.Add(Task.Factory.StartNew(async () =>
139+
{
140+
await DownloadFileAsync($"file{i}.txt"); // ❌ EPC36
141+
}, TaskCreationOptions.LongRunning));
142+
}
143+
await Task.WhenAll(tasks);
144+
```
145+
146+
**Good** - Efficient async pattern:
147+
```csharp
148+
// This uses the thread pool efficiently for async operations
149+
var tasks = new List<Task>();
150+
for (int i = 0; i < 100; i++)
151+
{
152+
tasks.Add(Task.Run(async () =>
153+
{
154+
await DownloadFileAsync($"file{i}.txt"); // ✅ Correct
155+
}));
156+
}
157+
await Task.WhenAll(tasks);
158+
```
159+
160+
## See also
161+
162+
- [Task.Run vs Task.Factory.StartNew](https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-run-vs-task-factory-startnew)
163+
- [TaskCreationOptions.LongRunning documentation](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions)
164+
- [Async/await best practices](https://docs.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming)

0 commit comments

Comments
 (0)