Laden...

Forenbeiträge von Palladin007 Ingesamt 2.079 Beiträge

24.04.2022 - 15:23 Uhr

was noch fehlt sind null-Checks fürs übergebene Ticket.

Stimmt - in einem nullable Projekt vergesse ich die gerne Mal ^^

Das ist aber nicht mehr sehr deterministisch und kann schnell zu unkontrolliertem Verhalten führen.

Welches unkontrollierte Verhalten meinst Du?
Wenn kein Task mehr eine Referenz auf das Ticket hat, gibt es doch auch keinen Task mehr, der etwas damit tun wollen könnte, oder?

Aber die Idee, das konfigurierbar zu machen, gefällt mir.
Microsoft verwendet ja auch immer häufiger Options-Typen dafür, daher übernehme ich das auch so

Das Erstellen der CancellationTokenSource erzeugt eine Menge (Maschinencode), daher wird EnterAsync eine recht große Methode, welche dann ihrerseits nicht mehr inlined wird bzw. werden soll, da einfach zu groß.

Also ich wusste ja, dass die StateMachine Zeit kostet, weshalb ich in den Überladungen auch kein await genutzt habe.
Aber auf so einen Zusammenhang wäre ich im Leben nicht gekommen 😁
Aber woher weiß ich, was viel Maschinencode erzeugt? Muss ich das selber herausfinden (gibt ja Tools, die mir zeigen, was der JIT erzeugt), oder gibt es dazu Quellen?

Ich sprach auch nicht vom "AsyncLock", sondern vom "async lock" (also den selbst erstellten).

Achso - ja, aber "async lock" gibt es ja nicht, alle Alternativen, die ich bisher gesehen habe, basieren auf einem AsyncSemaphore und das erlaubt keine mehrfachen Enter.
Aber ja, im Grunde verhält sich diese Klasse dann wie ein "richtiges" async lock - wobei aber auch mehrere parallel laufende Tasks den gleichen ExecutionContext haben können, wenn sie vom selben Task gestartet wurden, habe ich festgestellt, das wäre dann wieder ein Problem.

Die neue Version:

(Und jetzt ist der Code auch zu lang für eine Nachricht)

Es gibt jetzt die drei State-Klassen, wie Du vorgeschlagen hast.
Die Option ist in einem struct (weder readonly noch record), da es ja eigentlich nur ein bool halten und für die Zukunft Platz für weitere Flags bieten soll.
Null-Checks sorgen nur im EnterAsync für eine Exception, da die Anderen alle bool zurück geben, und ich dann erwarten würde, dass die keine Exception werfen, geben die bei ticket=null auch false zurück.

22.04.2022 - 17:08 Uhr

Neue Version* Timeout-Überladung der EnterAsync-Methode

  • Das Ticket wird jetzt als WeakReference gespeichert, wenn die Referenz verloren ist, gilt der State automatisch als freigegeben
  • Ein paar vars entfernt, wo es nicht direkt offensichtlich war. Bei Variablen mit "has" finde ich es aber ziemlich offensichtlich

public sealed class AsyncTicketLock
{
    private readonly Channel<State> _channel = Channel.CreateBounded<State>(1);
    private readonly State _state = new();

    public ValueTask<Releaser> EnterAsync(object ticket, int timeout, CancellationToken cancellationToken = default)
    {
        if (timeout == 0)
            return EnterAsync(ticket, cancellationToken);

        using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

        timeoutCts.CancelAfter(timeout);

        return EnterAsync(ticket, timeoutCts.Token);
    }

    public ValueTask<Releaser> EnterAsync(object ticket, TimeSpan timeout, CancellationToken cancellationToken = default)
    {
        if (timeout == TimeSpan.Zero)
            return EnterAsync(ticket, cancellationToken);

        using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

        timeoutCts.CancelAfter(timeout);

        return EnterAsync(ticket, timeoutCts.Token);
    }

    public async ValueTask<Releaser> EnterAsync(object ticket, CancellationToken cancellationToken = default)
    {
        while (true)
        {
            if (TryEnter(ticket, out Releaser releaser))
                return releaser;

            await _channel.Writer
                .WaitToWriteAsync(cancellationToken)
                .ConfigureAwait(false);
        }
    }

    public bool TryEnter(object ticket, out Releaser releaser)
    {
        var success = TryEnter(ticket);

        releaser = success
            ? new Releaser(this, ticket)
            : default;

        return success;
    }

    public bool TryEnter(object ticket)
    {
        lock (_state)
        {
            if (_channel.Reader.TryPeek(out State? state) && state.TryGetTicket(out object? stateTicket))
            {
                if (ReferenceEquals(ticket, stateTicket))
                {
                    state.IncrementEnteredCount();

                    return true;
                }

                return false;
            }
            else
            {
                var hasWritten = _channel.Writer.TryWrite(_state);

                Debug.Assert(hasWritten);

                _state.Reset(ticket);

                return true;
            }
        }
    }

    public bool Release(object ticket)
        => Release(ticket, all: false);

    public bool ReleaseAll(object ticket)
        => Release(ticket, all: true);

    private bool Release(object ticket, bool all)
    {
        if (!_channel.Reader.TryPeek(out _))
            return false;

        lock (_state)
        {
            if (!_channel.Reader.TryPeek(out State? state))
                return false;

            if (!state.TryGetTicket(out object? stateTicket))
                return false;

            if (!ReferenceEquals(ticket, stateTicket))
                return false;

            var count = state.DecrementEnteredCount();

            Debug.Assert(count >= 0);

            if (all || count <= 0)
            {
                var hasFreedChannel = _channel.Reader.TryRead(out State? readState);

                Debug.Assert(hasFreedChannel);

                if (readState is not null)
                {
                    readState.TryGetTicket(out object? readStateTicket);

                    Debug.Assert(ReferenceEquals(readStateTicket, stateTicket));
                }
            }

            return true;
        }
    }

    public readonly struct Releaser : IDisposable
    {
        private readonly AsyncTicketLock _parent;
        private readonly object _ticket;

        internal Releaser(AsyncTicketLock parent, object ticket)
        {
            _parent = parent;
            _ticket = ticket;
        }

        public void Dispose()
            => _parent?.Release(_ticket);
    }

    [DebuggerDisplay($"EnteredCount: {{{nameof(_enteredCount)},nq}}")]
    private sealed class State
    {
        private WeakReference<object>? _ticketRef;
        private int _enteredCount;

        public bool TryGetTicket([NotNullWhen(true)] out object? ticket)
        {
            if (_ticketRef is null)
            {
                ticket = null;
                return false;
            }

            return _ticketRef.TryGetTarget(out ticket);
        }

        public void Reset(object ticket)
        {
            _enteredCount = 1;

            _ticketRef = new WeakReference<object>(ticket);
        }

        public int IncrementEnteredCount() => ++_enteredCount;
        public int DecrementEnteredCount() => --_enteredCount;
    }
}

Das wäre dann ein normaler async lock?
Wie da jetzt das AsyncLocal<object> ins Spiel kommt erschließt sich mir nicht.

Ein normales AsyncLock erlaubt meine ich kein mehrfaches Enter, oder irre ich mich?
Ich meine sowas:


public sealed class LocalAsyncTicketLock
{
    private readonly AsyncLocal<object> _localTicket = new();
    private readonly AsyncTicketLock _ticketLock = new();

    private object Ticket
    {
        get => _localTicket.Value ??= new();
    }

    public ValueTask<AsyncTicketLock.Releaser> EnterAsync(int timeout, CancellationToken cancellationToken = default)
        => _ticketLock.EnterAsync(Ticket, timeout, cancellationToken);

    public ValueTask<AsyncTicketLock.Releaser> EnterAsync(TimeSpan timeout, CancellationToken cancellationToken = default)
        => _ticketLock.EnterAsync(Ticket, timeout, cancellationToken);

    public ValueTask<AsyncTicketLock.Releaser> EnterAsync(CancellationToken cancellationToken = default)
        => _ticketLock.EnterAsync(Ticket, cancellationToken);

    public bool TryEnter(out AsyncTicketLock.Releaser releaser)
        => _ticketLock.TryEnter(Ticket, out releaser);

    public bool TryEnter()
        => _ticketLock.TryEnter(Ticket);

    public bool Release()
        => _ticketLock.Release(Ticket);

    public bool ReleaseAll()
        => _ticketLock.ReleaseAll(Ticket);
}

22.04.2022 - 13:32 Uhr

Ja, das Thema var, das gibt's ja häufiger, war klar, dass Du das schreibst, bei dir war kein einziges var 😁
Für mich ist das Argument, dass die Bedeutung nicht offensichtlich ist, eher ein Grund dafür, die Benennungen anzupassen, die ist mMn. wichtiger.
Außerdem sorgt das var dafür, dass eine Änderung des Typs nicht automatisch überall für Compile-Fehler führt - wobei das aber auch als Nachteil sehen könnte, je nach Standpunkt.

V.a. wenn viel Code gelesen wird (und teilweise auf dem Handy) ist das ungemein praktisch.

Auf dem Handy habe ich noch nie Code gelesen - aber die Frage "Wer tut sowas?" kann ich mir vermutlich sparen 😁

Vllt. sollte noch ein Überladung mit Timeout, wie im ganz ursprünglichen Code von dir, mit rein.

Das könnte per verlinkter CancellationTokenSource durchgeführt werden

Das habe ich mir auch überlegt und weg gelassen, da der Aufrufer das ja auch selber machen kann, ohne die Komplexität der Klasse kennen zu müssen.
Aber schaden würde es auch nicht und Du hast schon recht, es macht die Klasse irgendwie "vollständiger", auch mit dem Hintergrund, es veröffentlichen zu wollen.
Ich baue es noch mit ein.

Statt TicketLock tendiere ich zu TicketMonitor, da Monitor mehr mit Überwachen zu tun hat als mit Verschließen.

Allerdings ist die Klasse ja nicht zum Überwachen da, sie sperrt nur den Zugang, oder gibt ihn wieder frei.
Die Monitor-Klasse liefert ja auch noch mehr, was einem regelrechten Ballspiel gleicht, das ist es hier ja nicht.

Ich hätte auch noch eine andere Idee:

Eine zweite Klasse die die gleichen Methoden anbietet, nur ohne Ticket.
Sie arbeitet dann intern mit AsyncLocal<object> und synchronisiert auf diese Weise automatisch.
Das erspart die Arbeit, wenn eine Instanz von mehreren Tasks genutzt wird, selber ein AsyncLocal zu verwalten.
Ich müsste nur recherchieren, was mit den Referenzen passiert, wenn die Tasks beendet sind, wäre doof, wenn ich dadurch ein MemoryLeak einbaue

22.04.2022 - 10:48 Uhr

Dann lasse ich es drin, tut ja nicht weh und der Channel sollte denke ich performant genug sein, dass das zweite Mal nicht auffällt.

Zum Namen: TicketLock? 😁
Es verhält sich wie ein Lock (bzw. Monitor), nur nicht anhand des Threads, sondern anhand eines Ticket-Objektes

Hier der aktuelle Code.
Ich habe mir erlaubt, ihn an meinen Stil anzupassen und ein paar Funktionen zu ergänzen - ich hoffe, Du nimmst mir das nicht übel 🙂


public sealed class AsyncTicketLock
{
    private readonly Channel<State> _channel = Channel.CreateBounded<State>(1);
    private readonly State _state = new();

    public async ValueTask<Releaser> EnterAsync(object ticket, CancellationToken cancellationToken = default)
    {
        while (true)
        {
            if (TryEnter(ticket, out var releaser))
                return releaser;

            await _channel.Writer
                .WaitToWriteAsync(cancellationToken)
                .ConfigureAwait(false);
        }
    }

    // Eine TryEnter-Methode brauche ich nicht, aber ich denke, dass sie für Andere nützlich sein kann
    public bool TryEnter(object ticket, out Releaser releaser)
    {
        var success = TryEnter(ticket);

        releaser = success
            ? new(this, ticket)
            : default;

        return success;
    }

    // Auch die ist nicht notwendig, aber ich finde sie übersichtlicher, wenn ich nicht bei jedem return den out-Parameter setzen muss
    public bool TryEnter(object ticket)
    {
        lock (_state)
        {
            if (_channel.Reader.TryPeek(out State? state))
            {
                if (ReferenceEquals(ticket, state.Ticket))
                {
                    state.IncrementEnteredCount();

                    return true;
                }

                return false;
            }
            else
            {
                var written = _channel.Writer.TryWrite(_state);

                Debug.Assert(written);

                _state.Reset(ticket);

                return true;
            }
        }
    }

    // Das "Try" brauche ich auch nicht, aber ich denke, dass es für Andere nützlich sein kann
    public bool Release(object ticket)
        => Release(ticket, all: false);

    // Anderer Code kann auf diese Weise garantiert alles freigeben
    // Wenn z.B. irgendwo ein Problem auftritt, dass nicht alles Released wurde,
    // dann kann diese Methode z.B. in Dispose oder finally aufgerufen und so jeder Zugang freigegeben werden
    public bool ReleaseAll(object ticket)
        => Release(ticket, all: true);

    private bool Release(object ticket, bool all)
    {
        Debug.Assert(_channel.Reader.Count > 0);

        if (!_channel.Reader.TryPeek(out _))
            return false;

        lock (_state)
        {
            if (!_channel.Reader.TryPeek(out var state))
                return false;

            if (!ReferenceEquals(ticket, state.Ticket))
                return false;

            var count = state.DecrementEnteredCount();

            Debug.Assert(count >= 0);

            if (all || count <= 0)
            {
                var freedChannel = _channel.Reader.TryRead(out var stateRead);

                Debug.Assert(freedChannel);
                Debug.Assert(ReferenceEquals(stateRead?.Ticket, state.Ticket));
            }

            return true;
        }
    }

    public readonly struct Releaser : IDisposable
    {
        private readonly AsyncTicketLock _parent;
        private readonly object _ticket;

        internal Releaser(AsyncTicketLock parent, object ticket)
            => (_parent, _ticket) = (parent, ticket);

        public void Dispose()
            => _parent?.Release(_ticket);
    }

