Laden...
Avatar #avatar-2894.jpg
gfoidl myCSharp.de - Team
Entwickler in der Gasmotorenbranche (nicht nur Software) Waidring Dabei seit 07.06.2009 6.911 Beiträge
Benutzerbeschreibung
FIS-Speedski (S-DH) world champion 2015 3x FIS-Speedski (S-DH) overall worldcup winner, 2008/09, 2009/10, 2012/13 5x Subaru Velocity Challenge winner, 2009, 2010, 2013, 2014, 2015 7x PopKL Verbier winner, 2009 2x, 2010 2x, 2011 2x, 2013 1x 27 victories in 69 international Speedski-races v_max = 208,333 km/h http://de.wikipedia.org/wiki/Günther_Foidl

Forenbeiträge von gfoidl Ingesamt 6.911 Beiträge

13.05.2022 - 10:30 Uhr

Hallo Palladin007,


GC.Collect();
GC.WaitForPendingFinalzers();
GC.Collect();

unter Angabe der max. Geneartion räumt den Speicher ordentlich auf.

mfG Gü

12.05.2022 - 17:04 Uhr

Hallo Palladin007,

.NET Memory Profiler einen Snapshot erstelle, sind die überflüssigen Instanzen weg und der Speicher freigegeben.

Die meisten dieser Profiler führen eine kompletten GC durch (also inkl. Gen-2 und LOH) und haken sich in den Informationsstrom vom GC rein um so an die Ojekte zu kommen.
Genau dieses Verhalten hast du beobachtet.

Versuch dich einmal an PerfView, das bietet i.d.R. mehr Infos / Möglichkeiten. Aber die UX ist sehr bescheiden...

mfG Gü

11.05.2022 - 11:50 Uhr

Hallo jogibear9988,

mach ein Speicherabbild und analysiere das. Dann siehst du genau welcher Objekt am größten ist, etc.
Das geht am einfachsten mit dem dotnet-dump Tool.

Danach kannst du überlegen wie Objekte geteilt werden, etc.
So pauschal ohne den Code zu kennen, kann ich keine konkrete Hilfe geben.

mfG Gü

09.05.2022 - 09:15 Uhr

Hallo Christoph K. ,

ein RAM-Cache benötigt wird, der entsprechend viele Objekte in einem Dictionary beinhalten soll.

Ist Cache in-memory ein Möglichkeit?
Das wären .NET-Bordmittel und genau für solche Szenarien getrimmt.

mfG Gü

06.05.2022 - 10:40 Uhr

Hallo Christoph K. ,

die Exception "Die Arraydimensionen haben den unterstützten Bereich überschritten."

Die Exception hat auch einen Typ. Schau dir den an, so weißt du worum es geht.
Also ob es OutOfMemory, IndexOutOfRange, etc. ist. Der Text alleine ist schön, aber für eine Diagnose ist der Typ wichtiger.

auf dem Computer ist noch genügend freien RAM vorhanden.

Bei der OutOfMemoryException geht es v.a. darum ob der GC einen zusammenhängenden Speicherbereich verwenden / finden / vom OS verlangen kann, der groß genug ist um das Objekt (hier beim Dictionary ein internen Array) halten zu können. Ist das nicht der Fall -> OOM.

mfG Gü

25.04.2022 - 12:55 Uhr

Hallo Palladin007,

Nun kann es aber sein, dass die arbeitende Instanz aus irgendeinem Grund ... nicht mehr existiert, das Lock vorher aber nicht korrekt freigegeben wurde.

Dazu ist der Releaser ja mit using (bzw. try-finally-Dispose) versehen. Somit wird auch im Fehlerfall der Lock korrekt verlassen.

Durch die WeakReference würde dieser Deadlock irgendwann (indirekt durch den GC) wieder freigegeben werden.

Wahrscheinlicher ist aber, dass dadurch jemand Zugriff zum Lock hat der nicht sollte -- siehe oben.
Statt der WeakReference, die eben nicht deterministisch ist, wäre eine Art Deadlock-Detection möglich. Schauen wie viele beim Warten vor dem Lock sind und wie viele im Lock sind. Wenn da nichts passiert (eine bestimmte Zeit) so mag das als Indiz für den Deadlock verwendet werden, der dann entsperrt wird.

Od. weniger kompliziert: eine automatische Entsperrung nach einer bestimmten Zeit. Wenn z.B. die Arbeiten im geschützen Gebiet X Sekunden dauern, so wird das Ticket automatisch noch 2X entfernt. Wird der Lock verlassen, so den Timer zurücksetzen, etc.

Dann ist das Verhalten wenigsten deterministisch und keine Bug-Quelle.

mfG Gü

25.04.2022 - 12:29 Uhr

Hallo Palladin007,

jetzt hab ich deine Antwort vorhin übersehen...

aber hier ist es ja inhaltlich und dank nullable reference types offensichtlich, dass kein null übergeben werden darf.

Eben nur "darf". Das hindert aber niemanden dennoch null zu übergeben -- v.a. wenn man nicht weiß woher das Objekt kommt.
Unabhängig von Nullability Annotations soll / muss bei public APIs auf null geprüft werden.
Das ist (leider) ein Missverständnis dieses Sprachfeatures. Siehe dazu auch die Diskussion in https://github.com/dotnet/docs/pull/28890#discussion_r841062932.

mfG Gü

25.04.2022 - 12:24 Uhr

Hallo Palladin007,

mir erschließt sich kein Einsatz der WeakReference. Ich halte das sogar für gefährlich und als Bug-Quelle -- das gehört mMn wieder raus.

Warum?
Folgendes Szenario: Lock wurde mit ticket1 betreten, somit ist er für ticket2, etc. versperrt.
Im durch den Lock geschützten Abschnitt passiert jetzt eine Menge, so dass u.a. ein GC durchgeführt wird und der Target der WeakReference wird null. Dadurch ergibt dann state.TryGetTicket(out object? stateTicket) false und es wird versucht in den Channel zu schreiben, da ja klappen wird. D.h. ticket2 bekommt Eintritt zum Lock währen dieser Abschnitt eigentlich noch durch ticket1 geschützt sein sollte.

Od. gibt es einen konkreten Fall wo so ein Verhalten sinnvoll ist?
Das lässt sich äußerst schwer nachvollziehen und erst recht nicht vorhersagen.
Wenn das Verhalten drin bleiben sollte, so sollte auch Diagnose-Unterstützung (EventLog) dazu welche meldet dass ein GC passierte und das Ticket abgeräumt wurde.
Das Ganze wird dadurch aber eher ein Ungetum für etwas wofür ich keinen Einsatzbereich sehe.

mfG Gü

25.04.2022 - 10:13 Uhr

Hallo,

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?

Bei der WeakReference kann der GC das Target-Objekt abräumen wenn er glaubt es sei richtig. Ob dies aber für die Synchronisierung richtig ist kann der GC nicht wissen. Daher ist das Verhlaten vom Lock nicht mehr vorhersehbar und kann zu Bugs führen. Es sei denn das ganze Programm ist dafür ausgelegt bzw. es stellt kein Problem für die Geschäftslogik dar wenn auf einmal andere Ticket bearbeitet werden können.

Aber woher weiß ich, was viel Maschinencode erzeugt?

Selbst heruasfinden. Entweder einen JIT-Dump ansehen (nur den erzeugten Maschinencode), das geht recht einfach mit https://sharplab.io/ und anderen offline Tools (da nehme ich ganz gerne bei BenchmarkDotNet den DisassemblyDiagnoser).
Hier hat aber ein Blick in den Quellcode der Implementierung gereicht um das abschätzen zu können. https://source.dot.net/ ist da ganz praktisch.

geben die bei ticket=null auch false zurück

