Laden...

ASP.NET Core File Read/Write Thread-Safe machen

Erstellt von mleadb vor 2 Jahren Letzter Beitrag vor 2 Jahren 541 Views
M
mleadb Themenstarter:in
10 Beiträge seit 2021
vor 2 Jahren
ASP.NET Core File Read/Write Thread-Safe machen

Mich würde interessieren wie Ihr den Read/Write eurer Dateien (Bilder) in einer ASP.NET Core Anwendung Thread-Safe macht.
Da in ASP.NET Core nun "alles" async und somit sehr performant abläuft benutze ich natürlich die async Varianten von der File-Klasse.
Vor async konnte ich das einfach mit dem ReaderWriterLockSlim absichern.

Leider scheint der AsyncReaderWriterLock vom "Microsoft.VisualStudio.Threading" Package zu einem Deadlock zu führen!? (Ist ja auch nicht für den allgemeinen Gebrauch gedacht)
Der SemaphoreSlim versteht leider kein "EnterReadLock" / "EnterWriteLock" / "EnterUpgradeableReadLock", somit keine alternative.

Möglich wäre natürlich auch das ganze ohne "Locking" laufen zu lassen und bei der Write IOException einfach mehrfach zu "retry"en.
Jedoch scheint mir das bei vielen Reads (FileStreamResult) sehr gewagt zu sein.

Leider finde ich zu diesem Thema sehr wenig Infos 😲

16.842 Beiträge seit 2008
vor 2 Jahren

Async != Parallel.
Das sind zwei unterschiedliche Dinge. Async hat primär nichts mit Performance zutun, sondern ist ein Mechanismus um blockierenden Code zu vermeiden und System-Resourcen besser zu verwenden.
Aber ja, im Falle von ASP.NET bzw. Webanwendungen im Allgemeinen spielt beides eine Rolle.

Ich rate einfach mal, weil Du es nicht genau sagst, dass Du ein stink-normales Dateisystem verwendest und über NTFS-Dateipfade zugreifst.
Diese Art und Weise ist nicht optimal für Webanwendungen, weil - wie Du schon gemerkt hast - solche Dateisysteme nicht wirklich für Webanwendungen gedacht sind.

Für Webanwendungen, insbesondere produktive Anwendungen in größer Umgebung, sollte man entsprechend ausgelegte Dienste für sowas verwenden.
In der Cloud sind das sowas wie CloudFlare R2, Azure Blob Storage, AWS S3. Äquivalente Services zum selbst hosten im eigenen Rechenzentrum sind zB. Cloudian, Dell Elastic Storage, NetApp Storage Grid, min.io.. Diese Systeme nehmen einem so etwas an, da sie i.d.R transaktionssicher sind.
Damit wäre beantwortet, was man generell so für Webanwendungen verwenden sollte 😉

Willst Du sowas selbst bauen, so ist es in .NET mit Bordmitteln möglich solch einen Pipelining Mechanismus zu bauen:
Datenfluss (Task Parallel Library)
Damit lassen sich viele Anfragen auf eine einzelne, transaktionssichere Pipeline bündeln und umsetzen.

Die ursprüngliche Dokumentation der TPL zeigt dabei auch die Bildmanipulation als Beispiel.
MS Docs Previous Version TPL Pipelines

Leider scheint der AsyncReaderWriterLock vom "Microsoft.VisualStudio.Threading" Package zu einem Deadlock zu führen!?

Das Paket Microsoft.VisualStudio.Threading kann für alle Arten von .NET verwendet werden und ist durchaus für den allgemeinen gebraucht gedacht.
Woher ich das weiß? Steht in der Info des Pakets 😉
Ich glaube des weiteren nicht, dass der Deadlock von diesem Paket kommt. Die Wahrscheinlichkeit dürfte bei 99,9999% bei Deinem Code liegen.
Üblicherweise sollte man die Logik und nicht den Schreibvorgang selbst locken, weshalb Du ein Lock mit async i.d.R. mit SemaphoreSlim.WaitAsync umsetzen solltest.


await mySemaphoreSlim.WaitAsync();
try
{
     await WriteMyFileHere();
}
finally
{
     mySemaphoreSlim.Release();
}

Siehe auch: Async/Await - Best Practices in Asynchronous Programming
PS: Das hilft Dir aber nur bei Single Instance Anwendungen. Multi Instance benötigt zwingend einen transaktionale Umsetzung.

Zielführender wäre aber, wenn Du einfach erklären würdest, was denn überhaupt Deine Anforderung ist. 😉
Dann könnte man Dir auch erklären, ob Du überhaupt die richtige Richtung verfolgst, oder wir hier an einem Problem helfen, das es ein Folgefehler ist.
Und wenn man von Code mit Deadlock spricht dann hilft es auch immer, wenn man Quellcode zeigt.

M
mleadb Themenstarter:in
10 Beiträge seit 2021
vor 2 Jahren

Erstmals Danke für deine umfassende Antwort Abt.

Das Problem lag tatsächlich in meiner AsyncDuplicateReaderWriterLock Implementierung.
(Den Code habe ich von Stephen Cleary übernommen und auf den VisualStudio.AsyncReaderWriterLock abgeändert. Siehe hier)


