1

Suppose (entirely hypothetically ;)) I have a big pile of async code.

10s of classes; 100s of async methods, of which 10s are actually doing async work (e.g. where we WriteToDbAsync(data) or we ReadFileFromInternetAsync(uri), or when WhenAll(parallelTasks).

And I want to do a bunch of diagnostic debugging on it. I want to perf profile it, and step through a bunch of it manually to see what's what.

All my tools are designed around synchronous C# code. They will sort of work with async, but it's definitely much less effective, and debugging is way harder, even when I try to directly manage the threads a bit.

If I'm only interested in a small portion of the code, then it's definitely a LOT easier to temporarily un-async that portion of the code. Read and Write synchronously, and just Task.Wait() on each of my "parallel" Tasks in sequence. But that's not viable for to do if I want to poke around in a large swathe of the code.

Is there anyway to ask C# to run some "async" code like that for me?

i.e. some sort of (() => MyAsyncMethod()).RunAsThoughAsyncDidntExist() which knows that any time it does real async communication with the outside world, it should just spin (within the same thread) until it gets an answer. Any time it's asked to run code in parallel ... don't; just run them in series on its single thread. etc. etc.

I'm NOT talking about just awaiting for the Task to finish, or calling Task.Wait(). Those won't change how that Task executes itself


I strongly assume that this sort of thing doesn't exist, and I just have to live with my tools not being well architected for async code.

But it would be great if someone with some expertise in the area, could confirm that.


EDIT: (Because SO told me to explain why the suggestion isn't an answer)... Sinatr suggested this: How do I create a custom SynchronizationContext so that all continuations can be processed by my own single-threaded event loop? but (as I understand it) that is going to ensure that each time there's an await command then the code after that await continues on the same thread. But I want the actual contents of the await to be on the same thread.
Brondahl
  • 7,402
  • 5
  • 45
  • 74
  • 1
    Here's hoping Stephen Cleary turns up :D – Brondahl Jun 02 '20 at 13:33
  • This sounds exactly like the use case of `Task.Wait`. Can you explain why `Task.Wait` does not work for you? What do you mean by "it won't change how that task executes itself"? – Sweeper Jun 02 '20 at 13:36
  • @Sweeper He means that `Task.Wait` wont force the task to be executed on the calling thread. It just blocks which ever thread calls `Wait` but that doesn effect how the task is executed. Of couse unless it is executed on the same thread which would result in a deadlock. – Ackdari Jun 02 '20 at 13:39
  • Sometime ago I read something about how one can changed how the compiler compiles or _"links"_ async code by defining some classes in your project. But I don't have any details right now. – Ackdari Jun 02 '20 at 13:40
  • 1
    Does this answer your question? [How do I create a custom SynchronizationContext so that all continuations can be processed by my own single-threaded event loop?](https://stackoverflow.com/questions/39271492/how-do-i-create-a-custom-synchronizationcontext-so-that-all-continuations-can-be) – Sinatr Jun 02 '20 at 13:40
  • @Ackdari That sounds like exactly what I was imagining! If you are able to find those details (or suggest any more specific search KeyWords) then that'd be great! – Brondahl Jun 02 '20 at 13:52
  • @Sweeper. Yes, Ackdari has correctly understood what I meant. – Brondahl Jun 02 '20 at 13:53
  • 1
    @Sinatr I don't think that's quite it - that determines how it *resumes* after the Task has run, but I don't *think* that it determines where that task itself runs? I'm not sure :( It would certainly need to be a *part* of the whole thing? – Brondahl Jun 02 '20 at 13:58
  • 2
    @Brondahl _"Here's hoping Stephen Cleary turns up"_ - He doesn't just "turn up". He must be summoned. :D – Fildor Jun 02 '20 at 14:14
  • BTW: A little dated, but he actually wrote something about this: https://blog.stephencleary.com/2013/05/announcement-async-diagnostics.html – Fildor Jun 02 '20 at 14:20
  • I've got a down vote and 2 close votes. Would anyone like to explain why (unless they were close-as-dupe for @Sinatr's suggestion, which I already replied to, 18 minutes after it was posted) – Brondahl Jun 03 '20 at 15:11
  • @Sinatr This will force continuations to run sequentially, but you can't do anything about `Task.Run` I think. I still think this will use threadpool or similar. – Lasse V. Karlsen Jun 03 '20 at 15:13
  • @Brondahl, are you asking [this question](https://stackoverflow.com/q/5095183/1997232)? It doesn't make sense to me to await something what is about to run using current thread.. so just run it synchronously. – Sinatr Jun 03 '20 at 15:18
  • @Sinatr, not really sure how to explain more so than I already have? I've already explained the context, the reason, and the details of what impact the thing I'm looking for would have. I'm looking for something that (temporarily) **changes** the nature of the `async` and `await` keywords. I think @Ackdari's comment about something that is changing the nature of the compiling/linking of the code is exactly what I want. :) – Brondahl Jun 03 '20 at 16:55
  • @Sinatr to answer your dupe suggestion directly. No, that linked question ALSO still runs the code on a new thread. I don't want that. I want everything running on a single thread. – Brondahl Jun 03 '20 at 16:55
  • @Sinatr regarding *"It doesn't make sense to me to await something what is about to run using current thread.. so just run it synchronously."* I'm looking at code that would **normally** run on a new thread (or on no thread at all, e.g. `SendHttpRequestAsync()`) and looking to **temporarily** change it for some testing so that it runs on a single thread. – Brondahl Jun 03 '20 at 16:57
  • I have been summoned, but I am too late. [This answer](https://stackoverflow.com/a/62186861/263693) pretty much says what I would say: you can get pretty far with a custom `SynchronizationContext`, but the only way to fully control `async`/`await` is by controlling the compiler. – Stephen Cleary Jun 06 '20 at 02:02
  • @StephenCleary. We're honoured to be graced by your beneficient presence ;) The more I try to explain what I'm imagining (and the more people suggest what *can* be done), the more I agree that I'm imagining a compiler-level modification of the code - in the same way that the compiler takes `IEnumerable` generators and turns them into code that looks nothing like the source code. Ackdari made noises about having recollections of such a thing, in the early question comments. I assume that you've not come across that? (I imagine you'd already have posted it as an answer if you had :) ) – Brondahl Jun 06 '20 at 07:40
  • @Ackdari any chance that you found / could find that reference? I've done some more directed searching with the obvious keywords from your idea, but no dice :( – Brondahl Jun 06 '20 at 07:41
  • I don't know the Roslyn specifics of how that would work. I can say that the `async` transformation is similar to (and I believe started as a copy/paste of) the `IEnumerable` transformation. But I'm not familiar enough with Roslyn to know what extension points are available. – Stephen Cleary Jun 06 '20 at 14:25

1 Answers1

1

Keep in mind that asynchronous != parallel.

  • Parallel means running two or more pieces of code at the same time, which can only be done with multithreading. It's about how code runs.
  • Asynchronous code frees the current thread to do other things while it is waiting for something else. It is about how code waits.

Asynchronous code with a synchronization context can run on a single thread. It starts running on one thread, then fires off an I/O request (like an HTTP request), and while it waits there is no thread. Then the continuation (because there is a synchronization context) can happen on the same thread depending on what the synchronization context requires, like in a UI application where the continuation happens on the UI thread.

When there is no synchronization context, then the continuation can be run on any ThreadPool thread (but might still happen on the same thread).

So if your goal is to make it initially run and then resume all on the same thread, then the answer you were already referred to is indeed the best way to do it, because it's that synchronization context that decides how the continuation is executed.

However, that won't help you if there are any calls to Task.Run, because the entire purpose of that method is to start a new thread (and give you an asynchronous way to wait for that thread to finish).

It also may not help if the code uses .ConfigureAwait(false) in any of the await calls, since that explicitly means "I don't need to resume on the synchronization context", so it may still run on a ThreadPool thread. I don't know if Stephen's solution does anything for that.

But if you really want it to "RunAsThoughAsyncDidntExist" and lock the current thread while it waits, then that's not possible. Take this code for example:

var myTask = DoSomethingAsync();
DoSomethingElse();
var results = await myTask;

This code starts an I/O request, then does something else while waiting for that request to finish, then finishes waiting and processes the results after. The only way to make that behave synchronously is to refactor it, since synchronous code isn't capable of doing other work while waiting. A decision would have to be made whether to do the I/O request before or after DoSomethingElse().

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • Thank you for this. This is more-or-less what I expected to hear - that there aren't things to do this, because it's so inimmicable to the concept of having written async and parallel code. – Brondahl Jun 06 '20 at 07:34
  • FWIW, regarding your last point, what I meant is for the "code changer/alternative compiler" to remove both async *and* parallelisation. i.e. when it sees `Task.Run()` it doesn't spin it up on a new `Task` - it just "unwraps" the `Task` and executes the action directly, still on that same thread. So in your code example, it would fully execute `DoSomethingAsync()` before stepping to the next line (... and not because it `.Wait()`ed for the task to finish ... but because it had entirely removed the Task in the first place. – Brondahl Jun 06 '20 at 07:37