-
Notifications
You must be signed in to change notification settings - Fork 102
Expand file tree
/
Copy pathAsyncHelper.fs
More file actions
78 lines (58 loc) · 2.71 KB
/
AsyncHelper.fs
File metadata and controls
78 lines (58 loc) · 2.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
module Plotly.NET.ImageExport.AsyncHelper
open System.Threading
open System.Threading.Tasks
(*
This is a workaround to avoid deadlocks
https://medium.com/rubrikkgroup/understanding-async-avoiding-deadlocks-e41f8f2c6f5d
TL;DR in many cases, for example, GUI apps, SynchronizationContext
is overriden to *post* the executing code on the initial (UI) thread. For example,
consider this code
public async Task OnClick1()
{
var chart = ...;
var base64 = ImageExport.toBase64PNGStringAsync()(chart).Result;
myButton.Text = base64;
}
Here we have an async method. Normally you should use await and not use .Result, but
assume for some reason the sync version is used. What happens under the hood is,
public async Task OnClick1()
{
var chart = ...;
var task = ImageExport.toBase64PNGStringAsync()(chart);
task.ContinueWith(() =>
UIThread.Schedule(() =>
myButton.Text = Result;
)
);
task.Wait();
}
(this is pseudo-code)
So basically, we set the task to wait until it finishes. However, part of it being
finished is to actually execute the code with button.Text = .... The waiting happens
on the UI thread, exactly on the same thread as where we're waiting for it to do
another job!
That's not the only place we potentially deadlock by using fake synchronous functions.
The reason why it happens, is because frameworks (or actually anyone) override
SynchronizationContext. In GUI and game development it's very useful to keep UI logic
on one thread. But our rendering does not ever callback to it, we're independent of
where the logic actually happens.
That's why what we do is we set the synchronization context to null, do the job, and
then restore it. It is a workaround, because it doesn't have to work everywhere and
independently. But it will work for most cases.
When will it also break? For example, if we decide to take in some callback as a para-
meter, which potentially accesses the UI thread (or whatever). In Unity, for instance,
you can only access Unity API from the main thread. So our fake synchronous function
will crash in the end, because due to the overriden (by us) sync context, the callback
will be executed in some random thread (as opposed to being posted back to the UI one).
However, our solution should work in most cases.
Credit to [@DaZombieKiller](https://github.com/DaZombieKiller) for helping.
*)
let runSync job input =
let current = SynchronizationContext.Current
SynchronizationContext.SetSynchronizationContext null
try
job input
finally
SynchronizationContext.SetSynchronizationContext current
let taskSync (task: Task<'a>) = task |> runSync (fun t -> t.Result)
let taskSyncUnit (task: Task) = task |> runSync (fun t -> t.Wait())