language-ext: Deadlock from calling Task.Result
Example 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.
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Reactions: 2
- Comments: 18 (18 by maintainers)
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
Monadapart intoMonadandMonadAsync. The attempt to unify the two has lead to a slightly messy interface and compromises like this implementation ofFoldforMTask; it should beFoldAsyncthat 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. 😃