Diesen Fall würde ich als "undefiniert" für die Synchronisation sehen und daher eine NullReferenceException werfen. "Fail early" ist ein gutes Paradigma, denn so kann unterschieden werden ob tatsächlich null übergeben wurde od. ob der lock einfach nicht betreten werden kann.

Hätte den Vorteil, dass eine besser Verständliche TimeoutException geworfen wird, anstelle einer OperationCanceledException.
Was meinst Du dazu?
Nachteil ist, dass dadurch wieder eine StateMachine nötig wird.

Gute Idee, finde ich besser als wenn nur die OperationCanceledException geworfen wird.
Im Exception-Filter würde es aber reichen when (!cancellationToken.IsCancellationRequested) zu prüfen, denn sonst kann es eh nirgends herkommen.
Wegen der State-Machine sehe ich hier keine relevanten Nachteile, da eh schon genug Code produziert wird, da fällt das eher nicht mehr in Gewicht.

mfG Gü

25.04.2022 - 09:55 Uhr

Hallo,

Einen konkreten Grund, warum sie das Factory-Pattern verwendet haben, weiß ich aber nicht. ... Was der Grund für dieses ungewöhnliche Vorgehen ist, würde mich aber auch interessieren.

Hat v.a. mit den verschiedenen Plattformen (Windows, Unix-artige, MacOs) und der Entstehungsgeschichte von .NET (Core) zu tun und da mit diesem Pattern die konkreten Implementierungen web-abstrahiert werden können. Somit war/ist es möglich auf unix-artigen System OpenSSL zu verwenden, während bei Windows deren Crypto-APIs verwendet werden. Weiters ist es somit möglich die Implementierungen zwischen managed und native zu verschieben ohne dass sich für einen Konsumenten dieser Typen etwas ändert.

mfG Gü

24.04.2022 - 12:45 Uhr

Hallo Palladin007,

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

Das Ticket wird jetzt als WeakReference gespeichert, wenn die Referenz verloren ist, gilt der State automatisch als freigegeben

Das ist aber nicht mehr sehr deterministisch und kann schnell zu unkontrolliertem Verhalten führen.
Zumindest sollte das konfigurierbar sein, z.B. durch ein TicketBehavior-Argument. Somit würde es dann zwei (od. je nachdem wieviele solcher Behaviors zur Verfügung stehen) spezielle State-Klassen geben (mit gemeinsamer abstrakter Basis, so dass die konkreten Typen sealed sein können)

Als Überladung für das Timout würde ich nur (mehr) TimeSpan anbieten. Die int-Überladung somit weg, denn das kann jeder Benutzer selbst erstellen.
Den Trend sehe ich auch bei neuen .NET-APIs.

Kleiner Perf-Tipp:


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

    ValueTask<Releaser> WithTimeout(object ticket, TimeSpan timeout, CancellationToken cancellationToken)
    {
        using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

        timeoutCts.CancelAfter(timeout);

        return EnterAsync(ticket, timeoutCts.Token);
    }
}

Ist schneller zur Laufzeit 😉
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ß.
So wird die Methode aufgeteilt in eine lokale Funktion, so dass EnterAsync selbst klein bleibt und eher sicher vom JIT inlined wird.

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

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

mfG Gü

22.04.2022 - 15:48 Uhr

Hallo Palladin007,

bzgl. Lock <-> Monitor hast du recht. Wenns Monitor genannt würde, so sollte auch ein Pulse, etc. dabei sein. Daher ist wohl doch Lock-Suffix passender.

Eine zweite Klasse die die gleichen Methoden anbietet, nur ohne Ticket.

Das wäre dann ein normaler async lock?
Wie da jetzt das AsyncLocal<object> ins Spiel kommt erschließt sich mir nicht.
Die Klasse kann ja einfach an den TicketLock weiterdelegieren, wobei das Ticket intern erstellt wird.

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

V.a. Code / PRs auf GitHub schau ich mir so auch an.

mfG Gü

22.04.2022 - 12:20 Uhr

Hallo Palladin007,

zum Namen: Ticket gefällt mir ganz gut. Statt TicketLock tendiere ich zu TicketMonitor, da Monitor mehr mit Überwachen zu tun hat als mit Verschließen. Nur so vom Gefühl her.

auch zum Stil ^^

Wenn du schon fragst: ich mag die var nicht, wenn nicht direkt ersichtlich ist welcher Typ es ist. Z.B.

  
if (TryEnter(ticket, out var releaser))  
    return releaser;  
  

Welcher Typ releaser ist erschließt sich nur aus dem Kontext, dem gefolgt werden muss. In VS mit Intellisense gehts, aber wenn nur der Text vom Code vorhanden ist kann das lästig werden.

Ähnlich bei

  
var success = TryEnter(ticket);  
  

Tipp-Ersparnis kanns nicht sein, v.a. mit Intellisense 😉
Es ist schon naheliegend dass success vom Typ bool ist, aber eindeutig lässt sich das nur beantworten wenn die Methode TryEnter bekannt ist.
Ich würde da immer bool direkt hinschreiben. Das vermeidet auch potentielle Fehler, falls (irgendwann) der Rückgabetyp von TryEnter geändert werden sollte, wie z.B. in eine Enum


public enum EnterStata
{
    Success,
    LockHeldByOtherTicket,
    GenericEnterFailure     // in Memoriem an die GDI+ Zeiten ;-)
}

Außer bei anonyment Type und vllt. Linq gibt es seit C# 9 (mit "target typed new") für mich keinen Grund var zu verwenden.

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

Auch hier würde ich gerne sehen welche Instanz da erstellt wird. Das target typed new ist nett -- und ein super Ersatzt für var -- aber es sollte damit nicht übertrieben werden.


var foo0 = new Foo();   // OK, aber mit target typed new nicht nötig
Foo foo1 = new();       // Finde ich besser, da wir von links nach rechts lesen und so der Typ sofort klar ist

Falls sonst der Typ nicht direkt klar ist, so sollte dieser unbedingt explizit angegeben werden. Außer bei, wie oben schon erwähnt, anonyment Typen (da gehts nicht anders) und ev. bei Linq (da kann es u.U. mehr verwirren als helfen).

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

Ansonsten passt dein Code schon. Das Aufteilen in Methoden, auch in Hinblick auf die TryEnter-Methoden, ist praktisch.

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, indem per CancellationTokenSource.CreateLinkedTokenSource eine neue CTS erstellt wird, welche dann mit cts.CancelAfter spätestens beim Timeout getriggert wird.
Hinweis: wenn eine CTS mit einem Timeout erstellt wurde, so muss diese unbedingt Disposed werden, da sonst die Timer-Queue (inter in .NET) wächst (memory leak).

mfG Gü

22.04.2022 - 10:26 Uhr

Hallo Palladin007,

ich hatte die gleiche Überlegung wie dannoe. Dieser Fall dürfte nie eintreten, sofern Enter auch richtig implementiert ist.

Im Allgemeinen hast du aber mit dem "double check lock" recht. Schnelle Prüfung, dann im lock nochmal Prüfen um zu Schauen ob eh durch kein Race sich der Zustand verändert hat.

mfG Gü

21.04.2022 - 13:20 Uhr

Hallo Palladin007,

Müsste das nicht durch das "??=" umgangen werden?

Ups, das hatte ich nur alt = gelesen. Sorry. Da hast du recht, das passt und wegen der Synchronisierung gibt es da auch kein Race.

auf den gleichen Task warten.
...denn die AsyncSemaphore-Implementierung ... nutzt ja auch eine Queue Task kann ja mehrfach erwartet werden, daher passt das schon. Bei ValueTask geht das i.d.R. nicht bzw. ist es im Allgemeinen besser den VT nur 1x zu erwarten.
Vllt. "korrekter" wäre es mit einer Liste (od. Queue) für die wartenden Tasks, da es im Grunde ja verschieden sind -- für jeden wartenden Aufrufer einer.

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.

Unabhängig davon frag ich mich schon ob es nicht ein passendes Konstrukt dafür schon gibt -- v.a. mit Namen, denn es gibt ja (unabhängig von Programmiersprache) mehr od. weniger überall die gleichen Primitiven (die oft auf Dijkstra zurückgehen).

mfG Gü

21.04.2022 - 08:11 Uhr

Hallo Palladin007,

und es tut ja auch nicht weh

Ist aber trotzdem falsch 😉

Noch zwei Dinge zu deinem Code:

  
if (ValidateAccess(obj))  
{  
    if (_obj?.IsAlive == false)  
        _count = 0;  
  
    _count++;  
    _obj = new(obj);  
  
    release = new(this, obj);  
    waitTask = null;  
    return true;  
}  
  

Würde ich als


if (ValidateAccess(obj))
{
    if (_obj?.IsAlive == false)
    {
        _count = 1;
        _obj = new(obj);
    }
    else
    {
        _count++;
    }

    release = new(this, obj);
    waitTask = null;
    return true;
}

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

  
else  
{  
    _waiter ??= new();  
  
    release = default;  
    waitTask = _waiter.Task;  
    return false;  
}  
  

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.
Um das zu korrigieren würde eine Liste mit den TCS benötigt werden. Das schaut den für die Allokationen gar nciht mehr gut aus und macht es doch recht kompliziert. Daher auch die Channels, da diese das ähnliche Problem intern recht elegant, aber nicht-trivial, lösen.

mfG Gü

20.04.2022 - 23:28 Uhr

(Teil 3)


//-----------------------------------------------------------------------------
// Keine Ahnung ob der Name treffend ist.
public sealed class ReEntryLock
{
    // Ein Channel ist hier praktisch, da er die interne Synchronisation und Signalisierung
    // der Tasks übernimmt.
    // Hier verwendet wir einen Channel mit Kapazität 1, da nur mit dem gleichen userState-Objekt
    // gleichzeitig eingetreten werden darf.
    private readonly Channel<State> _channel = Channel.CreateBounded<State>(new BoundedChannelOptions(1));

    // Damit wir wenig Allokationen haben, cachen wir das State-Objekt.
    private readonly State _state = new();
    //-------------------------------------------------------------------------
    // Hier hab ich es EnterAsync genennt, da wegen mehrfachem Eintritt das besser passt
    // als WaitAsync.
    public async ValueTask<Releaser> EnterAsync(object userState, CancellationToken cancellationToken = default)
    {
        while (true)
        {
            // Dieser lock könnte fein-granularer geschrieben werden. Aber dann müsste bei jeder
            // _channel.{Reader,Writer}.TryXYZ-Methode auf das Ergebnis reagiert werden. Somit
            // würde der Code nur unnötig verkompliziert. Daher lieber einfacher (und auch sicherer)
            // mit dem "äußeren" lock.
            lock (_state)
            {
                // Wir schauen ob ein Element im Channel ist.
                // Falls ja, so wurde der Lock schon betreten, daher müssen wir schauen
                // ob es das gleiche userState-Objekt ist um erneut betreten zu dürfen.
                if (_channel.Reader.TryPeek(out State? stateInChannel))
                {
                    if (ReferenceEquals(userState, stateInChannel.UserState))
                    {
                        // Es ist das gleiche Objekt, daher Zähler erhöhen und eintreten lassen.
                        stateInChannel.IncrementEnteredCount();
                        return new Releaser(this, userState);
                    }
                }
                // Es ist kein Element im Channel, daher eintreten lassen.
                else
                {
                    bool written = _channel.Writer.TryWrite(_state);
                    Debug.Assert(written);

                    _state.Reset(userState);
                    return new Releaser(this, userState);
                }
            }

            // Bleibt nichts anderes übrig als zu warten bis wieder Platz im Channel ist.
            await _channel.Writer.WaitToWriteAsync(cancellationToken).ConfigureAwait(false);
        }
    }
    //-------------------------------------------------------------------------
    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));
                }
            }
        }
    }
    //-------------------------------------------------------------------------
    // Eine struct wäre schön, geht hier aber nicht, da sie mutable sein muss
    // für das Inkrementieren vom Zähler, aber beim Schreiben in den Channel
    // wird eine Kopie (wegen Werttyp) erstellt und das Inkrementieren ist sinnlos.
    // Daher muss es ein Referenztyp sein.
    [DebuggerDisplay("EnteredCount: {_enteredCount,nq}, UserState: {UserState,nq}")]
    private class State
    {
        private int _enteredCount;
        public object? UserState { get; private set; }
        //---------------------------------------------------------------------
        public void Reset(object userState)
        {
            _enteredCount  = 1;
            this.UserState = userState;
        }
        //---------------------------------------------------------------------
        public int IncrementEnteredCount() => ++_enteredCount;
        public int DecrementEnteredCount() => --_enteredCount;
    }
    //-------------------------------------------------------------------------
    // Ist nicht nötig, aber so kann per using eleganter gearbeitet werden,
    // anstatt manuell Release aufrufen zu müssen (das ist in deinem Code ja auch schon so).
    public readonly struct Releaser : IDisposable
    {
        private readonly ReEntryLock _parent;
        private readonly object      _userState;
        //---------------------------------------------------------------------
        internal Releaser(ReEntryLock parent, object userState) => (_parent, _userState) = (parent, userState);
        //---------------------------------------------------------------------
        public void Dispose() => _parent?.Release(_userState);
    }
}

Natürlich ginge das ohne Channel auch, aber der Channel nimmt uns sehr viel komplizierte Arbeit ab.
Die Tasks, welche warten müssen, könnten auch per TaskCompletionSource<T>, IValueTaskSource, etc. erstellt werden. Aber wie denen dann signalisiert wird dass es weiter geht ist doch recht aufwändig -- v.a. wenns robust und sicher gemacht werden soll. Je nachdem wo die Anwendung laufen soll, kann auch noch der ExecutionContext usw. ins Spiel kommen. Das will ich mir und dir ersparen, daher sind die Channels verwendet worden.

  
[MaybeNullWhen(false)] out Releaser release  
  

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

mfG Gü

20.04.2022 - 23:27 Uhr

(Teil 2)

Zurück zu deinem Problem: das lässt sich mit Channel ganz gut abbilden -- (für mich) der Vorteil dabei ist, dass ValueTasks im Spiel sind und v.a. bei Channels sind diese mittels IValueTaskSource umgesetzt, so dass diese Tasks selbst keine zusätzlichen Allokationen haben (beim await die AsyncStateMachineBox, welche die lokalen Variablen, etc. hält, gibt es aber auch hier, geht nicht anders).
Nur dass du gedanklich das Problem da auch invertieren musst. Etwas fertiges kann ich nicht nennen, aber ein Beispiel (mit etlichen Kommentaren).
Hab die Klasse ReEntryLock genannt, keine Ahnung ob es bessere Bezeichnungen gibt -- Namensgebung ist ziemlich das schwerste 😉


//#define PRINT_ID

using System.Diagnostics;
using System.Threading.Channels;

const int N                       = 5;
using CancellationTokenSource cts = new();
ReEntryLock reEntryLock           = new();
List<Task> tasks                  = new(capacity: N);

Console.WriteLine("Start");

tasks.Add(Do(new UserState(1, ConsoleColor.Cyan)   , cts.Token));
tasks.Add(Do(new UserState(2, ConsoleColor.Magenta), cts.Token));
tasks.Add(Do(new UserState(3, ConsoleColor.White)  , cts.Token));

await Task.WhenAll(tasks);
Console.WriteLine("Done");
//-----------------------------------------------------------------------------
async Task Do(UserState state, CancellationToken cancellationToken)
{
    await Task.Yield();

    Print(state, ConsoleColor.Red, "before signal");
    using (await reEntryLock.EnterAsync(state, cancellationToken))
    {
        Print(state, ConsoleColor.Green, "behind signal");

        for (int i = 0; i < 3; ++i)
        {
#if PRINT_ID
            using (await reEntryLock.EnterAsync(state, cancellationToken))
            {
                Thread.Sleep(Random.Shared.Next(750, 1000));        // für Demo kein Task.Delay
                Console.Write(state.Id);
            }
#else
            Print(state, ConsoleColor.Red, "before signal");
            using (await reEntryLock.EnterAsync(state, cancellationToken))
            {
                Print(state, ConsoleColor.Green, "behind signal");
                Thread.Sleep(Random.Shared.Next(750, 1000));        // für Demo kein Task.Delay
            }
            Print(state, ConsoleColor.Yellow, "released signal");
#endif
        }
#if PRINT_ID
        Console.WriteLine();
#endif
    }
    Print(state, ConsoleColor.Yellow, "released signal");
}
//-----------------------------------------------------------------------------
[DebuggerStepThrough]
void Print(UserState state, ConsoleColor color, string message)
{
#if !PRINT_ID
    lock (tasks)
    {
        Console.ForegroundColor = state.Color;
        Console.Write($"Thread-Id: {Environment.CurrentManagedThreadId}, Id: {state.Id}, ");
        Console.ForegroundColor = color;
        Console.WriteLine($"{message}");
        Console.ResetColor();
    }
#endif
}
//-----------------------------------------------------------------------------
public record UserState(int Id, ConsoleColor Color);

Im nächsten Teil der eigentliche Teil, auf den ich schon die ganze Zeit hinaus will...

20.04.2022 - 23:25 Uhr

Hallo Palladin007 (Teil 1),

habs für dich mit ein paar Code-Beispielen mehr versehen (musste den Beitrag aufteilen, da zuviel Zeichen vorhanden sind) 😉

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.

So hab ich das eigentlich eh verstanden.
Vllt. hab ich mich im vorigen Kommentar unglücklich ausgedrückt, denn das von dir zitierte war nur für die Channel als Ersatz für die Semaphore gedacht.

Hier als Beispiel:


using System.Threading.Channels;

const int N = 3;

using CancellationTokenSource cts                       = new();
MySemaphareMadeWithChannels mySemaphareMadeWithChannels = new(2);
List<Task> tasks                                        = new(capacity: 3);

Print("Start");

tasks.Add(Do(cts.Token, ConsoleColor.Cyan));
tasks.Add(Do(cts.Token, ConsoleColor.Magenta));
tasks.Add(Do(cts.Token, ConsoleColor.White));

await Task.WhenAll(tasks);
Print("Done");
//-----------------------------------------------------------------------------
async Task Do(CancellationToken cancellationToken, ConsoleColor color)
{
    // Ohne Sync-Context und mit dem Standard-TaskScheduler wird die Continuation
    // einfach dem ThreadPool hinzugefügt.
    await Task.Yield();

    Print("before signal", color, ConsoleColor.Red);
    await mySemaphareMadeWithChannels.WaitAsync(cancellationToken);
    Print("behind signal", color, ConsoleColor.Green);

    // Für Demo ist Thread.Sleep geeigneter, da mit Task.Delay der Ausführungs-Thread
    // gewechselt werden kann.
    //await Task.Delay(Random.Shared.Next(750, 1000), cancellationToken);
    Thread.Sleep(Random.Shared.Next(750, 1000));

    mySemaphareMadeWithChannels.Release();
    Print("released signal", color, ConsoleColor.Yellow);
}
//-----------------------------------------------------------------------------
void Print(string message, ConsoleColor? colorForId = null, ConsoleColor? colorForMessage = null)
{
    if (colorForId is not null && colorForMessage is not null)
    {
        lock (tasks)
        {
            Console.ForegroundColor = colorForId.Value;
            Console.Write($"Thread-Id: {Environment.CurrentManagedThreadId,2}, ");
            Console.ForegroundColor = colorForMessage.Value;
            Console.WriteLine($"free spaces: {mySemaphareMadeWithChannels.FreeSpaces}, {message}");
            Console.ResetColor();
        }
    }
    else
    {
        Console.WriteLine($"Thread-Id: {Environment.CurrentManagedThreadId,2}, free spaces: {mySemaphareMadeWithChannels.FreeSpaces}, {message}");
    }
}
//-----------------------------------------------------------------------------
public sealed class MySemaphareMadeWithChannels
{
    private readonly Channel<int> _channel;
    //-------------------------------------------------------------------------
    public MySemaphareMadeWithChannels(int taskCount)
    {
        _channel = Channel.CreateBounded<int>(new BoundedChannelOptions(taskCount));

        for (int i = 0; i < taskCount; ++i)
        {
            _channel.Writer.TryWrite(42);
        }
    }
    //-------------------------------------------------------------------------
    public int FreeSpaces => _channel.Reader.Count;
    //-------------------------------------------------------------------------
    public ValueTask WaitAsync(CancellationToken cancellationToken = default)
    {
        ValueTask<int> readTask = _channel.Reader.ReadAsync(cancellationToken);

        // Falls synchron fertig, so kann die async-Statemachine, welche C# erstellt, vermieden werden
        if (readTask.IsCompleted)
        {
            // Der ValueTask kann aus einer IValueTaskSource erstellt worden sein. Diese muss
            // zurückgesetzt werden und das geschieht per Zugriff auf Result.
            // Mit GetAwaiter().GetResult() geschieht das Gleich, nur effizienter.
            readTask.GetAwaiter().GetResult();
            return ValueTask.CompletedTask;
        }

        return Core(readTask);

        static async ValueTask Core(ValueTask<int> task)
        {
            await task.ConfigureAwait(false);
        }
    }
    //-------------------------------------------------------------------------
    public void Release() => _channel.Writer.TryWrite(42);
}

20.04.2022 - 11:19 Uhr

Hallo Palladin007,

erstell dir eine eigene Datenstrukture welche für die Synchronisation zuständig ist.
Inter kann diese entweder via SemaphoreSlim arbeiten od. (was mir besser gefällt) mit Channel<T>s.

Bei der Variante mit den Channel<T> gibts die "Kapazitäten" vor und wenns 0 wird, so heißt es warten.
Da das alles in deiner Datenstruktur gekapselt ist, kannst du je nach übergebenen Objekt weiter entscheiden.

Der Vorteil von Channel<T> ist auch, dass diese ValueTask-basiert sind und somit bei vielen Vorgängen wenn keine "Contention" vorliegt performanter sind.
Grundsätzlich lässt sich das aber auch alles per Semaphore(Slim) erledigen.

mfG Gü

09.04.2022 - 10:17 Uhr

Hallo LangWind,

super, wenn du eine Lösung gefunden hast. Noch idealer und im Sinne der Community wäre jedoch, wenn du hier die Lösung auch zeigen könntest.
Falls jemand ein ähnliches Problem hat, so kann er sich daran orientieren.

mfG Gü

05.04.2022 - 09:34 Uhr

Hallo henrik1995,

da stimme ich Stefan.Haegele komplett zu.

Was soll den die Anwendung machen?
Wenns z.B. Berichte erstellt, so ist es "besser" wenn beispielsweise in der Fußzeile "Lizenziert für XYZ" steht. Das ist einfach zu bewerkstelligen und wenn "ABC" den Bericht weitergibt, so ist es wohl verwunderlich dass er mit "XYZ" annotiert ist.
Daher auch vorhin die Frage nach dem Anwendungsfall -- vllt. gibt es ja eine Lösung die sinnvoll erscheint.

Juristischer ausgedrückt (obwohl ich kein Jurist bin) ist die "Projekthyginie" vernüftiger als der Versuch einen Lizenzschutz per SW-Mechanismus zu erzwingen, der eh leicht auszuhebeln ist.

Ich denke dass jeder Programmierer (meist in der Anfangszeit seines Wirkens) einen ähnlichen Wunsch nach Lizenzierung des eigenen (Mini-) Programms hatte. Mit fortschreitender Erfahrung geht dieser Wunsch aber meist gegen 0, aus den erwähnten Gründen.

mfG Gü

02.04.2022 - 12:29 Uhr

Hallo LangWind,

achte bei iText unbedingt auf die Lizenz.

Falls das PDF rein textuell aufgebaut ist, so kann auch z.B. mittels pdfgrep der Text extrahiert und anschließend von deinem C#-Programm geparst werden.
Da der Export rein textuell ist und gem. deinem Beispiel das recht kanonisch aufgebaut ist, sollte das gehen. Bei den Arbeitern ist der Fettgedruckte auch mit einem x versehen, so dass dies (beim Beispiel zumindest) eindeutig ist.

mfG Gü

02.04.2022 - 12:24 Uhr

Hallo henrik1995 ,

Eine License-Validierung beendet dann aber wieder den Prozess.

So verlockend das auch sein mag, das lässt sich relativ einfach umgehen / patchen, so dass dieser Schutz nicht wirklich wirksam ist.
Das ist per se keine Eigenheit von .NET / C#, sonder trifft auch auf native Anwendungen (wie jene in C/C++ geschrieben) zu.

Gibt es einen konkreten Anwendungsfall den du abdecken willst?
Vllt. gibt es dafür eine speziellere und sicherere Lösung.

mfG Gü

29.03.2022 - 16:52 Uhr

Hallo ill_son,

als einfaches "Pattern":


public interface IStep
{
    Task ExecuteAsync(CancallationToken cancellationToken);
}

public class FirstStep : IStep
{
    // ...
}

public class SecondStep : IStep
{
    // ...
}

public class Manager
{
    private readonly List<IStep> _steps;

    public Manager(List<IStep> steps) => _steps = steps;

    public async Task RunAsync(CancallationToken cancellationToken)
    {
        foreach (IStep step in _steps)
        {
            try
            {
                await step.ExecuteAsync(cancellationToken);    // mit od. ohne ConfigureAwait -- abhängig von der Umgebung
                // Hier ev. Event feuern für Fortschritt od. mittels IProgress-Pattern das berichten
            }
            catch (Exception ex)
            {
                // Loggen od. was auch immer
                // Schleife kann hier auch mit break verlassen werden        
            }
        }
    }
}

So ist jeder Schritt eine eigene Klasse und kann separat implementiert und getestet werden.

mfG Gü

20.03.2022 - 11:39 Uhr

Hallo Loofsy,

Ich habe ein Client Serversystem entwickelt

Was hast du denn (als größeres Ziel) vor? Client-Server Protokolle / Systeme wurden schon viele entwickelt, manche sind wieder verschwunden, andere haben sich etabliert.
Die Etablierten sind allesamt vielfach im Einsatz und somit auch sehr robust, wie z.B. HTTP-APIs, gRPC, etc. Je nach konkreter Anforderung können weitere Möglichkeiten in Betracht gezogen werden -- ob es rein Client/Server ist od. doch eher Pub/Sub, etc.

Selbst entwickeln würde ich so ein System eigentlich nur zu Lern-/Verständniszwecken od. wenn es so eine Nischenanwendung ist dass es wirklich kein vorhandenes System dafür gibt.

mfG Gü

17.03.2022 - 14:10 Uhr

Hallo sane,

Also besser keinen Code von anderen annehmen

So würde ich das nicht schlussfolgern. Besser ist es einen aktuellen und sicheren Editor (wie VS in aktueller Version) zu verwenden, damit kein Schaden angerichtet werden kann.

mfG Gü

08.03.2022 - 16:00 Uhr

Hallo edilovsky,

data [] von Socket lesen ----> in einem Queue speichern dann von eine andere Thread byteweise rausgeben und bewerten.

Schau dir dazu System.IO.Pipelines in .NET an. Das passt womöglich gut zu deinem Anwendungsfall.

mfG Gü

03.03.2022 - 19:34 Uhr

Hallo BlackSea,

kannst du das bitte ein bischen genauer beschreiben?
Ein Forum lebt davon dass die Community etwas davon hat und vllt. kann jemand anderes diese Info auch benötigen.

mfG Gü

03.03.2022 - 12:08 Uhr

Hallo BlackSea,

Im ersten Programm werden die Werte auf dem DB geändert. Auf dem 2'ten Laufenden Programm soll die geänderte wert im Labelbox angezeigt werten.

Das erste Programm kann dann auch einen "Broadcast" machen, welcher die geänderten Werte versendet.
Das zweite Programm empfängt dann diesen Broadcast.

Recht einfach geht das z.B. per SignalR od. mit gRPC. Schau dir das einmal an und entscheide dann was besser zu deiner Anwendung passt.

mfG Gü

16.02.2022 - 18:17 Uhr

Hallo sane,

der Code, wenn man ihn ansieht, aussieht, wie ein richtiger code

Schau mal: https://trojansource.codes/ und lass dich überaschen.

mfG Gü

04.02.2022 - 13:42 Uhr

Hallo ill_son ,

resete ich den Timeout (CancelAfter)

Das geht bei einer CancellationTokenSource nicht* immer. Sie ist sozusagen "one way" falls einmal gesetzt bzw. gecanceled und kann dann nicht mehr zurückgesetzt werden. Trifft das zu? Daher hat es womöglich auch keinen Effekt.

Erstell besser eine neue CTS für jeden Request-Vorgang mit entsprechendem Timeout.

* CancellationTokenSource.TryReset wurde für diese Zwecke mit .NET 6 eingeführt.

Nach jedem DataReceivedEvent packe ich die empfangenen Daten in eine globale Liste, schaue ob Anfangs- und Endekennung da sind und gebe den Rahmen zurück

Das wäre ein Anwendungsfalls für System.IO.Pipelines in .NET.

mfG Gü

21.01.2022 - 10:22 Uhr

Hallo oehrle,

Heißt das das es für die Einbindung noch ein Trick gibt?

Es geht halt einfach nicht. .NET Core bzw. ab .NET 5 ohne Core ist neuer und wird von .NET Desktop (bis .NET 4) nicht unterstützt. Da kann man nichts machen.
.NET Standard wäre möglich um für .NET 4 zu entwickeln.
WPF geht aber seit .NET Core 3.1 auch.

mfG Gü

10.01.2022 - 10:49 Uhr

Hallo Toren,

als Inspiration kannst du dir GitHub - gfoidl/TicTacToe: Tic Tac Toe in C# with minimax and alpha-beta-pruning -- compact storage of the board as ints and vectorized where possible anschauen (hab das damals 2018 so gemacht um wiedereinmal mit WPF was zu machen, keine Ahnung ob ich das immer noch so machen würde).

mfG Gü

28.12.2021 - 14:18 Uhr

Hallo Verzweifelt47 ,

"Segmentation fault"

Lt. Fehler wird versucht auf Speicher außerhalb des erlaubten Bereichs zuzugreifen. Prüf mal mit dem Debugger ob die Indizirung in den "Vektoren" auch passt od. ob hier ev. Zeilen und Spalten vertauscht wurde od. die Index-Berechnung nicht passt.

