Deadlock from calling Task.Result
See original GitHub issueExample Code
Here is a test that exhibits a deadlock.
[Fact]
public void MTaskFold_InAsyncContextWithTaskWaitingForActivation_Halts() =>
AsyncContext.Run(() => MTaskFold_WithTaskWaitingForActivation_Halts());
private static void MTaskFold_WithTaskWaitingForActivation_Halts() {
var intTask = TimeSpan
.FromMilliseconds(100)
.Apply(Task.Delay)
.ContinueWith(_ => 0);
var actual = default(MTask<int>)
.Fold(intTask, 0, (x, y) => 0)
(Unit.Default);
// execution terminates by reaching here
}
The type AsyncContext
is from the NuGet package Nito.AsyncEx.
Expected Behavior
As stated in the test, the expected behavior is that the test terminates.
Actual Behavior
The test does not terminate. Here is the source code under test.
[Pure]
public Func<Unit, S> Fold<S>(Task<A> ma, S state, Func<S, A, S> f) => _ =>
{
if (ma.IsFaulted) return state;
return f(state, ma.Result);
};
The test does not terminate because of a deadlock that is caused when calling ma.Result
.
Possible Fix
Stephen Cleary, an expert on this topic and author of the aforementioned NuGet package Nito.AsyncEx, explains why this deadlock occurs. To summarize, my understanding is that it is a best practice to not call Task.Result
unless Task.Status == TaskStatus.RanToCompletion
. This is not true in the example that I have provided. Instead it is in the state TaskStatus.WaitingForActivation
.
Otherwise, the best practice is to await
the task after calling Task.ConfigureAwait(false)
. I don’t see how you can do this without changing the return type of Fold
. If I may adjust the return type slightly from Func<Unit, S>
to Func<Unit, Task<S>>
, then I would implement Stephen’s best practice suggestion like this.
[Pure]
public Func<Unit, Task<S>> Fold<S>(Task<A> ma, S state, Func<S, A, S> f) => async _ =>
{
if (ma.IsFaulted) return state;
return f(state, await ma.ConfigureAwait(false));
};
This new return type seems more natural to me. My understanding of monads is that you are not allowed to just extract the underlying value of the monad. Unfortunately, Fold
is doing just that when it calls Task.Result
. However, I have only recently started using monads and your library, so I know that I still have a great deal to learn and that it is very possible that I am misunderstanding something here.
Thank you for considering my bug report. I am interested to hear your thoughts about it.
Issue Analytics
- State:
- Created 6 years ago
- Reactions:2
- Comments:18 (18 by maintainers)
Top GitHub Comments
You’re both right for different reasons. Yes, the quick fix would be to asyncify the code. But what I’ve actually started doing is looking into breaking
Monad
apart intoMonad
andMonadAsync
. The attempt to unify the two has lead to a slightly messy interface and compromises like this implementation ofFold
forMTask
; it should beFoldAsync
that returns aTask
.I have a busy few days coming up, but hopefully I should have something by the end of the week.
Yeah, I tried to fix the bug in the repo and immediately ran into many issues…
I think breaking Monad into Monad and MonadAsync does seem likely the much better approach.
I’m looking forward to this. 😃