    [DebuggerDisplay($"EnteredCount: {{{nameof(_enteredCount)},nq}}, UserState: {{{nameof(Ticket)},nq}}")]
    private sealed class State
    {
        private int _enteredCount;
        public object? Ticket { get; private set; }

        public void Reset(object ticket)
        {
            _enteredCount = 1;

            Ticket = ticket;
        }

        public int IncrementEnteredCount() => ++_enteredCount;
        public int DecrementEnteredCount() => --_enteredCount;
    }
}

Ich bin offen gegenüber Kritik, auch zum Stil ^^

22.04.2022 - 10:18 Uhr

Hmm - stimmt eigentlich.
Klar, es wäre möglich, dass der Zähler auf 1 steht und zwei Mal freigegeben werden soll, aber das ist dann ein Synchronisationsfehler des Aufrufers.
Die Klasse soll ja nur die Aufrufe mit verschiedenen Objekten synchronisieren, alles, was das gleiche Objekt hat, darf ungehindert tun und lassen, was es will

22.04.2022 - 00:53 Uhr

Ich schaue mir gerade deinen Code an und habe eine Frage zur Release-Methode:


public void Release(object userState)
{
    Debug.Assert(_channel.Reader.Count > 0);

    // Wir schauen ob das userState-Objekt zum Objekt im Channel passt.
    // Falls ja, so dekrementieren wir den Zähler vom State im Channel.
    // Ist der Zähler 0, so wird der Channel geleert und ist somit wieder frei.
    //
    // - Ist der Channel leer, so passiert nichts.
    // - Ist es ein anderes userState-Objekt so passiert nichts.
    if (_channel.Reader.TryPeek(out State? stateInChannel))
    {
        lock (_state)
        {
            if (ReferenceEquals(userState, stateInChannel.UserState)
            && stateInChannel.DecrementEnteredCount() == 0)
            {
                bool freedChannel = _channel.Reader.TryRead(out State? stateRead);

                // Nur zur Sicherheit und da ich Debug.Assert normal gerne als "Verträge"
                // Code verwende, da sonst so gut wie keine Kommentare vorhanden sind.
                Debug.Assert(freedChannel);
                Debug.Assert(ReferenceEquals(stateRead!.UserState, stateInChannel.UserState));
            }
        }
    }
}

Ich wollte eigentlich ein Problem ansprechen, dann habe ich deine Debug.Assert-Zeilen gesehen und dass sie genau dieses Problem asserten 😁
Wäre es nicht theoretisch denkbar, dass in der Release-Methode, während sie auf das lock wartet, ein zweiter Thread den gleichen userState releasen will und danach ein dritter Thread mit EnterAsync einen neuen userState hinzufügt?
In dem Fall würde das Release vom ersten Thread den State vom dritten Thread verwerfen, was natürlich nicht sein darf.

Wäre es nicht besser, das TryPeek zwei Mal - einfach vor dem lock und einmal im lock - zu haben?
Das äußere TryPeek ist dann ein Versuch, möglichst unnötiges Warten auf das lock zu vermeiden und das innere lock ist eine Versicherung, dass es auch wirklich drin ist.
Mit deinem zweiten Debug.Assert prüfst Du ja genau diesen Fall, aber das fällt dann ja nur im Debug auf.

Mein Gedanke wäre also sowas:


public void Release(object userState)
{
    Debug.Assert(_channel.Reader.Count > 0);

    // Wir schauen ob das userState-Objekt zum Objekt im Channel passt.
    // Falls ja, so dekrementieren wir den Zähler vom State im Channel.
    // Ist der Zähler 0, so wird der Channel geleert und ist somit wieder frei.
    //
    // - Ist der Channel leer, so passiert nichts.
    // - Ist es ein anderes userState-Objekt so passiert nichts.
    if (_channel.Reader.TryPeek(out _))
    {
        lock (_state)
        {
            if (_channel.Reader.TryPeek(out State? stateInChannel))
            {
                if (ReferenceEquals(userState, stateInChannel.UserState)
                    && stateInChannel.DecrementEnteredCount() == 0)
                {
                    bool freedChannel = _channel.Reader.TryRead(out State? stateRead);

                    // Nur zur Sicherheit und da ich Debug.Assert normal gerne als "Verträge"
                    // Code verwende, da sonst so gut wie keine Kommentare vorhanden sind.
                    Debug.Assert(freedChannel);
                    Debug.Assert(ReferenceEquals(stateRead!.UserState, stateInChannel.UserState));
                }
            }
        }
    }
}

PS:
Debug.Assert habe ich bisher noch nie genutzt - keine Ahnung, warum 😁
Aber wenn ich so darüber nachdenke, gefällt mir das Konzept, auch wenn der Code damit irgendwie komisch aussieht

21.04.2022 - 22:51 Uhr

Warum nicht einfach so?


return (Action<int>)Delegate.CreateDelegate(typeof(Action<int>), myMethodInfo);

21.04.2022 - 22:19 Uhr

Nutze doch einfach die Überladung, bei der Du den Delegate-Typ mitgeben kannst.
Dann tut's einfaches Casten

Delegate.CreateDelegate Method (System)

21.04.2022 - 20:55 Uhr

Ich hab mir den Code nicht angeschaut (und deinen Code zeigst Du nicht), aber negative Werte können durchaus vorkommen.

Die Position X:0/Y:0 ist immer links oben vom Haupt-Monitor!
Wenn Du links daneben einen zweiten Monitor hast, dann hat der eine negative X-Position.
Und wenn darüber ein zweiter Monitor ist, dann hat der eine negative Y-Position.

21.04.2022 - 20:21 Uhr

Du kannst nur Position und Maße speichern.
Was willst Du da noch machen bzw. was funktioniert nicht?

Position und Maße zu ändern, funktioniert auch heute noch, habe ich vorhin erst gemacht.

21.04.2022 - 20:15 Uhr

Du kannst anhand eines Delegates auch eine Action<int> erzeugen - macht den Aufruf etwas lesbarer

21.04.2022 - 17:37 Uhr

Naja, wobei man am Ende ja trotzdem noch Reflection braucht, um die jeweiligen Implementierungen zu finden.
Oder man registriert sie alle einzeln im Code, aber das will pollito ja nicht - was ich auch nachvollziehen kann ^^

21.04.2022 - 17:08 Uhr

Also ich würde daraus einzelne Klassen machen, die dann alle ein bestimmtes Interface implementieren.
Typen suchen und instanziieren ist immer noch Reflection, aber einfacher, als Methoden suchen und aufrufen.

21.04.2022 - 15:22 Uhr

Du kannst nur den Reflection-Aufruf umgestalten, aber es bleibt immer noch Reflection.
Z.B. ist es meines Wissens nach performanter, wenn Du die Methode nicht einfach ausführst, sondern ein Delegate dafür erstellst.

Aber besser wäre natürlich, Du müsstest gar nicht auf Reflection zurückgreifen, damit sparst Du dir diese ganze Komplexität.

21.04.2022 - 14:50 Uhr

Da hast du recht, das passt und wegen der Synchronisierung gibt es da auch kein Race.

