Zapis do jednego pliku przez wiele Tasków

1

Posiadam odpalonych kilkanaście tasków, które nieustannie zapisują dane do kilku plików, po kilka razy na sekundę każdy. Próbuję znaleźć najlepsze rozwiązanie dla uniknięcia błędów związanych z dostępem do pliku.

Wstępnie sprawdzałem czy plik jest gotowy do zapisu:

 private void SaveToFile(string line, string file)
        {
            if (File.Exists(file))
            {
                while (!IsFileReady(file))
                {
                    await Task.Delay(5);
                }
            }

            File.AppendAllText(file, line + Environment.NewLine);
        }
        private bool IsFileReady(string file)
        {
            try
            {
                using (FileStream inputStream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.None))
                    return inputStream.Length > 0;
            }
            catch (Exception)
            {
                return false;
            }
        }

Jednak zdarzały się błędy, a mi osobiście trochę ciężko się to debuguje.

Następnie znalazłem poniższe rozwiązanie tutaj: https://stackoverflow.com/a/41262326/12603542

private ReaderWriterLock _locker = new ReaderWriterLock();

private void SaveToFile(string line, string file)
        {
            try
            {
                _locker.AcquireWriterLock(10000); //You might wanna change timeout value 
                File.AppendAllText(file, line + Environment.NewLine);
            }
            finally
            {
                _locker.ReleaseWriterLock();
            }
        }

Jednak jeszcze dobrze go nie przetestowałem i ciekaw jestem czy macie jakieś inne sprawdzone podejścia?

1

Pierwsze chyba nie powinno działać -- i jak piszesz -- nie działa.

Drugie jest oparte na blokowaniu (file locking) plików i rokuje nadzieje. :)

Takie rzeczy robi się właśnie blokowanie plików -- rodzaj semafora, który jest na danym pliku ustawiony (i jest to albo na poziomie systemu operacyjnego, albo biblioteki, albo w końcu można zrobić samemu, co zwykle źle działa :)).

Natomiast mam pytanie -- co zrobi Twoja funkcja SaveToFile z drugiego przykładu, gdy plik będzie zablokowany? Wydaje się, że po cichu nic nie zapisze i o tym się nawet nie dowiesz... :/

4

Może niech twoje taski wrzucają dane do jakiejś kolejki i niech jeden dedykowany Task ściaga z niej i zapisuję do pliku?

0
koszalek-opalek napisał(a):

Natomiast mam pytanie -- co zrobi Twoja funkcja SaveToFile z drugiego przykładu, gdy plik będzie zablokowany? Wydaje się, że po cichu nic nie zapisze i o tym się nawet nie dowiesz... :/

Zaktualizowałem pierwszy przykład, gdzie kod się też znajduje w funkcji SaveToFile. I właściwie to masz rację, po 10s metoda przejdzie dalej. W pierwszym przykładzie po dłuższym czasie z jakiegoś powodu kod się zapętlał w while, nawet gdy plik był zwalniany przez inny Task, chciałem tego uniknąć w drugim przykładzie.

kzkzg napisał(a):

Może niech twoje taski wrzucają dane do jakiejś kolejki i niech jeden dedykowany Task ściaga z niej i zapisuję do pliku?

Myślałem o tym rozwiązaniu, ale odpada, ponieważ każdy Task pobiera też dane z tych plików, z ostatniej linii, więc muszą czekać na ich nadpisanie.

0
bakunet napisał(a):
kzkzg napisał(a):

Może niech twoje taski wrzucają dane do jakiejś kolejki i niech jeden dedykowany Task ściaga z niej i zapisuję do pliku?

Myślałem o tym rozwiązaniu, ale odpada, ponieważ każdy Task pobiera też dane z tych plików, z ostatniej linii, więc muszą czekać na ich nadpisanie.

A to nie ma szans. :)

Jeśli dobrze rozumiem, Każdy Twój task musi czekać na to, co będzie w pliku, odczytać to i potem zapisać? W tej sytuacji, to tym bardziej jedyne rozwiązanie to zaproponowane przez @kzkzg -- wtedy ten task piszący będzie też mógł zwrócić innym taskom, co tam ostatnio się w pliku znalazło.

Ale wygląda na to, że nie potrzebujesz tak do końca czegoś takiego jak opisałeś, lecz jakiejś formy komunikacji między taskami... Napisz więc szerzej, bo mi to zaczyna wyglądać na problem XY: https://typeofweb.com/problem-xy-czyli-gdy-nie-wiemy-o-co-pytamy/

0

Więc dokładniej: Mam kilkanaście tasków, które wykonują zadania z daną z kolekcji. Każde zadanie ma inny czas wykonania, niektóre są wykonane szybciej, inne wolniej. Przed zaczęciem każdego zadania task zapisuje w pliku który indeks kolekcji danych wejściowych pobiera. Po wykonaniu zadania zapisuje wynik w pliku, tym samym co pozostałe taski też zapisują wyniki, i pobiera numer indeksu ostaniej wykonywanej iteracji przez dowolny z tasków, zaczyna wykonywać z kolejnym indeksem kolekcji itd. Tak więc każde kolejne wykonywane zadanie musi znać numer indeksu ostatniego zaczętego. Zapis do pliku jest potrzebny na wypadek wyłączenia aplikacji.

