Last active
February 9, 2024 17:59
-
-
Save eternityowo/3579e84cf690085271b00af04701aefb to your computer and use it in GitHub Desktop.
Deadlock and Task.WhenAll. Don't forget to use Task.Run or Task.Factory.StartNew
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Diagnostics; | |
using System.Threading.Channels; | |
await TestDeadlock(); | |
return; | |
var tests = new (string, Func<Task>)[] | |
{ | |
(nameof(TestSync), () => TestSync()), | |
(nameof(TestDelayAndYield), () => TestDelayAndYield()), | |
(nameof(TestSmallDelayAndConfigureAwaitForZero), () => TestSmallDelayAndConfigureAwaitForZero()), | |
(nameof(TestTaskRun), () => TestTaskRun()) | |
}; | |
var sw = new Stopwatch(); | |
PrintPid(true, Scope.Main); | |
foreach (var (testName, test) in tests) | |
{ | |
sw.Restart(); | |
PrintPid(true, Scope.Test, testName); | |
await test.Invoke(); | |
PrintPid(false, Scope.Test); | |
sw.Stop(); | |
Console.WriteLine(" total sleep: {0} ms", sw.ElapsedMilliseconds / 1000M); | |
} | |
PrintPid(false, Scope.Main); | |
return; | |
async Task<int> MethodAsync(int taskId, int sleepMs, int delayMs = 0, bool safeCtx = true, bool yield = false) | |
{ | |
var taskIdStr = $"tid: {taskId,2}, "; | |
var taskInfo = $"sleep: {sleepMs}, delay: {delayMs}, safeCtx: {safeCtx,5}, yield: {yield}"; | |
PrintPid(true, Scope.Task, taskIdStr + taskInfo); | |
if (yield) | |
{ | |
await Task.Yield(); | |
} | |
else | |
{ | |
await Task.Delay(delayMs).ConfigureAwait(safeCtx); | |
} | |
Thread.Sleep(sleepMs); | |
PrintPid(false, Scope.Task, taskIdStr); | |
return (int)Math.Sqrt(sleepMs); | |
} | |
/// <summary> | |
/// Sync execution of both task. | |
/// </summary> | |
async Task TestSync() | |
{ | |
var task0 = MethodAsync(0, 1000); | |
var task1 = MethodAsync(1, 2000); | |
await Task.WhenAll(task0, task1); | |
} | |
/// <summary> | |
/// First escape by delay, second escape by yield. | |
/// Leave caller thread after await some task inside | |
/// </summary> | |
async Task TestDelayAndYield() | |
{ | |
var task0 = MethodAsync(0, 1000, 10); | |
var task1 = MethodAsync(1, 2000, yield: true); | |
await Task.WhenAll(task0, task1); | |
} | |
/// <summary> | |
/// First escape by small delay, second stay on main. | |
/// Leave caller thread after await some task inside | |
/// </summary> | |
async Task TestSmallDelayAndConfigureAwaitForZero() | |
{ | |
var task0 = MethodAsync(0, 1000, 1); | |
var task1 = MethodAsync(1, 2000, 0, safeCtx: false); | |
await Task.WhenAll(task0, task1); | |
} | |
/// <summary> | |
/// Use the Task.Run(), Luke! | |
/// Both task start in another thread from begin | |
/// </summary> | |
async Task TestTaskRun() | |
{ | |
var task0 = Task.Run(() => MethodAsync(0, 1000)); | |
var task1 = Task.Factory.StartNew(() => MethodAsync(1, 2000)); | |
await Task.WhenAll(task0, task1); | |
} | |
/// <summary> | |
/// This case show how can we still get deadlock with async\await inside in method. | |
/// That is <see cref="TestSmallDelayAndConfigureAwaitForZero"/> and <see cref="TestDelayAndYield"/> | |
/// can be depends on inner behavior of awaited method. We can get the different result. | |
/// </summary> | |
async Task TestDeadlock() | |
{ | |
var source = new CancellationTokenSource(TimeSpan.FromSeconds(5)); | |
var channel = Channel.CreateBounded<int>(100); | |
var writeTask = new Task(async () => // Task.Run(async () => | |
{ | |
try | |
{ | |
foreach (var i in Enumerable.Range(0, 10000)) | |
{ | |
await channel.Writer.WriteAsync(i, source.Token); | |
} | |
} | |
catch (Exception ex) | |
{ | |
Console.WriteLine("U think u can exit by timeout? But u got lock on main thread"); | |
} | |
finally | |
{ | |
channel.Writer.TryComplete(); | |
} | |
}, TaskCreationOptions.PreferFairness | TaskCreationOptions.LongRunning); | |
var readTask = new Task(async () => // Task.Run(async () => | |
{ | |
try | |
{ | |
var sum = 0; | |
Console.Write("calc sum"); | |
while (await channel.Reader.WaitToReadAsync(source.Token)) | |
{ | |
var i = await channel.Reader.ReadAsync(source.Token); | |
sum += i; | |
Console.Write(new string('.', (i % 3)+1).PadRight(3)); | |
Console.SetCursorPosition(Console.CursorLeft - 3, Console.CursorTop); | |
} | |
Console.WriteLine(); | |
Console.WriteLine($"sum: {sum}"); | |
} | |
catch (Exception ex) | |
{ | |
Console.WriteLine("U think u can exit by timeout? But u got lock on main thread"); | |
} | |
}, TaskCreationOptions.PreferFairness | TaskCreationOptions.LongRunning); | |
await Task.WhenAll(writeTask, readTask); | |
} | |
void PrintPid(bool @in, Scope scope, string taskInfo = "") | |
{ | |
var pad = new string(' ', (int)scope); | |
var way = @in ? "in" : "out"; | |
var pid = Thread.GetCurrentProcessorId(); | |
Console.WriteLine($"{pad}{way,-4}({scope}) pid: {pid,2}. {taskInfo}"); | |
} | |
enum Scope | |
{ | |
Main = 0, | |
Test = 4, | |
Task = 8 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment