0

I'm trying to wrap my brain around using async, await, and Tasks in C# and Unity. Here's the scenario:

  1. A user clicks a button.
  2. Its script (ex: MyScene.ButtonOnClick()) spawns a (custom) dialog box and enters a waiting pattern for the user's response.
  3. The user then chooses from three options (ex: enum DBResponse { Yes, No, Cancel }) by clicking on one of three buttons.
  4. The result is returned to the script behind the original button click.
  5. The original script resumes executing code using the response.

It seems like async, await, and Task<TResult> (that is, a "Task-based Asynchronous Pattern", or TAP ) would be the modern-day answer to this problem, rather than using events and listeners or Unity's Coroutines, but the only examples I can find have to do with computationally expensive routines or for reading/writing, not for user input.

Am I trying to use the wrong tool here? If not, then what can I put in the three buttons' OnClick() methods to get the user's response back to the original script? I'm struggling with flagging an async-in-progress that there's been an update and that it has the all-clear to proceed (which sounds an awful lot like listening for an event, which is also a weak spot for me).

In earlier times, I would use a static event manager and a series of listeners and invokers as the triggers. Most recently, I used a coroutine, but because that doesn't return a value, I stashed the response as a public static value, which seems very wrong.

This seems like such a simple and fundamental problem, but I haven't been able to crack it yet. I have a feeling that if this is the right tool, there's some basic usage of Task's methods and properties that I am lacking.

Pointers to additional reading would be very helpful!

Tuld
  • 23
  • 6
  • What does your "waiting pattern" involve, exactly? – Dai Jun 11 '21 at 23:11
  • Also, I note that Unity has its own peculiarities w.r.t. coroutines (e.g. it's been (ab)using `yield return` + `IEnumerable` for coroutines for years now, which isn't _exactly_ what it's intended for) - so I wonder if that might also be throwing you off. – Dai Jun 11 '21 at 23:12
  • I reopened this question because the marked dupe is not for Unity, and **Unity is different** enough from normal .NET environments to warrant its own question. – Dai Jun 11 '21 at 23:15
  • @Dai Holding pattern simply means blocking a method (the original ButtonOnClick() method, for example) from moving forward until a result is determined. At the moment, it calls StartCoroutine(WaitForThingToFinishThenRunThis(TheRestOfTheOperation)). – Tuld Jun 11 '21 at 23:38
  • Can you post a full example using `StartCoroutine`? Also, to confirm, are you asking about when you're writing code for Unity specifically, or are you asking in-general? – Dai Jun 11 '21 at 23:44

1 Answers1

-2

It seems like async, await, and Task (that is, a "Task-based Asynchronous Pattern", or TAP ) would be the modern-day answer to this problem

It's deceiving you. While it appears that would be a good use-case, it's not. The major advantage of using async/await pattern is to return threads that would normally wait for an IO result (Network, Hard Drive, etc) back to being available for other tasks. This is not that scenario.

I'd highly recommend watching Lucian Wischik take on wrapping an event to expose async/await

Lucian Wischik was/is on VB/C# language design team at Microsoft and at release one of the most knowledgeable guys at Microsoft regarding async/await.

So this is a very very rough example of how to wrap events for await consumption. After looking at it, you can decide if it's easier to read and easier to maintain instead of using events directly.

public async Task<DBResponse> ShowDialogAsync() 
{
  var tcs = new TaskCompletionSource<DBResponse>();
  EventHandler<object> lamda = (s,e) => tsc.TrySetResult(MyDialog.Result);

  try 
  {
    MyDialog.OnClose += lambda;
    MyDialog.Show();
    var result = await tcs.Task;
    return result;
  }
  finally
  {
    MyDialog.OnClose += lambda;
  }
}

So to be absolutely clear, this does not provide an alternative as you suggested in your question, because it's still using events.

It seems like async, await, and Task (that is, a "Task-based Asynchronous Pattern", or TAP ) would be the modern-day answer to this problem, rather than using events

Erik Philips
  • 53,428
  • 11
  • 128
  • 150
  • What makes you think awaiting IO while allowing a thread to do other work until it finishes is not a good idea? – Servy Jun 11 '21 at 23:15
  • @Blindy That condescension is uncalled for. First-class async in .NET is all about IO in practice! It's true that `Task` and TPL predates `async`, but the idea of using coroutines to reduce/eliminate threads blocked by IO is precisely why it's a big deal for modern development. Each thread takes up 1-4MB by default. – Dai Jun 11 '21 at 23:17
  • @Servy Where in the answer does it say that's "not a good idea"? Remember **this is specific to Unity**, not .NET in general. – Dai Jun 11 '21 at 23:18
  • @Dai If you think awaiting a task involved blocking a thread then you frankly need to be reading some intro to `await` tutorials, rather than trying to answer questions on the topic. Using the `async` feature is about *asynchronous* operations, not synchronous operations. – Servy Jun 11 '21 at 23:19
  • @Dai The answer saying, "It's deceiving you." and "This is not that scenario." suggest that. What about the answer makes you think it *is* suggesting that? – Servy Jun 11 '21 at 23:20
  • @Erik & Dai, I would be happy to entertain an alternative solution, if you care to provide one. – Tuld Jun 11 '21 at 23:26
  • 1
    @Servy I never said `await` blocks a thread nor that blocking a thread was a good idea (in general). – Dai Jun 11 '21 at 23:32
  • 1
    @Blindy Observe that `async`/`await` is a first-class language feature in C# since VS2010 (though I grant that the term "first class" is subjective). "and async has nothing to do with threads" is just demonstrably untrue if you look at the source-code of `SynchronizationContext` and more. But chiefly, the official documentation for `async` describes it in terms of threading: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/async – Dai Jun 11 '21 at 23:33
  • 1
    @Dai You said .Net, not C#. Stick to the truth. And `SynchronizationContext` still has nothing to do with threads, you can (and do) build single threaded synchronization contexts. In fact in this specific case this is exactly what you need. – Blindy Jun 11 '21 at 23:37
  • @Blindy You're nitpicking, that's distracting us from the conversation at hand. For all intents and purposes in _most contexts_ the terms ".NET" and "C#" _are interchangeable_, not least because all of the other major .NET languages (VB.NET and F#) support `async`/`await` too (source: I'm a former Microsoft SWE from the Visual Studio team - and I don't usually like to pull my credentials like this either...) – Dai Jun 11 '21 at 23:39
  • 1
    @Tuld Updated answer with an example of wrapping events for async/await. Good luck. – Erik Philips Jun 11 '21 at 23:51
  • @Dai You stated that it was important to not use `async` in order to avoid using more threads. .NET and C# aren't just interchangeable (particularly in a context of what concepts are first class and what aren't). Your link which you claim "describes `async` in terms of threads` mentions threads exactly once, and that's to say that awaiting something doesn't block the thread. It in fact indicates the *opposite* of your point, that `async` is about *asynchronous* operations, not multithreading. – Servy Jun 11 '21 at 23:53
  • I don't know if I'd say async has *nothing* to do with threading, as it was designed to be useful in multithread situations, but it's not *inherent* to asynchronous operations. – Servy Jun 11 '21 at 23:53
  • @Servy "You stated that it was important to not use async in order to avoid using more threads" - I never said that. If I did then it was a typo or other unintentional mistake. Do you have a link to my comment or post where I said that? I'd like to correct it. – Dai Jun 12 '21 at 00:05
  • @ErikPhilips And yet *this exact situation* is the situation where you say using it is a major advantage. This is a situation where there is IO that you want to respond to asynchronously. `async`, events, callbacks, coroutines, are all designed to solve that problem. *Both* events *and* tasks are designed for responding to something asynchronously, and any problem solved with one can be solved using the other using a simple transformation with minimal if any impact on runtime behavior. – Servy Jun 12 '21 at 00:09
  • @Servy "Your link which you claim "describes async in terms of threads` mentions threads exactly once" - yes, but I felt the single example gave was adequate. I know that `await` does not require concurrency nor does it directly imply that multi-threading is always being used. I think my stumbled over my words when the conversation got heated earlier on, for which I apologize. – Dai Jun 12 '21 at 00:09
  • @Dai https://stackoverflow.com/questions/67944301/can-async-await-be-used-for-user-input/67944333?noredirect=1#comment120092323_67944333 – Servy Jun 12 '21 at 00:09
  • @Dai How is stating that awaiting something doesn't block the execution of the thread a that point mean that `async` is inherently about threading? By that logic events are inherently about threading, and coroutines are inherently about threading, and callbacks are inherently about threading, because they all allow you to perform an operation without blocking a thread. `async` is inherently about threading exactly as much as all of those other concepts are. – Servy Jun 12 '21 at 00:11
  • @Servy I guess I should have been more specific to the advantage. If async/await was so advantageous for user input, I wonder why the team hasn't updated any user input features to include it? Probably because it didn't fall under that category. – Erik Philips Jun 12 '21 at 00:11
  • @Servy I'm not sure how you interpreted my comment that way. My point was that _normally_ doing IO using blocking methods like `FileStream.Read` and `NetworkStream.Write` _will_ block a thread. By using `ReadAsync`/`WriteAsync` (assuming the platform supports _true_ async IO, e.g. Win32 Overlapped IO, `io_uring` on Linux, etc) then no application threads will be blocked (though `io_uring` will block kernel threads, I understand) – Dai Jun 12 '21 at 00:12
  • @ErikPhilips But there are user input features that use the TPL... User input is just a specific type of IO. User ***Input*** is a type of **Input**, after all. – Servy Jun 12 '21 at 00:15
  • @Servy Okay, I see your position now - but I pointed that out in my comment where I wrote " is all about IO **in practice!**" - because _in practice_ the majority of the time `await` is being used in a multi-threaded or concurrent context because .NET uses the thread-pool for continuations the vast majority of the time - **or** `await` is being used in a UI Win32 window-message thread so operations don't block the message pump - then it really is about threading. – Dai Jun 12 '21 at 00:16
  • @Dai So how is that relevant to the question of whether you use `async` or a different implementation of performing asynchronous operations? No one was suggesting using methods that synchronously read from a file, and comparing anything anyone *was* suggesting to that is indicating that you think those other suggestions are comparable to a synchronous method reading a file, and they *aren't* analogous. – Servy Jun 12 '21 at 00:17
  • @servey that's not the point. It may be your point, but not the point of my post. Almost all other IO in .net has Async methods, user input is not one of them. There is probably a very good reason for that. – Erik Philips Jun 12 '21 at 00:20
  • @Dai Honestly, I find it's used in multithreaded context the *minority* of the time, but regardless, *this* isn't a situation that involves multithreading. But if you *meant* to say that it's *often* used in a multithreaded context then don't say it's *inherently* about threading, because those are two different things. Additionally, that it's *sometimes* used in threaded context is irrelevant *to this situation*, as it *doesn't* involve it here. Saying it *needs* to involve threading is wrong, saying it usually involves threading is at best off topic to this question. – Servy Jun 12 '21 at 00:20
  • @Servy I'm going to (respectfully) bow out of this conversation - I agree with what you're saying but this thread has gone on too long to be useful. – Dai Jun 12 '21 at 00:21
  • @ErikPhilips Again, there *are* plenty of situations where user input involves the TPL. What reason do you think user input shouldn't use the TPL? Just saying that there must be one because APIs that existed before the TPL was created isn't a really convincing argument. – Servy Jun 12 '21 at 00:22