1

@bakunet: To jeszcze pytanie pomocnicze -- jakie wyłączenie aplikacji masz na myśli? Normalne (wtedy można taki zapis oprogramować na jakimś onexit czy tym podobnym zdarzeniu/wyjątku) czy brak zasilania (wtedy i tak Ci tracisz ostatnie zmiany w pliku, bo były jeszcze w buforze systemowym)?

W obu przypadkach potrzebny Ci jakiś task master, który z jednej strony nadzoruje, co mają robić pozostałe wątki, a z drugiej zapisuje do pliku co jest ukończone (zamykając plik od razu, żeby nie tracić za dużo danych w razie awarii).

0

@koszalek-opalek: Brzmi dobrze, choć nie mam jeszcze pojęcia jak stan miałby być zapisany w razie utraty zasilania / systemu. Czy przez zamykając plik od razu masz na myśli dopisywanie / nadpisywanie na bieżąco? A na myśli miałem przewidziane i nieprzewidziane wyłączenie aplikacji, co by nie wykonywać ponownie całej pracy w wypadku gdy Windows zdecyduje że jest teraz najlepsza pora na aktualizację i restart systemu :)

0

w programie pod linuxem w podobnym celu jest użyty syslog ale nie wiem czy jest jakiś odpowiednik dla windows

2

Jeśli zdecydowałeś się na użycie ReaderWriteLocka to radziłbym użyć ReaderWriterLockSlim. Ale w tym przypadku użycia nie ma to wielkiego sensu, bo nie używasz go w kontekście read/write to którego był tworzony a tylko blokujesz zasób do zapisu. Szkoda więc tracić czas na overhead jaki narzuca użycie tej klasy, lepiej (szybciej) będzie to działać przy użyciu Monitora (który też można timeoutować w razie potrzeby, tak jak jest w twoim kodzie).

Ale może lepiej byłoby podejsć do tego w inny sposób i skorzystać z jakiegoś bufora do którego wszystkie zainteresowane wątki mogłyby ładować dane a ten co jakiś czas byłby zrzucany do pliku. Tak chyba działają bilblioteki logujące (nie mam pewności bo nie analizowałem ich źródeł, może mają jeszcze lepsze podejście).

Btw, z użyciem mechanizmu jaki zaprezentowałeś to musisz uważać. Jeśli chcesz blokować coś w aplikacji wielowątkowej to musisz pamiętać o tym, że żeby ReaderWriterLock coś rzeczywiście blokował to musi być współdzielony przez wszystkie wątki, w podanym przykładzie wygląda to tak że niestety nie jest (chyba że cała klasa obsługująca ten zapis jest odpowiednio użyta albo zarejestrowana w DI) i raczej nie wygląda na to że zadziała w sposób jakiego oczekujesz.

0

@var: Pomysł z buforem ciekawy, co jakiś czas można by wykonywać zrzut do plików danych + indeksu. A wątki pobierały by wartość indeksu z pamięci. To by mnie zadowoliło w 100%. A co do blokowania zapisu pliku, to bez obawy, wszystkie wątki widzą tego lockera.

2

A po Co ten plik? Bo jeśli tylko do komunikacji między taskami to można to lepiej zrobić.

1

@jacek.placek: ten plik jest po to, żeby w razie niespodziewanego shutdownu nie utracić postępu

0

Przetestuj jakąś thread safe kolekcję do przechowywania danych w trakcie pracy programu. https://docs.microsoft.com/pl-pl/dotnet/standard/collections/thread-safe/
Do pliku (lokalnej bazy?) zapisuj dane już przetworzone przez te wątki. Może jakiś osobny wątek do zapisywanie tej kolekcji thread safe do pliku albo jakiś timer zapisujący kolekcję.

1

@gswidwa1: takie coś najlepiej robić przez bazę danych.

  1. Jeden task produkuje kolejkę
  2. Drugi task odczytuje z kolejki ale nie usuwa (!)
  3. Przetwarza task
  4. Robi w transakcji zapis wyniku i usunięcie zadania
    Jeżeli coś pójdzie nie tak zadanie zostanie w kolejce. Jeżeli zadania nie ma to na pewno jest wynik bo to gwarantuje transakcja bazodanowa
0

Obczaj czy Hangfire nie jest tym czego potrzebujesz.

0

Do takiego zadania użyłbym loggera, np. NLog.
Sprawdzone, wydajne, przetestowane.
Dodatkowo nie tylko wiele tasków może zapisywać do tego jednego pliku, ale również wiele procesów.

Oczywiście można zrobić usługę implementującą jakiś interfejs aby ukryć tą implementację.

0

Jeżeli wątków jest tylko kilkanaście to może niech każdy wątek zapisuje po prostu do oddzielnego pliku w wydzielonym folderze i na koniec się je połączy wszystkie 😂

1 użytkowników online, w tym zalogowanych: 0, gości: 1