Gut zu wissen, dass ich da richtig gedacht habe ^^

Gestern hab ich auch einen Versuch unternommen mit ValueTask (IValueTaskSource) ohne Channel. Wurde aber schnell sehr komplex, so dass das wieder verworfen wurde, da eben die Channel hier schon alles bietet was benötigt wird.

Klar, wenn die Channels sowas in der Art schon machen, dann nehme ich natürlich auch diese Lösung.
Darauf zielte meine Frage hier ja ab: Eine mehr oder weniger fertige Lösung finden, damit diese Komplexität eben nicht testen muss.

Unabhängig davon frag ich mich schon ob es nicht ein passendes Konstrukt dafür schon gibt

Naja, für einen Thread kann man das selber Verhalten ja mit dem klassischen lock erreichen - zumindest in meinem Fall.
Nur für Async geht genau das nicht, mein Gedanke mit dem anderen Verhalten soll das gleiche Verhalten wie vom lock ermöglichen.
Warum es dafür noch nichts gibt, wundert mich aber auch, das Problem sollte es ja auch in anderen Projekten geben, async Synchronisierung ist ja kein neues Thema.

Aber wenn es das wirklich nicht gibt - wovon ich jetzt mal ausgehe, wenn Du und Abt nichts kennen und wir nichts finden - dann mache ich daraus ein kleines NuGet-Package.

21.04.2022 - 10:45 Uhr

Das spart die wiederholte Allokation der WeakReference und _count kann gleich auf 1 gesetzt werden.

Stimmt, danke 🙂

Da ist ein (latenter) Bug. Wenn TryEnter mit einem anderen obj aufgerufen wird als bisher in der WeakReference gehalten so wird jedesmal eine neue TaskCompletionSource erstellt. Bereits von der TaskCompletionSource (TCS) erzeugte Tasks können so nie komplettiert werden und das ist Fehlverhalten.

Müsste das nicht durch das "??=" umgangen werden?
Die TCS wird nur dann erstellt, wenn es noch keine gibt, sodass am Ende alle auf den gleichen Task warten.
Oder ich übersehe etwas, denn die AsyncSemaphore-Implementierung von Stephen T nutzt ja auch eine Queue, ich dachte nur, dass ich die nicht brauche...

21.04.2022 - 01:10 Uhr

Ok, damit hab ich nicht gerechnet 😁
Ich brauche (neben der Arbeit) noch etwas Zeit, um mich da Mal ausführlich einzuarbeiten, besonders da ich die Channel bisher auch nur vom Namen kenne.
Gerade bei so einem schwierigen Thema übernehme ich ungern Code, ohne jede Zeile erklären zu können, teils aus Interesse, teils weil ich ihn warten muss ^^

Bis dahin erst einmal ein riesiges Dankeschön 🙂

Releaser ist ein Werttype, kann also nicht null sein, daher ist diese Annotation umsonst.

Ich weiß ^^
Ich habe es aber bewusst rein geschrieben, um den Unterschied zum zweiten out-Parameter offensichtlich zu machen, also dass je nach Rückgabewert nur einer von beiden genutzt werden soll - und es tut ja auch nicht weh.
Die TryEnter-Methode ist auch erst zum Schluss entstanden, weil die Enter-Methode für meinen Geschmack zu groß wurde und sich an der Stelle gut aufsplitten ließ.
Ich hatte erst etwas ganz anderes vor, hab das dann aber für übertrieben befunden und bin zu der zwar etwas merkwürdigen, dafür aber einfachen out-Lösung gekommen.

Hab die Klasse ReEntryLock genannt, keine Ahnung ob es bessere Bezeichnungen gibt -- Namensgebung ist ziemlich das schwerste

Oh ja 😁
Vielleicht irgendetwas mit "Ticket"?
Die ganzen anderen Worte, die mir in den Sinn kommen, sind bereits irgendwie vergeben 😠

20.04.2022 - 11:34 Uhr

erstell dir eine eigene Datenstrukture welche für die Synchronisation zuständig ist.

Also gibt's das noch nicht - das habe ich befürchtet.

Bei der Variante mit den Channel<T> gibts die "Kapazitäten" vor und wenns 0 wird, so heißt es warten.

Das wäre ja das Verhalten vom Semaphore, was ich aber gerade nicht haben möchte - deshalb ist auch eine auf Semaphore aufbauende Implementierung keine Lösung für mich.
Ich brauche das umgekehrte Verhalten, sodass 0 quasi ein Freifahrtschein für alle ist und der erste, der "gewinnt", darf danach exklusiv arbeiten, bis es wieder auf 0 steht.

Die Channel schaue ich mir aber trotzdem Mal an, ich hab nur grob was dazu gelesen und bin noch nicht tiefer eingestiegen - könnte sich noch lohnen.

Was sagst Du denn zu meinem Versuch? Das verhält sich bisher ja so, wie ich es brauche, nur wäre mir eine fertige Lösung lieber, vor allem, weil sich da so leicht schwer zu entdeckende Fehler einschleichen.
Man sagt dir ja einiges an Erfahrung und Detail-Wissen nach, über etwas Feedback/Kritik würde ich mich daher freuen 🙂
Alternativ kann ich auch unter "Code-Reviews" eine neue Frage auf machen (oder einer von euch spaltet ab), wenn es keine mehr oder weniger passende vorhandene Lösung gibt, ist es da vielleicht besser aufgehoben.

20.04.2022 - 10:31 Uhr

Nein, keine State Machine.
Die Flow-Beschreibung war nur ein Versuch, es halbwegs verständlich zu beschreiben, das ist bei so Themen ja immer etwas schwerer 🙂

19.04.2022 - 22:54 Uhr

Nutze den HttpClient, der ist in einigen Punkten einfacher.

Und den Body kannst Du doch einfach als String zusammen bauen?
Beim WebRequest geht das mit OpenRequestStream und in den Stream musst Du dann noch deine Daten schreiben. Vergiss aber nicht, ContentType und ContentLength zu setzen.

Oder Du greifst auf Refit zurück, das kann dir extrem viel Arbeit abnehmen.
Wenn Du noch mehr Requests für andere Endpunkte brauchst, würde ich definitiv auf Refit setzen.

19.04.2022 - 20:19 Uhr

'n Abend,

ich habe das leidige Thema lock in einem async Kontext - was nicht geht und mir ist auch klar, warum es nicht geht.
Meine Frage zielt eher darauf ab, wie man das umgehen könnte.

Für ein generelles lock (nur einer gleichzeitig) kann man ja ein AsyncSemaphore nehmen.
Allerdings scheitert das, wenn der selbe Task EnterAsync nochmal aufruft, bevor der Releaser disposed wurde, genau das kann/darf bei mir aber vorkommen.
Meine Überlegung ist daher, dass ein Objekt als Reservierungs-ID verwendet wird und nicht (wie bei lock/Monitor) als ID für die reservierte Ressource.

Ich versuche es Mal stichpunktartig zu beschreiben:* Enter mit Obj1 ⇒ darf weiter arbeiten.

  • Enter mit Obj1 ⇒ darf weiter arbeiten.
  • Enter mit Obj2 ⇒ muss warten.
  • Exit mit Obj1
  • Obj2 muss immer noch warten.
  • Exit mit Obj1
  • Obj2 darf weiter arbeiten.
  • Exit mit Obj2
  • Enter mit Obj3 ⇒ darf weiter arbeiten.

