Laden...

Async Methode gegen eigenen Dispatcher laufen lassen

Erstellt von Palladin007 vor 6 Jahren Letzter Beitrag vor 6 Jahren 2.368 Views
Palladin007 Themenstarter:in
2.080 Beiträge seit 2012
vor 6 Jahren
Async Methode gegen eigenen Dispatcher laufen lassen

Moin,

ich hab mir einen eigenen Dispatcher geschrieben.
Warum? Weil ich außerhalb einer UI-Anwendung eine Message-Loop brauche und weil's interessant ist.

Nun hätte ich gerne, dass ich eine async-Methode aufrufen kann und alle darin enthaltenen Aufrufe auf diesem Dispatcher ausgeführt werden.

Ich hab z.B. in WPF folgendes getestet:

private void Button_Click(object sender, RoutedEventArgs e)
{
    DoSomething((Button)sender);
}
private async void DoSomething(Button button)
{
    for (int i = 0; i < 100; i++)
    {
        await Task.Delay(100);
        await Task.Yield();

        button.Content = i + " | " + Environment.CurrentManagedThreadId + " | " + SynchronizationContext.Current?.GetType()?.Name;
    }
}

Wenn ich die Methode durch das Click-Event vom Button aufrufe, bekomme ich immer die selbe ThreadId und den selben SyncContext angezeigt.
Soweit alles klar

Wenn ich Vergleichbares aber in einer Console-App mache und den Code einfach in die Console schreibe anstatt auf den Button, dann wird der SyncronizationContext scheinbar nie genutzt.

In einer Console-App gibt's ja keinen SyncContext, also hab ich meinen Eigenen, der meinen Dispatcher bekommt und jeden Aufruf weiter reicht.

So wie ich es verstanden habe, muss ich dann, um das Gleiche zu erreichen, den Current SyncContext setzen und bei jedem await wird der folgende Task gegen den aktuellen SyncContext "gepostet" - wenn er vorhanden ist.
Bloß passiert das nicht

Mein SyncContext:

public class MyDispatcherSynchronizationContext : SynchronizationContext
{
    private readonly MyDispatcher _dispatcher;

    public MyDispatcherSynchronizationContext(MyDispatcher dispatcher)
    {
        _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
    }

    public override void Post(SendOrPostCallback callback, object state)
    {
        Debug.WriteLine("Post");
        _dispatcher.BeginInvoke(() => callback(state));
    }
    public override void Send(SendOrPostCallback callback, object state)
    {
        Debug.WriteLine("Send");
        _dispatcher.Invoke(() => callback(state));
    }

    public override SynchronizationContext CreateCopy()
    {
        return new MyDispatcherSynchronizationContext(_dispatcher);
    }
}

Und der Test-Code dazu:

static void Main(string[] args)
{
    using (var dispatcher = new MyDispatcher())
    {                
        SynchronizationContext.SetSynchronizationContext(new MyDispatcherSynchronizationContext(dispatcher));

        DoSomething();

        dispatcher.ShutDown();
    }

    Console.ReadKey();
}
private static async void DoSomething()
{
    for (int i = 0; i < 100; i++)
    {
        await Task.Delay(100);
        await Task.Yield();

        var text = i + " | " + Environment.CurrentManagedThreadId + " | " + SynchronizationContext.Current?.GetType()?.Name;

        Console.WriteLine(text);
    }
}

Wenn ich mir die Debug-Ausgabe dazu anschaue, bekomme ich genau einmal Post rein geschrieben.

Hat jemand eine Idee, was ich falsch mache?
Oder was muss ich machen, um dieses Verhalten wie bei WPF, bloß gegen meinen eigenen Dispatcher, zu erzwingen?

Beste Grüße

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

D
985 Beiträge seit 2014
vor 6 Jahren

Da gibt es was von Rati*pharm Microsoft: Await, SynchronizationContext, and Console Apps

Palladin007 Themenstarter:in
2.080 Beiträge seit 2012
vor 6 Jahren

Den Artikel kannte ich, auf dessen Basis hab ich gearbeitet.
Mein Fehler lag ganz woanders

Ich hab in dem oben gezeigten Code alles richtig gemacht.
Das Problem war, dass mein Dispatcher die async Methode ja in einem anderen Thread ausführt.
In diesem Thread war der vorherige SyncContext aber nicht mehr bekannt.

Den ziehe ich jetzt entsprechend mit, dann klappts auch.

Wie sagt man so schön: Kaum macht man es richtig, schon funktioniert es. 😛

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Palladin007 Themenstarter:in
2.080 Beiträge seit 2012
vor 6 Jahren

'n Abend,

ich kram das hier nochmal hervor.

Mein Plan funktioniert, aber ich hab ein Performance-Problem, das ich auch identifizieren konnte.

Folgender Test-Code:

for (int i = 0; i < 1000; i++)
{
    Debug.WriteLine("for-loop: " + SynchronizationContext.Current?.GetType()?.Name ?? "<null>");
    await Task.Delay(1);
}

Folgender Synchronization-Context:

public class MyDispatcherSynchronizationContext : SynchronizationContext
{
    public override void Post(SendOrPostCallback callback, object state)
    {
        Debug.WriteLine("syncContext: " + Current?.GetType()?.Name ?? "<null>");
        SetSynchronizationContext(this);
        _dispatcher.InvokeAsync<object>(() => { callback(state); return null; }, CancellationToken.None);
    }
}

Da steht noch mehr drin (z.B. ctor), aber es geht auch nur um die Post-Methode.
Warum das SetSynchronizationContext(this) drin steht, kommt noch.

Die beiden Debug.WriteLine()-Methoden produzieren folgenden Output:

for-loop: MyDispatcherSynchronizationContext
syncContext:
for-loop: MyDispatcherSynchronizationContext
syncContext:
for-loop: MyDispatcherSynchronizationContext
syncContext:
for-loop: MyDispatcherSynchronizationContext
syncContext:

Und so weiter ...

Beim await auf Task.Delay() geht immer der SynchronizationContext "verloren".
Deshalb auch das SetSynchronizationContext(this), nur so funktioniert es, leider kostet das immer rund 10ms

Mein Dispatcher hat damit also nichts zu tun, der ist da noch gar nicht im Spiel.
Wenn ich Task.Delay(1).ConfigureAwait(true) schreibe, ändert sich nichts.
Wenn ich Task.Yield() schreibe, bekomme ich die Ausgabe, die ich erwarte:

for-loop: MyDispatcherSynchronizationContext
syncContext: MyDispatcherSynchronizationContext
for-loop: MyDispatcherSynchronizationContext
syncContext: MyDispatcherSynchronizationContext
for-loop: MyDispatcherSynchronizationContext
syncContext: MyDispatcherSynchronizationContext
for-loop: MyDispatcherSynchronizationContext
syncContext: MyDispatcherSynchronizationContext

Hat jemand eine Idee, wie ich das Verhalten korrigieren kann?
... oder warum das so ist?

Es kann auch sein, dass das mein ursprüngliches Problem, weshalb ich dieses Thema aufgemacht habe.
Ich dachte, ich hätte das gelöst, scheinbar hab ich aber nur Symptome überdeckt anstatt das Problem zu beheben.
Deshalb schreib ich in diesem Thema

PS:

Oder kann mir jemand erklären, was Task.Yield macht?
Es muss ja irgendetwas anders machen, vielleicht hilft es mir, meinen Fehler zu finden

PPS:

Kann es sein, dass der Inhalt von Task.Delay auch an meinen SynchronizationContext übergeben wird?
Das würde das Problem erklären.

Kann ich das irgendwie verhindern?
Mein Dispatcher bedeutet einen gewissen Overhead, das ist klar.
Es wäre super, wenn solche .NET-internen Aufrufe nicht auf meinem Dispatcher laufen würden.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

B
66 Beiträge seit 2013
vor 6 Jahren

**Auch Du mein Sohn ... **solltest Dir das Null Object Pattern anschauen:
Nullobjekt (Entwurfsmuster)

Du bildest für jeden Call einfach ein Null Object, dann hast Du mit den Dispatcher keine Probleme mehr bzw. die Fehler sollten leichter zu finden sein.

Grüße.

K
166 Beiträge seit 2008
vor 6 Jahren

PS:

Oder kann mir jemand erklären, was Task.Yield macht?
Es muss ja irgendetwas anders machen, vielleicht hilft es mir, meinen Fehler zu finden

await Task.Yield() will force your method to be asynchronous [...]

Source: Task Yield - Why use it?

Btw - da steht auch

Internally, await Task.Yield() simply queues the continuation on either the current synchronization context or on a random pool thread, if SynchronizationContext.Current is null.

Kann es daher vllt sein, das durch eine eventuelle Synchronizität deiner Threads der "falsche" Dispatcher genutzt wird, während du deinen mitgibst?

Falls ich falsch liege, korregiert mich bitte.

VG Killerkruemel

**Auch Du mein Sohn ... **solltest Dir das Null Object Pattern anschauen:

>

[...]

PS.: Das mit dem Nullobjekt verstehe ich nicht ansatzweise... Und wenn ich mir die Diskussionsseite auf Wikipedia anschaue, auch sonst kaum einer...

16.841 Beiträge seit 2008
vor 6 Jahren

Das mit dem Nullobjekt verstehe ich nicht ansatzweise

Blasters Einwurf würde ich - wenn man seine anderen Posts in diesem Zeitraum anschaut - nicht ganz so thementreu sehen.
Er wirft aktuell irgendwelche Dinge in die Themen, die nicht immer was mit dem Thema zutun haben - und löst auch nicht auf, was er damit sagen wollte; leider.