Bau am besten auch gleich entsprechende "asserts" ein, welche nur in der Debug-Konfiguration vorhanden sind und im Release entfernt werden.

Mit Code-Tags statt Bildanhang könnte dir besser gehofen werden, es sollte also in deinem Interesse liegen das Helfen so einfach wie möglich zu machen.
BTW: ist das Copy & Paste vom Code hier als Beitrag wohl weniger Aufwand als einen Screenshot anzuhängen...

Als Randbemerkung: auf myCSharp.de ist wie der Namen anklingen vermag C# die übliche Sprache um die es geht, aber wir versuchen soweit möglich auch bei anderen Sprachen zu helfen.

mfG Gü

21.12.2021 - 21:43 Uhr

Hallo,

Ansonsten würde ich den Stream nie so verarbeiten.
...
Die Length kann man z.B. verwenden um bei kleinen Dateien ein Array zu erstellen und die Daten in einem Rutsch zu lesen...
...
Bei einer lokalen Datei ist das kein Problem, bei einem Stream z.B. über http hingegen muss erst ein Read erfolgen.

Da stimm ich zu 🙂
V.a. für Streams "unbekanner" Größe ist I/O pipelines - .NET sehr empfehlenswert.

mfG Gü

21.12.2021 - 21:36 Uhr

Hallo,

auch wenn "Alt" davor steht

  
Alt : if (such_ar_bez1 != "") sqlstr += "ar_bez1 LIKE '" + such_ar_bez1.ToSqlString() + "' AND ";  
  

der dringende Hinweis zu [Artikelserie] SQL: Parameter von Befehlen.
So ist es ein Musterbeispiel für SQL-Injection

mfG Gü

18.12.2021 - 11:04 Uhr

Hallo Kriz,

um wieviele Emails sprechen wir denn überhaupt?

Limit bei 200 Mails am Tag ... vllt alle drei-vier Monate mal einen Newsletter,

Wenns sichs ausgeht, so könntent die Emails auch über mehrere Tage verteilt geschickt werden.
Bei 3-4 Montage Intervall, sollte ein Verzug von 1-2 Tage wohl drin sein 😉
Hängt aber von der Anzahl zu sendenen Mails ab und vom Inhalt der versandt wird.

Auch könntest du mehrere freie Mail-Sender kombinieren, indem die zu versendenden Mails aufgeteilt werden.

Od. auch einen Mail-Sender suchen und evaluieren mit dem per Email verrechnet wird, dann wäres eine unkomplizierte Lösung.

mfG Gü

12.12.2021 - 17:02 Uhr

Hallo Pedant,

auch dir ein Danke für die ausführliche Rückmeldung und weiteren Infos.
So soll ein Forenbetrieb funktionieren 🙂

Auf meinen Entwicklungsrechnern waren diese *d.dll vorhanden, auf normalen Windows10-Rechnern aber nicht.
Auf den normalen Windows10-Rechnern startete mein Programm daher nicht.

Daher als Tipp:* probier die Anwendung "immer" auf einem frisch aufgesetzten Rechner aus -- am einfachsten mit einer virtuellen Maschine

  • verwende CI (continuous integration) mit anschließenden automatischen Tests (im Idealfall mit automatischen Deploy --> CI/CD)

So kann das Problem "läuft auf meinem Rechner, woanders nicht" eliminiert bzw. eingedämmt werden.

mfG Gü

09.12.2021 - 22:07 Uhr

Hallo Pedant,

Nebenbei gefragt: Was ist eigentlich ein module?

In .NET besteht eine Assembly aus einem od. mehreren Modulen. I.d.R. jedoch nur aus einem Modul, daher verinfacht (und nicht ganz korrekt): Assembly = Modul.
Weiters hat eine Assembly ein Manifest, so dass diese auch von der CLR geladen werden kann, während das Modul den "ausführbaren" Teil enthält (also Code, Resourcen, etc.).

Wenn gemeldet wird, dass ein module nicht gefunden werden konnte, dann sollte man meinen, dass nach einem module gesucht wurde und nicht nach irgendeinem, sondern nach einem bestimmten.

Das stimmt, allerdings kann die CLR (die Laufzeitumgebung in .NET) nur managed Assemblies / Module verfolgen.
Sobald es in den unmanaged / nativen Bereich geht, kann die CLR das nicht mehr verfolgen, daher ist die Fehlermeldung nicht mehr sehr präzise.

Die ITapi3.dll ist lt. Beschreibung " C++/CLI TAPI 3.0 client .NET wrapper" als eine "mixed mode assembly". D.h. sie hat einen manged Teil (den die CLR ausführen kann) und einen nativen / unmanaged Teil. Entsprechend Fehlermeldung scheint es so, als ob eine native Abhängigkeit vermisst wird ("or one of its dependencies").
Ist die ITapi3.dll richtig installiert bzw. das naitve tapi3.dll vorhanden?
Versuch mal mit ILSpy o.ä. die Referenzen anzuschauen. Dort sind die nativen Abhängigkeiten (in erster Ebene) angeführt. Sonst ev. mit DependencyWalker* gucken was fehlt.

Edit: * den gibts jetzt sogar neu: GitHub - lucasg/Dependencies: A rewrite of the old legacy software "depends.exe" in C# for Windows devs to troubleshoot dll load dependencies issues.

mfG Gü

09.12.2021 - 11:58 Uhr

Hallo T-Virus ,

Vermutlich habt ihr auch einige Stellschrauben gedreht um da noch etwas besseres Verhalten zu bekommen.

Ein paar Stellschrauben-Änderungen haben wir geplant, aber absichtilch noch nicht umgesetz, da wir den reinen Vergleich von .NET 5 -> .NET 6 sehen wollten.
Bei solchen Schritten wollen wir nicht "so viel wie möglich", sondern "Schritt für Schritt", da sich so auch der Einfluss der einzelnen Änderungen besser sehen lässt (und wir ja auch davon lernen können und wollen).

Mehr Artikel in dieser Hinsicht sind geplant (eigentlich eh schon lange), aber die Zeit ist da ein Antagonist (wie so oft).

Gibt es hier auch ein paar Zahlene wie sich z.B. die Requests pro Sek. verbessert haben?

Ja, aber das lass ich Abt beantworten 😉
Persönlich finde ich die Application efficiency = (Requests per second) / (CPU utilization of application) interessanter als die normalen Reqs/s.

mfG Gü

09.12.2021 - 10:11 Uhr

Working set (MB)

09.12.2021 - 10:11 Uhr

