1

I have a static method, which can be called from anywhere. During execution it will encounter Invoke. Obviously when this method is called from UI thread it will deadlock.

Here is a repro:

public static string Test(string text)
{
    return Task.Run(() =>
    {
        App.Current.Dispatcher.Invoke(() => { } );
        return text + text;
    }).Result;
}
void Button_Click(object sender, RoutedEventArgs e) => Test();

I've read multiple questions and like 10 answers of @StephenCleary (even some blogs linked from those), yet I fail to understand how to achieve following:

  • have a static method, which is easy to call and obtain result from anywhere (e.g. UI event handlers, tasks);
  • this method should block the caller and after it the caller code should continue run in the same context;
  • this method shouldn't freeze UI.

The closest analogy to what Test() should behave like is MessageBox.Show().

Is it achieve-able?

P.S.: to keep question short I am not attaching my various async/await attempts as well as one working for UI calls, but terrible looking using DoEvents one.

Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • Does it really cause a deadlock? Or just block for a while? I don't see a mutex or any sort of lock that would cause a deadlock in your code. – Arian Motamedi May 23 '17 at 15:34
  • What is your `App` type? – Svek May 23 '17 at 15:35
  • 5
    @PoweredByOrange Accessing the `.Result` property is the blocking call. – Ivan Stoev May 23 '17 at 15:36
  • 3
    Remove the `.Result` and make the return type `Task` then your calling code can `await` the result. That will cause the calling code to pause and then continue after the result is obtained. Does that meet the reqs? – JSteward May 23 '17 at 15:39
  • 1
    Your code will block UI if called inside the UI Thread. You have two options if you want to free the UI thread: `async` event handlers or `BackgroundWorker`. – Federico Dipuma May 23 '17 at 15:41
  • 1
    @FedericoDipuma BGW is obsolete and event handlers aren't needed at all. Progress reporting is performed using the `IProgress< T>` class in .NET 4.5+. Or just break up the method in UI and truly async parts and use `await` to await the execution of the async parts – Panagiotis Kanavos May 23 '17 at 16:02
  • 1
    @Sinatr what are you trying to do? Why are you trying to modify the UI from inside a task? Why not use an async event handler? – Panagiotis Kanavos May 23 '17 at 16:04
  • @PanagiotisKanavos I was referring to event handlers like the `Button_Click` method mentioned by the OP (which should be `async` if he do not want to block UI when he `await`s a task inside it). Regarding BGW I agree, i didn't know about `IProgress`. – Federico Dipuma May 23 '17 at 16:10
  • @PanagiotisKanavos, see my answer, perhaps it will better describe the problem. I don't like the way I went, this why I tried to ask the question in a "broad" way. I am trying to mimic `MessageBox` behavior (method which will be called from various places of the software to display something, not another window though). This method will modify UI (therefore it will use Invoke). Async event handler are not the only callers and I do not like the idea of moving boiler code to the caller (there will be many). To summarize:static synchronous method to call, block threads, do not freeze UI.Possible? – Sinatr May 24 '17 at 07:41
  • @Sinatr actually, no. None of this is useful. It looks like you misunderstand what `Task.Run` and `await` do. You haven't posted anything that actually needs to run in the background. If you do have something that does, just use `await Task.Run(whatever)` and update the UI after that. – Panagiotis Kanavos May 24 '17 at 08:03

2 Answers2

1

You can not.

Even just 2 of those 3 requirements can't be achieved together - "this method should block the caller" is in conflict with "this method shouldn't freeze UI".

You have to make this method either asynchronous in some way (await, callback) or make it executable in small chunks to block UI only for short periods of time using for example timer to schedule each step.

Just to reiterate what you already know - you can't block thread and call it back at the same time as discusses in many questions like - await works but calling task.Result hangs/deadlocks.

Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
  • Thanks for the answer, though just saying "not possible" is not what I expect to find this morning as an answer ;) I mention `MessageBox` for a reason, perhaps my requirements aren't clearly described? I want to call `Test()` from either UI thread or some `Task` and it should *block* the caller, while not freezing UI. I'll post my crappy (but surprisingly working) attempt as an answer, please have a look. – Sinatr May 24 '17 at 07:26
  • @Sinatr Writing message pump that works correctly for all types of message and at the same time does no cause unexpected re-entrancy problems is very hard. I would not do it myself and would not recommend to anyone. At very least read warnings in https://msdn.microsoft.com/en-us/library/system.windows.forms.application.doevents(v=vs.110).aspx and https://blogs.msdn.microsoft.com/oldnewthing/20050428-00/?p=35753 carefully. – Alexei Levenkov Jun 04 '17 at 18:49
  • I know about problems of `DoEvents()` and it was interesting to know about alternatives. Currently I am trying to avoid this specific case by not having to `Invoke` anything. Despite the question is poor (it's downvoted and it lacks important details - `Task` is returning something what require `Invoke`), still I can't accept "No" as an answer, sorry ;) I'd rather mark my own answer as one (though I wouldn't as it's poorly tested). – Sinatr Jun 06 '17 at 07:51
0

To achieve something what MessageBox does (but without creating window) one can do something like this:

public class Data
{
    public object Lock { get; } = new object();
    public bool IsFinished { get; set; }
}

public static bool Test(string text)
{
    var data = new Data();
    Task.Run(() =>
    {
        Thread.Sleep(1000); // simulate work
        App.Current.Dispatcher.Invoke(() => { });
        lock (data.Lock)
        {
            data.IsFinished = true;
            Monitor.Pulse(data.Lock); // wake up
        }
    });
    if (App.Current.Dispatcher.CheckAccess())
        while (!data.IsFinished)
            DoEvents();
    else
        lock (data.Lock)
            Monitor.Wait(data.Lock);
    return false;
}

static void DoEvents() // for wpf
{
    var frame = new DispatcherFrame();
    Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new Func<object, object>(o =>
    {
        ((DispatcherFrame)o).Continue = false;
        return null;
    }), frame);
    Dispatcher.PushFrame(frame);
}

The idea is simple: check if current thread need invoke (UI thread) and then either run DoEvents loop or block thread.

Test() can be called from UI thread or from another task.

It works (not fully tested though), but it's crappy. I hope this will make my requirements clear and I still need the answer to my question if there is any better "no, you can't do this" ;)

Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • What is the point of all this code? With `async/await` you need none of this. The real problem is that you still haven't shown *anything* running in the background that needs reporting. What you've written here starts a task only to block it and try to call back to the UI thread. Well, if you don't have anything else to do inside the task, don't do that and stay in the UI thread. – Panagiotis Kanavos May 24 '17 at 08:01
  • @PanagiotisKanavos, thanks, now I see what I miss. Notice returning value? It will not be available immediately (I simply return `false` in this snippet, without even passing it from the task). The code now blocks the caller (UI or non-UI thread) until result become available and then the caller should receive result and continue. Could it be achieved differently? It's something what `TaskCompletionSource` is doing when you call `SetResult` and someone is awaiting for its `Task`. But in scenario where caller is not `async` (caller call some static method synchronously). – Sinatr May 24 '17 at 08:32