using Microsoft.VisualStudio.Threading;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

public sealed class AsyncDuplicateReaderWriterLock
{
    private sealed class RefCounted<T>
    {
        public RefCounted(T value)
        {
            RefCount = 1;
            Value = value;
        }

        public int RefCount { get; set; }
        public T Value { get; }
    }

    private static readonly Dictionary<object, RefCounted<AsyncReaderWriterLock>> _asyncReaderWriterLocks = new();

    private AsyncReaderWriterLock GetOrCreate(object key)
    {
        RefCounted<AsyncReaderWriterLock>? item;
        lock (_asyncReaderWriterLocks)
        {
            if (_asyncReaderWriterLocks.TryGetValue(key, out item))
            {
                ++item.RefCount;
            }
            else
            {
                item = new RefCounted<AsyncReaderWriterLock>(new AsyncReaderWriterLock(new JoinableTaskContext()));
                _asyncReaderWriterLocks[key] = item;
            }
        }
        return item.Value;
    }

    public async Task<IDisposable> ReadLockAsync(object key)
    {
        await GetOrCreate(key).ReadLockAsync();
        return new Releaser { Key = key };
    }

    public async Task<IDisposable> UpgradeableReadLockAsync(object key)
    {
        await GetOrCreate(key).UpgradeableReadLockAsync();
        return new Releaser { Key = key };
    }

    public async Task<Releaser> WriteLockAsync(object key)
    {
        await GetOrCreate(key).WriteLockAsync();
        return new Releaser { Key = key };
    }

    public sealed class Releaser : IDisposable
    {
        public object Key { get; set; } = null!;

        public void Dispose()
        {
            RefCounted<AsyncReaderWriterLock> item;
            lock (_asyncReaderWriterLocks)
            {
                item = _asyncReaderWriterLocks[Key];
                --item.RefCount;
                if (item.RefCount == 0)
                    _asyncReaderWriterLocks.Remove(Key);
            }
            item.Value.Dispose();
        }
    }
}

Anwendung:


    using (await _lock.WriteLockAsync(key))
    {
        await WriteAllBytesAsync(filePath, content);
    }

Dieser Code führt jedoch zu einem Deadlock.

Habe dann den Code so angepasst bis es funktioniert.


using Microsoft.VisualStudio.Threading;
using System.Collections.Generic;
using System.Linq;

public sealed class AsyncDuplicateReaderWriterLock
{
    private sealed class RefCounted<T>
    {
        public RefCounted(T value)
        {
            RefCount = 1;
            Value = value;
        }

        public int RefCount { get; set; }
        public T Value { get; }
    }

    private static readonly object _lock = new();
    private static Dictionary<object, RefCounted<AsyncReaderWriterLock>> _asyncReaderWriterLocks = new();

    public AsyncReaderWriterLock Create(object key)
    {
        RefCounted<AsyncReaderWriterLock>? item;
        lock (_lock)
        {
            if (_asyncReaderWriterLocks.TryGetValue(key, out item))
            {
                ++item.RefCount;
            }
            else
            {
                item = new(new AsyncReaderWriterLock(new JoinableTaskContext()));
                _asyncReaderWriterLocks[key] = item;
            }

            // Avoid memory leak:
            _asyncReaderWriterLocks = _asyncReaderWriterLocks
                .Where(x => x.Value.RefCount > 0)
                .ToDictionary(x => x.Key, x => x.Value);
        }
        return item.Value;
    }
}

Anwendung:


    using (await _lock.Create(key).WriteLockAsync())))
    {
        await WriteAllBytesAsync(filePath, content);
    }

Ehrlichgesagt ist mir nicht klar, warum die vorherige Implementierung nicht funktionieren soll?!
.Dispose() wird ja eigentlich ausgeführt und somit sollte der Lock ja eigentlich wieder "Released" werden. Mmm...

Nun ja AsyncReaderWriterLock.WriteLockAsync() gibt mir ein Awaitable zurück. Dieses dann "Awaiter GetAwaiter()" und dieser dann "Releaser GetResult()".
Vermutlich müsste ich an diesen Releaser rankommen und diesen dann "Disposen"... (nicht getestet).
Ich dachte eigentlich, dass ein "asyncReaderWriterLockInstance.Dispose" genügen würde...

16.842 Beiträge seit 2008
vor 2 Jahren

Naja, aber wieso schaust Du Dir nur die aus dem Jahr 2015 markierte Lösung an, und nicht die weiteren Beiträge?
Wie man schon in der 2. Klasse bei Tests lernt: Seiten immer erst mal durchlesen, bevor man anfängt.

Ganz am Ende ist ne Lösung, die nur wenige Tage alt ist und den async/await Pattern hinein implementiert hat - wie man auch empfiehtl mit Semaphore.
Hättest Dir also das eigene Zeug sparen können 😉

Hab aber keiner der Snippets validiert (weil ich das halt eh nich so lösen würde).
Hast ja trotz Nachfrage leider auch gar nichts zur Anforderung gesagt, daher..