Thread Pool completed work items (# Items / s)

09.12.2021 - 10:11 Uhr

Thread count (# Threads)

Anmerkung: die VM hat 4 Kerne

09.12.2021 - 10:10 Uhr

EF Query Cache Hit-rate (%)

Anmerkung: bis zum Ende des Sommers hatten wir eine Hit-Rate von 100 %, aber durch irgendein Update sank diese auf 50-60 %. Bisher konnten wir nicht wirklich exakt lokalisieren warum das so war. Aber da wir jetzt wieder bei 100 % sind, werden wir diese Issue wohl schließen können.

09.12.2021 - 10:10 Uhr

GC Heap Size (# Objects)

09.12.2021 - 10:10 Uhr

Allocation Rate (Objekte / s)

Man beachte die ausgeprägte Korrelation zwischen diesen beiden Metriken (CPU Usage und Allocation Rate) (da IO-bound).

09.12.2021 - 10:09 Uhr

CPU Usage (%)

09.12.2021 - 10:08 Uhr

myCSharp.de läuft auf .NET 6

am Sonntagmittag des 28.11.2021 wurde die Laufzeitumgebung vom Forum von .NET 5 auf .NET 6 gewechselt.

Metriken / EventCounters

Wir nutzen Application Insights um Metriken zu sammeln. Dabei sind es zum einen standardmäßige Metriken von .NET, ASP.NET Core und EF Core die aufgezeichnet werden, zum anderen haben wir eine Menge an eigenen Metriken erstellt, um zu sehen wie sich unser Code so verhält.

Das hört sich komplizierter an als es ist, daher ein Beispiel.
Zum Rendern vom BBCode in eine HTML-Darstellung werden StringBuilder verwendet, welche gepoolt werden. Um hier zu sehen, ob das Pooling funktioniert od. nicht, haben wir eine spezielle Policy erstellt:


using System.Text;
using Microsoft.Extensions.ObjectPool;

namespace MyCSharp.Portal.Diagnostics;

public class MonitoringStringBuilderPooledObjectPolicy : StringBuilderPooledObjectPolicy
{
    public override StringBuilder Create()
    {
        StringBuilderPoolEventCounterSource.Log.LogCreated();
        return base.Create();
    }

    public override bool Return(StringBuilder obj)
    {
        bool isReturned = base.Return(obj);

        if (isReturned)
        {
            StringBuilderPoolEventCounterSource.Log.LogReturned();
        }
        else
        {
            StringBuilderPoolEventCounterSource.Log.LogNotReturned();
        }

        return isReturned;
    }
}

Die dazugehörende EventSource:


using System;
using System.Collections.Generic;
using System.Diagnostics.Tracing;
using System.Runtime.CompilerServices;
using System.Threading;

namespace MyCSharp.Portal.Diagnostics;

[EventSource(Name = EventSourceName)]
internal sealed class StringBuilderPoolEventCounterSource : EventSource
{
    public static readonly StringBuilderPoolEventCounterSource Log = new();

    public const string EventSourceName = "MyCSharp.StringBuilderPool";
    private const string CreatedPerSecondCounterName = "created-per-second";
    private const string ReturnedPerSecondCounterName = "returned-per-second";
    private const string NotReturnedPerSecondCounterName = "not-returned-per-second";
    private const string TotalCreatedCounterName = "total-created";
    private const string TotalReturnedCounterName = "total-returned";
    private const string TotalNotReturnedCounterName = "total-not-returned";
    private const string TotalUsedCounterName = "total-used";

    private const int CreatedId = 1;
    private const int ReturnedId = 2;
    private const int NotReturnedId = 3;

    private long _totalCreated;
    private long _totalReturned;
    private long _totalNotReturned;
    private long _totalUsed;

    private IncrementingPollingCounter? _createdPerSecondCounter;
    private IncrementingPollingCounter? _returnedPerSecondCounter;
    private IncrementingPollingCounter? _notReturnedPerSecondCounter;
    private PollingCounter? _totalCreatedCounter;
    private PollingCounter? _totalReturnedCounter;
    private PollingCounter? _totalNotReturnedCounter;
    private PollingCounter? _totalUsedCounter;

    private StringBuilderPoolEventCounterSource() { }

    [NonEvent]
    public void LogCreated()
    {
        Interlocked.Increment(ref _totalCreated);

        if (IsEnabled())
        {
            Created();
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    [Event(CreatedId, Level = EventLevel.Informational)]
    private void Created() => WriteEvent(CreatedId);

    [NonEvent]
    public void LogReturned()
    {
        Interlocked.Increment(ref _totalReturned);
        Interlocked.Increment(ref _totalUsed);

        if (IsEnabled())
        {
            Returned();
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    [Event(ReturnedId, Level = EventLevel.Informational)]
    private void Returned() => WriteEvent(ReturnedId);

    [NonEvent]
    public void LogNotReturned()
    {
        Interlocked.Increment(ref _totalNotReturned);
        Interlocked.Increment(ref _totalUsed);

        if (IsEnabled())
        {
            NotReturned();
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    [Event(NotReturnedId, Level = EventLevel.Informational)]
    private void NotReturned() => WriteEvent(NotReturnedId);

    protected override void OnEventCommand(EventCommandEventArgs command)
    {
        if (command.Command == EventCommand.Enable)
        {
            _createdPerSecondCounter ??= new IncrementingPollingCounter(CreatedPerSecondCounterName, this, () => Volatile.Read(ref _totalCreated))
            {
                DisplayName = "Created Rate",
                DisplayRateTimeScale = TimeSpan.FromMinutes(1)  // Application Insights is 1 minute
            };

            _totalCreatedCounter ??= new PollingCounter(TotalCreatedCounterName, this, () => _totalCreated)
            {
                DisplayName = "Total Created"
            };

            _returnedPerSecondCounter ??= new IncrementingPollingCounter(ReturnedPerSecondCounterName, this, () => Volatile.Read(ref _totalReturned))
            {
                DisplayName = "Returned Rate",
                DisplayRateTimeScale = TimeSpan.FromMinutes(1)  // Application Insights is 1 minute
            };

            _totalReturnedCounter ??= new PollingCounter(TotalReturnedCounterName, this, () => Interlocked.Read(ref _totalReturned))
            {
                DisplayName = "Total Returned"
            };

            _notReturnedPerSecondCounter ??= new IncrementingPollingCounter(NotReturnedPerSecondCounterName, this, () => Volatile.Read(ref _totalNotReturned))
            {
                DisplayName = "Not-Returned Rate",
                DisplayRateTimeScale = TimeSpan.FromMinutes(1)  // Application Insights is 1 minute
            };

            _totalNotReturnedCounter ??= new PollingCounter(TotalNotReturnedCounterName, this, () => Volatile.Read(ref _totalNotReturned))
            {
                DisplayName = "Total Not-Returned"
            };

            _totalUsedCounter ??= new PollingCounter(TotalUsedCounterName, this, () => _totalUsed)
            {
                DisplayName = "Total Used"
            };
        }
    }

    public static IEnumerable<string> GetCounterNames()
    {
        yield return CreatedPerSecondCounterName;
        yield return ReturnedPerSecondCounterName;
        yield return NotReturnedPerSecondCounterName;
        yield return TotalCreatedCounterName;
        yield return TotalReturnedCounterName;
        yield return TotalNotReturnedCounterName;
        yield return TotalUsedCounterName;
    }
}

Application Insights sammelt standardmäßig keine Counters, daher müssen diese selbst aktiviert werden. Das machen wir mit (sorry für den vielen Code, aber dort seht ihr welche Counter wir sonst noch aktiviert haben):


using System.Collections.Generic;
using Microsoft.ApplicationInsights.Extensibility.EventCounterCollector;
using MyCSharp.Portal.Diagnostics;

namespace MyCSharp.Portal.Monitoring.Diagnostics;

public static class AppInsightsExtensions
{
    public static void AddMyCSharpEventCounters(this EventCounterCollectionModule module)
    {
        AddRuntimeCounters(module);
        AddHostingCounters(module);
        AddKestrelCounters(module);
        //AddHttpConnectionCounters(module);    // counters for SignalR
        AddDnsCounters(module);
        AddEntityFrameworkCounters(module);

        foreach (string eventCounterName in ContentRendererEventSource.GetCounterNames())
            module.Counters.Add(new EventCounterCollectionRequest(ContentRendererEventSource.EventSourceName, eventCounterName));

        foreach (string eventCounterName in StringBuilderPoolEventCounterSource.GetCounterNames())
            module.Counters.Add(new EventCounterCollectionRequest(StringBuilderPoolEventCounterSource.EventSourceName, eventCounterName));

        foreach (string eventCounterName in BBCodeLinkToUrlTextFormatterEventSource.GetCounterNames())
            module.Counters.Add(new EventCounterCollectionRequest(BBCodeLinkToUrlTextFormatterEventSource.EventSourceName, eventCounterName));

        foreach (string eventCounterName in SmtpMailSenderEventSource.GetCounterNames())
            module.Counters.Add(new EventCounterCollectionRequest(SmtpMailSenderEventSource.EventSourceName, eventCounterName));
    }

    private static void AddRuntimeCounters(EventCounterCollectionModule module)
    {
        const string EventSourceName = "System.Runtime";

        foreach (string eventCounterName in GetCounters())
            module.Counters.Add(new EventCounterCollectionRequest(EventSourceName, eventCounterName));

        static IEnumerable<string> GetCounters()
        {
            yield return "time-in-gc";
            yield return "alloc-rate";
            yield return "cpu-usage";
            yield return "exception-count";
            yield return "gc-heap-size";
            yield return "gen-0-gc-count";
            yield return "gen-0-size";
            yield return "gen-1-gc-count";
            yield return "gen-1-size";
            yield return "gen-2-gc-count";
            yield return "gen-2-size";
            yield return "loh-size";
            yield return "poh-size";
            yield return "gc-fragmentation";
            yield return "monitor-lock-contention-count";
            yield return "active-timer-count";
            yield return "threadpool-completed-items-count";
            yield return "threadpool-queue-length";
            yield return "threadpool-thread-count";
            yield return "working-set";
        }
    }

    private static void AddHostingCounters(EventCounterCollectionModule module)
    {
        const string EventSourceName = "Microsoft.AspNetCore.Hosting";

        foreach (string eventCounterName in GetCounters())
            module.Counters.Add(new EventCounterCollectionRequest(EventSourceName, eventCounterName));

        static IEnumerable<string> GetCounters()
        {
            yield return "current-requests";
            yield return "failed-requests";
            yield return "requests-per-second";
            yield return "total-requests";
        }
    }

    private static void AddHttpConnectionCounters(EventCounterCollectionModule module)
    {
        const string EventSourceName = "Microsoft.AspNetCore.Http.Connections";

        foreach (string eventCounterName in GetCounters())
            module.Counters.Add(new EventCounterCollectionRequest(EventSourceName, eventCounterName));

        static IEnumerable<string> GetCounters()
        {
            yield return "connections-duration";
            yield return "current-connections";
            yield return "connections-started";
            yield return "connections-stopped";
            yield return "connections-timed-out";
        }
    }

    private static void AddKestrelCounters(EventCounterCollectionModule module)
    {
        const string EventSourceName = "Microsoft-AspNetCore-Server-Kestrel";

        foreach (string eventCounterName in GetCounters())
            module.Counters.Add(new EventCounterCollectionRequest(EventSourceName, eventCounterName));

        static IEnumerable<string> GetCounters()
        {
            yield return "connection-queue-length";
            yield return "connections-per-second";
            yield return "current-connections";
            yield return "current-tls-handshakes";
            yield return "failed-tls-handshakes";
            yield return "request-queue-length";
            yield return "tls-handshakes-per-second";
            yield return "total-connections";
            yield return "total-tls-handshakes";
        }
    }

    private static void AddDnsCounters(EventCounterCollectionModule module)
    {
        const string EventSourceName = "System.Net.NameResolution";

        foreach (string eventCounterName in GetCounters())
            module.Counters.Add(new EventCounterCollectionRequest(EventSourceName, eventCounterName));

        static IEnumerable<string> GetCounters()
        {
            yield return "dns-lookups-requested";
            yield return "dns-lookups-duration";
        }
    }

    private static void AddEntityFrameworkCounters(EventCounterCollectionModule module)
    {
        const string EventSourceName = "Microsoft.EntityFrameworkCore";

        foreach (string eventCounterName in GetCounters())
            module.Counters.Add(new EventCounterCollectionRequest(EventSourceName, eventCounterName));

        static IEnumerable<string> GetCounters()
        {
            yield return "active-db-contexts";
            yield return "total-queries";
            yield return "queries-per-second";
            yield return "total-save-changes";
            yield return "save-changes-per-second";
            yield return "compiled-query-cache-hit-rate";
            yield return "total-execution-strategy-operation-failures";
            yield return "execution-strategy-operation-failures-per-second";
            yield return "total-optimistic-concurrency-failures";
            yield return "optimistic-concurrency-failures-per-second";
        }
    }
}

registriert wird das Ganze im Startup mittels


public static class AspNetCoreMonitoringRegistration
{
    public static IServiceCollection AddMyCSharpAspNetCoreMonitoring(this IServiceCollection services)
    {
        services.AddApplicationInsightsTelemetry();
        services.AddSingleton<ITelemetryInitializer, HttpRequestUserInformationTelemetryInitializer>();

        services.ConfigureTelemetryModule<EventCounterCollectionModule>((module, _) =>
        {
            module.AddMyCSharpEventCounters();
        });

        return services;
    }
}

Metriken vom Forum für .NET 6

In performance-verbesserungen-in-net-6 wurde ja schon allgemein über die Verbesserungen von / mit .NET 6 berichtet, nun wollen wir aber ein Metriken / Charts von unserem Forum anschauen.

In den jeweiligen Diagrammen ist per roter vertikaler Linie der Zeitpunkt des Upgrades von .NET 5 auf .NET 6 gekennzeichnet.
Es gab im dargestellten Zeitraum keine Änderungen am Forum-Code!. D.h. die Effekte beruhen auf Verbesserungen in .NET 6, ASP.NET Core 6 und EF Core 6. Od. anders ausgedrückt: alleine durch ein Upgrade auf .NET 6 bekommt man Leistung geschenkt.

Das Forum an sich ist nicht unbedingt als "CPU-bound" zu sehen, der Großteil ist "IO-bound":* HTTP-Request entgegennehmen

  • Holen von Daten aus der Datenbank
  • HTML-Ausgabe vorbereiten -- hauptsächlich Rendern vom BBCode*
  • HTTP-Response senden

* das ist der einzig wirkliche CPU-bound Teil, aber diese Komponenten haben wir soweit optimiert, dass sind in den CPU-Profiles und auch GC-Profiles nicht mehr so einfach zu finden sind 🙂

Da "IO-bound" dominant ist, geht es bei Performance-Optimierungen v.a. um das Vermeiden von Allokationen, da so der GC weniger Arbeit hat.
Dies wurde in ASP.NET Core 6 und auch in EF Core 6 vorangetrieben, so dass diese Effekte bei uns deutlich zu sehen sind.

Die nachfolgenden Diagramme -- aufgeteilt auf mehrere Beiträge -- der Metriken kommentiere ich nicht gesondert (nur Mini-Anmerkungen), sondern will sie einfach wirken lassen.
Bei Bedarf stellt eine Frage und wir schauen ob und wie wir den Themenpunkt klären können.

Ich plane demnächst* einen weiteren Beitrag mit ein paar statistischen Einblicken ins Forum, wie Verteilung der Anzahl der Beiträge pro Thread, Länge der Beiträge, etc.
Diese Werte spielten bei der Optimierung der Foren-Software eine wichtige Rolle, da so bestimmte Schwellenwerte festgelegt werden konnten.

* kanns leider nicht genauer terminisieren, da ich sehr viel zu tun habe

mfG Gü

03.12.2021 - 10:22 Uhr

Hallo oehrle,

die SAP-DLLs sind bei dir als Content hinzugefügt, du brauchst aber Reference.

Nimm ein ganz neues einfaches Konsolen-Programm, füg dort via VS die Referenz zu SAP-x64 hinzu und schau dir den Abschnitt in der csproj an. Das kopierst du dann in deine csproj und fügst die Conditions (die sind korrekt) entsprechend hinzu.

mfG Gü