Und so weiter, wobei aber egal ist, aus welchem Thread oder Task das aufgerufen wird, es zählt nur, welches Objekt übergeben wurde.
Oder vielleicht wird es mit etwas Code klarer:


var taskCount = 5;
var counter = new CountdownEvent(taskCount);
var myLock = new AsyncLock();

for (int i = 0; i < taskCount; i++)
{
    var id = i;

    _ = Task.Run(async () =>
    {
        object obj = id;

        try
        {
            using (await myLock.EnterAsync(obj, default))
            {
                for (int i = 0; i < 3; i++)
                {
                    using (await myLock.EnterAsync(obj, default))
                    {
                        Thread.Sleep(10);
                        Console.Write(id);
                    }
                }

                Console.WriteLine();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            throw;
        }
        finally
        {
            counter.Signal();
        }
    });
}

counter.Wait();

Die Ausgabe soll sowas sein wie:


111
444
000
222
333

Gibt es dafür schon etwas?

Mein erster Entwurf, der mit dem TestCode auch läuft:


public class AsyncLock
{
    private readonly object _syncRoot = new();
    private TaskCompletionSource? _waiter;
    private WeakReference? _obj;
    private uint _count;

    public Task<Releaser> EnterAsync(object obj, CancellationToken cancellationToken = default)
        => EnterAsync(obj, Timeout.InfiniteTimeSpan, cancellationToken);

    public async Task<Releaser> EnterAsync(object obj, TimeSpan timeout, CancellationToken cancellationToken = default)
    {
        var startTimestamp = DateTime.Now;

        while (true)
        {
            if (TryEnter(obj, out var releaser, out var waitTask))
                return releaser;

            if (timeout > Timeout.InfiniteTimeSpan)
                timeout -= DateTime.Now - startTimestamp;

            await waitTask
                .WaitAsync(timeout, cancellationToken)
                .ConfigureAwait(false);
        }
    }

    private bool TryEnter(object obj, [MaybeNullWhen(false)] out Releaser release, [MaybeNullWhen(true)] out Task waitTask)
    {
        lock (_syncRoot)
        {
            if (ValidateAccess(obj))
            {
                if (_obj?.IsAlive == false)
                    _count = 0;

                _count++;
                _obj = new(obj);

                release = new(this, obj);
                waitTask = null;
                return true;
            }
            else
            {
                _waiter ??= new();

                release = default;
                waitTask = _waiter.Task;
                return false;
            }
        }
    }

    public void Exit(object obj)
    {
        lock (_syncRoot)
        {
            if (ValidateAccess(obj))
            {
                if (_count == 1)
                {
                    _count = 0;
                    _obj = null;

                    Interlocked.Exchange(ref _waiter, null)?.TrySetResult();
                }
                else
                    _count--;
            }
        }
    }

    private bool ValidateAccess(object obj)
    {
        return _obj is null or { IsAlive: false }
            || ReferenceEquals(_obj.Target, obj);

    }

    public readonly struct Releaser : IDisposable
    {
        private readonly AsyncLock _owner;
        private readonly object _obj;

        internal Releaser(AsyncLock owner, object obj)
        {
            ArgumentNullException.ThrowIfNull(owner, nameof(owner));
            ArgumentNullException.ThrowIfNull(obj, nameof(obj));

            _owner = owner;
            _obj = obj;
        }

        public void Dispose()
            => _owner?.Exit(_obj);
    }
}

Mir wäre eine bestehende und gut getestete Lösung lieber, aber wenn es die nicht gibt:
Seht Ihr da mögliche Probleme? Ich habe nicht viel Erfahrung auf dem Gebiet
Wie könnte man das Ding nennen? 😁 AsyncLock oder AsyncMonitor finde ich nicht passend, da es sich ja eigentlich sehr anders verhält, als lock bzw. Monitor.

=========================

Die Lösung/Umsetzung des Problems: https://github.com/loop8ack/AsyncTicketLock

15.04.2022 - 14:26 Uhr

evtl. mit Hilfe von
>
o.ä.

Das gibt's auch von Microsoft:
https://github.com/Microsoft/XamlBehaviorsWpf

12.04.2022 - 17:30 Uhr

weiß ich gerade leider nicht in welches "Unterforum" meine Frage passt ich hoffe es ist hier richtig.

Ich finde deine Wahl richtig.

Dein Code dagegen aber nicht ... 😉

Ein paar generelle Kritikpunkte:* Halte bitte Namenskonventionen ein, z.B. "InsertEntry" anstatt "db_eintragen" oder "nameTextBox" anstatt "textBox1", oder "[Configurations]" anstatt "[Table]"

  • Nutze SQL-Parameter! Wo auch immer das manuelle Zusammenstückeln von SQL noch empfohlen wird - die sollte man steinigen.
  • Alles, was IDisposable (bei dir: SqlConnection und SqlCommand) implementiert, sollte in einem using-Block stehen.
  • Bist Du sicher, dass Du ohne Transaktion arbeiten willst?

Und dein Problem:
Ein simples INSERT löscht keine Daten, das Problem liegt also nicht im gezeigten Code.
Was passiert denn vorher? Wird die DB vielleicht immer überschrieben? Wird sie woanders gelöscht?
Irgendwas passiert vor dem Insert, dass deine Datenbank alle Daten verliert.

Z.B. hatte ich vor Ewigkeiten Mal bei jemanden den Fall gesehen, dass die DB-Datei (bei Sqlite) im Projekt lag und mit ins Zielverzeichnis kopiert wurde - wo sie natürlich immer und immer wieder überschrieben wurde.

By the way:
Mit dem Entity Framework nimmst Du dir den Großteil der Arbeit ab.

12.04.2022 - 13:46 Uhr

Ich persönlich würde in dem Event-Handler die Radio-Buttons (ob alle oder nur die Relevanten) abfragen und die ausgewählten Radio-Buttons auf ein Enum mappen.
Damit kann man dann andere Methoden aufrufen oder den Zustand anpassen und man muss sich nicht mehr um die Radio-Buttons kümmern.

Mit MVVM und WPF (oder andere ähnliche Frameworks) wäre das natürlich um einiges einfacher, aber es wurde ja nicht mitgeteilt, um was es sich handelt.

12.04.2022 - 13:11 Uhr

Der Hinweis mit der Auswahl ging mir darum, dass je nachdem was er ausgewählt hat auch noch das Framework beim Endanwender benötigt.
Aber ist natürlich richtig, sollte man nur noch nehmen wenn nötig uns sonst aust .NET 5+ setzen!

Das war auch nicht als Kritik an dich gemeint 🙂
Es sollte mehr ein Hinweis sein, da Viele lieber den augenscheinlich einfacheren Weg gehen (weil "kenn ich ja"), sich dabei aber nur noch mehr Probleme aufhalsen.

12.04.2022 - 11:34 Uhr

Heute gibt's .NET 5 mit quasi neu entwickelter Runtime, da hat sich eben einiges getan - zum Positiven.

Prinzipiell solltest Du aber nicht einfach den Inhalt des Debug- oder Release-Ordners aus dem bin-Verzeichnis nehmen.
Für das, was Du machen willst, gibt es die Publish-Funktion (Rechtsklick auf das Projekt), da kannst Du mehrere Publish-Profile erstellen, z.B. eines für das Datei-System. Visual Studio erstellt dann das Programm und bereitet alles in dem eingestellten Ordner vor und alles, was da drin ist, brauchst Du dann auch.

Man kann das aber noch weiter einstellen, z.B. kannst Du Runtime und Framework selber mit liefern (self contained) oder alle managed DLLs zusammenfassen (single file) und wenn Du PDBs nicht haben willst, kann man die auch deaktivieren und so weiter.

Ganz vermeiden kann man andere Dateien aber nie, daher merke dir die Grundregel:
Alles, was in deinem publish-Ordner liegt, muss auch weitergegeben werden.
Wenn Du einzelne Dateien nicht haben willst, schau, ob es Funktionen gibt, die nicht dort abzulegen.
Wenn das nicht geht, werden sie eben benötigt.

Veröffentlichen einer .NET-Konsolenanwendung mit Visual Studio - .NET

PS @T-Virus:

Die .exe allein reicht auch nicht, wenn du Abhängigkeiten zu anderen Libs (DLLs) hast.

In dem Fall ist das wahrscheinlich keine extra Abhängigkeit, sondern das Programm selber, das ja nur noch in DLLs liegt - die exe startet dann nur noch.
Daher vermutlich auch die Verwirrung - welche Abhängigkeit sollte man auch ohne Abhängigkeiten brauchen? ^^

Du kannst bei den Projekten in VS 2019 schon zwischen .NET Framework und .NET Code/5 Konsolenanwendungen auswählen.

Was man allerdings nicht tun sollte! Außer man hat Abhängigkeiten, die nur mit dem alten Framework laufen.
Wenn man das freiwillig macht, verschenkt man freiwillig viele Vorteile und Verbesserungen und kettet sich freiwillig an ein altes System.

10.04.2022 - 20:07 Uhr

Hat das Th69 gemeint?
Ok, dann haben wir aneinander vorbei geredet 😁
Ich dachte, nur auf das Event von einem RadioButton horchen.

Aber klar, eine Methode an das Event von allen RadioButtons hängen, funktioniert natürlich.

10.04.2022 - 18:39 Uhr

Weil bei drei oder mehr RadioButtons der Handler nicht bei jeder Änderung ausgeführt wird, der wird ja nur ausgeführt, wenn sich auch wirklich der Checked-Zustand ändert, weil bei drei oder mehr RadioButtons nicht immer der Fall ist. Bei drei RadioButtons bräuchte man also zwei Handler und bei vier Buttons braucht man drei Handler.

Oder wird der Handler von RB1 (in meinem Beispiel) auch ausgerufen, wenn er selber sich gar nicht ändert, sondern nur RB2 und RB3?

10.04.2022 - 16:56 Uhr

Ok, dann habe ich dich falsch verstanden, aber das Problem bleibt.

Ich versuche es zu verdeutlichen:

RB1 => true => CheckedChanged-Handler registriert
RB2 => false
RB3 => false

Klicke ich nun auf RB2:

RB1 => false => CheckedChanged-Handler wird ausgeführt
RB2 => true
RB3 => false

Klicke ich nun auf RB3:

RB1 => false => CheckedChanged-Handler wird nicht ausgeführt
RB2 => false
RB3 => true

Und mit "auf den jeweils anderen schließen" meine ich:

RB1 => true => RB2 muss false sein
RB2 => false => RB1 muss true sein

In dem Fall würde ein CheckedChanged-Handler ausreichen, denn egal welchen der beiden Buttons ich anklicke, wird dieser Handler immer ausgeführt, weil eine Änderung immer den anderen Button betrifft. Und im Handler kann ich mir dann den Checked-Status anschauen und weiß damit automatisch den Zustand des anderen Buttons.
Aber klar, man kann ja auch einfach die Referenzen der Anderen angucken - so weit hab ich irgendwie nicht gedacht 😁

10.04.2022 - 14:57 Uhr

Das funktioniert aber nur, wenn es nur zwei RadioButtons gibt.
In dem Fall könnte man auf den jeweils Anderen schließen, aber bei 3 oder mehr geht das nicht mehr.

10.04.2022 - 13:43 Uhr

Ich sähe da lieber ein FirstOrDefault + das Verhalten beim Default.
Auch wenn das beim RadioButton eigentlich nicht geht, ist der Code so etwas robuster.
Außerdem funktioniert das nur so lange, wie die Controls auch RadioButtons sind, sobald man da z.B. nochmal was drum legt, scheitert es.
Und bei so einfachen Queries würde ich die Methoden-Syntax nehmen, aber das sieht jeder anders.

Diese Art - also direkt auf die Controls zugreifen - ist aber eigentlich nur bei WinForms vernünftig.
Z.B. bei WPF und UPW gibt's MVVM und DataBinding, damit löst sich das ganze Problem aus dem Stand in Luft auf.

03.04.2022 - 20:48 Uhr

Joa - kann man drüber streiten, was besser ist. Ich finde Division intuitiver, aber naja.

03.04.2022 - 16:53 Uhr

Dann hast Du etwas anderes falsch gemacht, die Rechnung von Th69 funktioniert.


for (var i = 0; i < 100; i++)
{
    var j = 5 * ((i % 10) / 5);
    Console.WriteLine($"{i:00} => {j:00}");
}

28.03.2022 - 21:18 Uhr

Wie muss ich es schreiben, dass diese Argumente in das bereits offene Programm einfließen?

Das ist deutlich komplexer 🙂

Es gibt verschiedene Wege, der heute vermutlich einfachste Weg (weil es sehr gutes Tools gibt) wäre eine Web-API, die Du aus deinem ominösen Skript-Programm heraus aufrufst.

28.03.2022 - 14:18 Uhr

Ich verstehe das Ziel dahinter, aber das würde ich auch nicht technisch erzwingen, sondern eher "soft" im Team.

Z.B. hatte ein ehemaliger Arbeitgeber von mir dafür eine kleine Tradition.
Jeder CheckIn hat einen Build angestoßen und wenn der ohne Fehler durchlief, wurde nur der Urheber informiert, wenn nicht, wurden alle informiert.
Derjenige, der zuletzt den Build "kaputt gemacht" hat, hat dann einen Plüsch-Earny als zweifelhafte Trophäe samt feierlicher Übergabe erhalten, den er solange bei sich auf dem Platz sitzen lassen musste, bis jemand Anderes den Build "kaputt gemacht" hat.

In dem Team haben es alle mit Humor genommen und mit der Zeit haben sich alle daran gewöhnt, ihre Arbeit nochmal zu kontrollieren und auch vor Feierabend darauf zu achten.
Sollte es nicht gehen (z.B. weil wirklich etwas dazwischen kam und dann nur halb-fertiger Code da war), haben viele die ShelveSets vom TFVC genutzt - die Funktion vermisse ich auch ein bisschen bei Git, die Stashes werden ja nicht auf dem Server gespeichert.

27.03.2022 - 15:22 Uhr

Das (was Th69 vorschlägt) machen übrigens viele Programme - ist die einzige mehr oder weniger zuverlässige Möglichkeit.

Z.B. gibt's Visual Studio Extensions, die externe Programme in Visual Studio integrieren. Die Extension weiß natürlich nicht, wo das Programm liegt (besonders wenn das nicht installiert werden muss), also gibt es eine Einstellung, wo man den Pfad zur exe oder zum Programm-Ordner eingeben muss.

25.03.2022 - 18:05 Uhr

Ist ClosedXML in beide Richtungen gut und vor allem Lizenzfrei?

Schau doch nach?

Warum überhaupt Excel?

Fast alles kann CSV Dateien erzeugen/lesen, die lassen sich einfach verarbeiten und z.b. auch in Excel normal einlesen.

Häufig ist aber explizit Excel gefordert, z.B. um Formate festlegen oder Styles anpassen zu können. Oder einfach nur, damit die Datei "xlsx" heißt.
Zumindest scheint das hier kein privates Projekt zu sein, sondern etwas, wo es auch einen Abnehmer für gibt?

24.03.2022 - 22:50 Uhr

Ich habs jetzt mit Microsoft.Office.Interop.Excel gemacht.

Mein Beileid.
Lass doch als XLSX speichern und lies das, das erspart dir grob über den Daumen gepeilt 90% der Arbeit 😁

24.03.2022 - 19:42 Uhr

Ja es sind jeweils Excel Dateien.

Excel kann nervig sein zum Lesen 😠
Für XLSX empfehle ich "ClosedXml", das ist mMn. sehr intuitiv und hat mir schon ein paar Mal viel Kopfzerbrechen erspart.
Leider kann es kein Async (oder ich weiß nicht wie), dann wäre es in Ordnung, wenn Du das in einem eigenen Task kapselst, allerdings dürfte das IAsyncEnumerable (sofern Du es nachrüstest) etwas schwieriger werden. Bestimmt findet sich da eine geeignete async-fähige Queue, mit der man das richtige Verhalten erreichen kann.

Du hast mir ja mal Stichworte gegeben wie ich das mit dem "new" vermeiden könnte. einmal mit DependencyInjection, einmal mit IAsyncEnumerable oder auch mit dem ServiceLocator-Pattern. Sagt mir stand heute alles überhaupt nichts, aber es wird der Tag kommen wo das zum Thema wird. Nicht in diesem Projekt haha.

Vorsicht: Nicht vermischen!
DependencyInjection und ServiceLocator sind Ansätze um Abhängigkeiten in der Anwendung zu verteilen bzw. anders herum, wie eine Klasse an ihre Abhängigkeiten kommt. Das "new" ersetzt es nicht, es macht es nur weitläufig obsolet.
Und IAsyncEnumerable hat damit genau nichts zu tun 😉

Async sehe ich jetzt nicht ganz bei den C# Basics.

Wenn Du mit WPF, ASP.NET, etc. arbeitest, wird das aber schnell notwendig 😉

Was aber definitiv zu den Basics gehört, weil es wirklich überall drin steckt: IEnumerable.
Ich habe schon viele kennen gelernt, die nicht wirklich begriffen haben, was das ist und welche Möglichkeiten, aber auch Gefahren sich dahinter verbergen. Stichwort: Iterator-Prinzip
Das IAsyncEnumerable ist im Grunde das gleiche, nur eben asynchron.

24.03.2022 - 01:33 Uhr

Öhm das ist ein Argument was ich gerne prüfen werde. Ich hatte diesen Aufbau aus einem Video und hat mir soweit eingeleuchtet, da ich ja so in einem anderen Thread arbeite damit das Window nicht einfriert.

Ist ja auch richtig.
Beim async/await hast Du dann aber nicht einen Thread, sondern prinzipiell viele, immer dann, wenn das Programm auf irgendwas (z.B. IO) warten muss.

Du kannst natürlich auch eine Liste übergeben und die live füllen, aber dann hast Du wieder das Problem, was Du anfangs hattest.
Das zu umgehen ist mMn. aber etwas schwieriger, außerdem bin ich ein großer Fan von Methoden nach dem einfachen EVA-Prinzip: Eingabe, Verarbeitung, Ausgabe.
Bei einer Liste, die Du als Parameter übergibst und dann füllst, ist dieses Prinzip nicht so ganz deutlich.
Prinzipiell falsch ist es aber nicht, nur eben anders und sicher auch vom Programmier-Stil abhängig.

Ich übergebe mit dem Command das ViewModel, damit ich das alt bekannte Dilemma nicht mehr habe einen Converter zu brauchen nur um 3-4 Parameter übergeben zu können die im ViewModel stecken. Darum ist der Commandparameter schlicht {Binding Mode=OneWay}

Hm ... ich kenne deinen Aufbau nicht, aber mit dem Aufbau kettest Du die ViewModels aneinander. Ist jetzt eigentlich keine MVVM- oder Schichten-Verletzung, aber vielleicht geht's auch besser.
Z.B. könntest Du ein Interface verwenden. Oder Du hast den Command im "übergeordneten" ViewModel, was die Items sowieso kennt, oder ...
Es gibt viele Wege, dein Weg ist nicht automatisch falsch, nur anders, als ich es machen würde.

Der Command führt sein Execute ja async aus. Der wiederum braucht ja dann einen Prozess auf den er warten kann mit dem "await". Wenn ich nun die LoadFile Methode auch async mache worin das Laden erfolgt welches den Freez auslöst, braucht der ja auch wieder auf etwas auf das er warten kann, aber es kommt dahinter nichts mehr.

Ich gehe davon aus, dass Du eine Datei liest?
Dann hast Du da das Warten: Das Lesen der Datei dauert.
Und IO-Operationen (also auch Dateien lesen) haben meistens bereits eine Async-Implementierung, die Du verwenden kannst.

Ich heul gleich hahaha.

Ich hoffe, Du dachtest nicht, dass Du fertig bist? 😉
Das Thema hört nie auf und DependencyInjection ist nur - meiner Meinung nach - der nächste logische Schritt nach der Schichtentrennung.

Haupt-Ziel: Trennung von Abhängigkeiten.
Die Verantwortlichkeiten hast Du getrennt, aber immer noch sind die Klassen voneinander abhängig, was die ganze Software mit der Zeit sehr starr macht, was Du nicht willst. DependencyInjection ist ein Weg, damit umzugehen.

Statische Methoden in anderen Klassen sind dabei keine Hilfe, da die Klassen ja immer noch voneinander abhängig sind - mit static sogar noch ein gutes Stück mehr.

Hatte mich da schon gefragt ob es dann wirklich immer mit "new" gemacht werden soll

Kommt drauf an, manchmal ja, meistens nein.
Das Problem mit "new" ist, dass Du an der Stelle, wo Du die Instanz erzeugen willst, dann zwangsläufig auch alle Abhängigkeiten brauchst.
In deinem Fall ist das egal, aber sobald Du z.B. Einstellungen weit in die Details verteilen willst, oder noch schlimmer, nachträglich Abhängigkeiten ergänzen willst, stellst Du fest, dass sich dabei schnell eine elendig anstrengende Kette aufbaut, bei der dann am Ende irgendwie alles von allem abhängig ist.

Das kann man natürlich auch anders umgehen, DependencyInjection ist ein Weg, der viel Verbreitung gewonnen hat.
Ein anderer Weg wäre das ServiceLocator-Pattern, aber davon rate ich ab ^^

Der "Nachteil" (für einen Anfänger) von DependencyInjection ist, dass man den recht einfachen straight forward Ablauf aufgeben muss, Du musst also umdenken.
Jeder Service ist prinzipiell für sich alleine gestellt und unabhängig, (im besten Fall) alle Abhängigkeiten kommen per Konstruktor rein und genauso bekommen andere Klassen auch eine Instanz auf die Services.
Diese andere Art, ein Problem anzugehen, musst Du erst lernen und das fällt manchen nicht leicht.
In deinem Fall wäre das vermutlich ein mittelgroßer Umbau (auch, weil das mit WPF schwierig ist), deshalb solltest du dich auch erst daran versuchen, wenn die Basics, die dahin führen, wirklich in Fleisch und Blut übergegangen sind. Ohne wirklich verinnerlicht zu haben, wie man Aufgaben in Services trennt, kannst Du die Services nicht unabhängig voneinander "dependencyinjecten" lassen 😉

Contra: Beim Laden füllt es nicht die Liste laufend, sondern am Ende auf einen Schlag.

Wenn das Lesen der Datei asynchron und klug aufgebaut ist, kannst Du die IAsyncEnumerable anschauen.
Das funktioniert aber nur, wenn Du keinen Task drum herum startest, sondern die Methode in sich asynchron arbeiten kann, außerdem bringt das wieder ein paar Schwierigkeiten mit.
Und es gibt noch viele andere Wege, das IAsyncEnumerable wäre nur mein persönlicher Weg.

23.03.2022 - 20:13 Uhr

Steinigt mich nicht falls dass bei euch wiedermal Augenkrebs auslöst

Schade 😠

  1. Punkt:
    Dein Service lädt nicht nur Daten sondern fügt sie auch noch irgendwo hinzu.
    Das mag kleinkariert klingen, ist aber eine zweite Aufgabe und dir steht's gerade im Weg.

Also warum so kompliziert?
Warum gibt die LoadFile-Methode nicht einfach die geladenen Datensätze zurück?


var myData = await Task.Run(() => new LoadCustomerImportFileService().LoadFile(filename, vm.Progress));

vm.CustomersList.AddRange(myData);

Und ab dem Punkt sollte das await dein ganzes Problem übernehmen.
Wobei das "vm.CustomersList" so aussieht, als würdest Du hier VierModel-übergreifend arbeiten? Muss nicht schlecht sein, aber sieht ein bisschen danach aus.

  1. Punkt:
    Warum liest die LoadFile-Methode synchron? IO-APIs können meistens asynchrone Vorgänge, also warum nutzt Du die nicht?
    Als Problem fällt mir dabei aber allerdings nur ein, dass Du auf diese Weise einen ThreadPool-Thread unnötig lange blockierst, bei dir dürfte das aber kaum ein Problem sein.
    Außerdem ist ein vollständig asynchroner Ablauf nicht gerade einfach, spätestens wenn es dazu kommt, mehrere asynchrone Abläufe untereinander zu synchronisieren, wird's schwer.

Behalte es daher nur im Hinterkopf, dass Du - wenn Du soweit bist - lieber bereits implementierte asynchrone Methoden nutzt, wenn es sie auch gibt.
Bis dahin funktioniert deine Variante auch und ist - solange Du keinen anderen gravierenden Fehler gemacht hast - auch kein großes Problem.

  1. Punkt:
    Du erzeugst den Service selber - also das "new LoadCustomerImportFileService()" ist das Problem.
    Lösen kann man das mit DependencyInjection, allerdings glaube ich, dass es etwas zu weit geht, wenn Du dieses Riesen-Thema auch noch auf machst 😁
    Außerdem ist DI in WPF alles andere als einfach, da WPF ursprünglich nicht dafür gebaut wurde, man es also mehr schlecht als recht nachrüsten muss.
    Es gibt zwar Frameworks, die das versprechen, aber die arbeiten (soweit ich das überblicken kann) immer mit dem ServiceLocator-Pattern, aber das mag ich gar nicht.

Also betrachte diesen Punkt auch erst im Hinterkopf, kann gut sein, dass das Thema weit über's Ziel hinaus schießt.

23.03.2022 - 19:24 Uhr

Die BOM steht am Anfang, das erklärt die falschen Zeichen am Ende nicht.

Wie schon gesagt: Du hast ein Encoding-Problem.
VisualStudio speichert nicht plötzlich ein anderes Encoding, also ist irgendetwas anderes schuld an dem Problem und die Synchronisation ist eine naheliegende Option.

Tut euch einen Gefallen und setzt auf Git.
Solche Tools gibt's nicht ohne Grund, sondern weil sie perfekt auf die Aufgaben zugeschnitten sind.

23.03.2022 - 13:41 Uhr

Hierbei könnte man die Teile der Software mit keinem oder geringem Änderungs-/Erweiterungsbedarf in einer Datei (z.B. dll) zusammenfassen und die restlichen Teile jeweils separat auslagern (dll, Interfaces, Module,...)

Und so ein Satz (ohne weitere Ergänzungen) führt meiner Erfahrung nach schnell zu sowas wie statische Utils-Klassen (sind ja ausgelagert) oder zig partial Klassen (ist ja eine eigene Datei).
Einfach nur auslagern reicht eben nicht aus, Ausgelagerter Code kann ebenso schlimm sein, wenn planlos ausgelagert wurde.

22.03.2022 - 20:07 Uhr

Wie trennt man Verantwortlichkeiten in WPF?
Noch nicht sehr alt und sehr ausführlich

21.03.2022 - 12:53 Uhr

Die Klasse "SerialPort" ist nicht im Namespace "System.IO" und auch nicht in der Assembly "System".

SerialPort Klasse (System.IO.Ports)

21.03.2022 - 09:41 Uhr

Konfigurationsanbieter in .NET

Und wo Du sie findest, kann hier niemand sagen, da das ganz von deinem Code abhängt.

PS:
Es heißt ".NET 6", nicht ".NET Core 6" 😉

11.03.2022 - 21:57 Uhr

Ich frage mich: Warum WinForms?
UWP ist viel näher an WPF, warum geht Ihr nicht zu WPF?
Der Aufwand wäre vermutlich geringer.

10.03.2022 - 17:41 Uhr

Ergänzend zu Th69:

Warum nicht direkt Sleep(milliSeconds) bzw. Delay(milliSeconds)
Ist auch nicht exakt, aber weniger Code und arbeitet nicht mit solchen Krücken.

10.03.2022 - 17:35 Uhr

Um zwei Zeiten zu subtrahieren schau dir das mal an:

Oder einfach der Minus-Operator 😉