.Net Core3.1 MVC, Entity Framework, wielowątkowość w kontrolerze

0

Witam,

Mam problem. Nie wiem jak znacząco zwiększyć wydajność przetwarzania danych w aplikacji .NET Core3.1 MVC. Akcja kontrolera czyta z kilku tabel DB do listy tworzonej w pamięci RAM. Dane są przetwarzane w metodach kontrolera w celu obliczenia wyników. Wyniki są zapisywane do tabeli DB.
Problemem jest czas przetwarzania danych ponieważ app pracuje jednowątkowo a rekordów do przetworzenia jest kilkanaście milionów.
Aplikacja działa jednak czas przetwarzania jest nieakceptowalny z powodu wielu milionów rekordów oraz jednowątkowego trybu pracy app.
Próbuje Parallel.ForEach po to aby zwiększyć wydajność poprzez wielowątkowość. Niestety próba użycia kończy się wyjątkiem.
Poniżej kod który więcej powie.

public class AppLottoDBContext: DbContext
{
        public AppDBContext(DbContextOptions<AppDBContext> options) : base(options)
        {
        }
        public DbSet<Model1> Model1 { get; set; }
        public DbSet<Model2> Model2 { get; set; }
        public DbSet<Model3> Model3 { get; set; }
        public DbSet<Model4> Model4 { get; set; }
        public DbSet<Model5> Model5 { get; set; }
        public DbSet<Model6> Model6 { get; set; }
...
}

 public class DataProcessingController : Controller
 {
        private readonly AppLottoDBContext _context;
        
// Ponieważ do czytania tabel DB używane są różne klasy modelu danych więc nie używam dependency injection. 
        public DataProcessingController(AppLottoDBContext context)
        {
            _context = context;
        }

        public async Task<IActionResult> Processing()
        {
            List<FinalResult> finalResultList = new List<FinalResult>();
            FinalResult finalResult = new FinalResult();
            DateTime startTimer = DateTime.Now;

            int amountOfBlocks = 392;
            int block = (int)wieleMilionów / amountOfBlocks;

            for (int n = 0; n < amountOfBlocks; n++)
            {
                // pobieranie z DB danych do przetwarzania porcjami
                var dataBlock = await _context.Model6
                .OrderBy(x => x.N1)
                .ThenBy(x => x.N2)
                .ThenBy(x => x.N3)
                .ThenBy(x => x.N4)
                .ThenBy(x => x.N5)
                .ThenBy(x => x.N6)
                .Skip(n * block)
                .Take(block)
                .ToListAsync();

           // tutaj próba wykorzystania Parallel.ForEAch kończy się wyjątkiem

            Parallel.ForEach (dataBlock, oneSet =>
            {
                var oneSet = dataBlock.ElementAt(0);

                finalResult.Model6ID = oneSet.Model6ID;
                finalResult.AllOne = (ushort)GetTheNumberOfOne(One(oneSet));
                finalResult.AllTwo = (ushort)GetTheNumberOfTwo(Two(oneSet));
                finalResult.AllThree = (ushort)GetTheNumberOfThree(Three(oneSet));
                finalResult.AllFour = (ushort)GetTheNumberOfFour(Four(oneSet));
                finalResult.AllFive = (ushort)GetTheNumberOfFive(Five(oneSet));
                finalResult.AllSix = (ushort)GetTheNumberOfSix(oneSet);

                finalResult.Result = (finalResult.AllSix / 1) +
                                         (finalResult.AllFive / 258) +
                                         (finalResult.AllFour / 13545) +
                                         (finalResult.AllThree / 246820) +
                                         (finalResult.AllTwo / 1851150) +
                                         (finalResult.AllOne / 5775588);
                finalResultList.Add(finalResult);
            });

            try
            {
                _context.UpdateRange(finalResultList);
                _context.SaveChanges();
            }
            catch (DbUpdateConcurrencyException)
            {
                throw;
            }

            finalResultList.Clear();
       }

            var timer = DateTime.Now - startTimer;
            ViewBag.Message = "Koniec przetwarzania " + timer.ToString();

            return View();
        }   

public (byte, byte, byte, byte, byte)[] Five(Model6 model6)
public Tuple<byte, byte, byte, byte>[] Four(Model6 model6)
public Tuple<byte, byte, byte>[] Three(Model6 model6)
public Tuple<byte, byte>[] Two(Model6 model6)
public byte[] One(Model6 model6)

public int GetTheNumberOfSix(Model6 model6)
public int GetTheNumberOfFive((byte, byte, byte, byte, byte)[] five)
public int GetTheNumberOfFour(Tuple<byte, byte, byte, byte>[] four)
public int GetTheNumberOfThree(Tuple<byte, byte, byte>[] three)
public int GetTheNumberOfTwo(Tuple<byte, byte>[] two)
public int GetTheNumberOfOne(byte[] one)
}

Czy ktoś z forumowiczów zna sposób implementacji przetwarzania wielowątkowego w wyżej opisywanym przypadku. Będę wdzięczny za wszystkie merytoryczne sugestie, uwagi.

0
 Parallel.ForEach (dataBlock, oneSet =>
            {
                var oneSet = dataBlock.ElementAt(0);
...

to się w ogóle kompiluje?

0

Słusznie! Nie kompiluje się ponieważ odkomentowałe linie. Linia ta powinna być zakomentowana.

//var oneSet = dataBlock.ElementAt(0);

Po uruchomieniu mam wyjątek.

An unhandled exception occurred while processing the request.
AggregateException: One or more errors occurred. (A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.) (A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.) (A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.) (A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.) (A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.) (A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.) (A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.)
System.Threading.Tasks.TaskReplicator.Run<TState>(ReplicatableUserAction<TState> action, ParallelOptions options, bool stopOnFirstFailure)

2

Z tego co pamiętam kontekst w EF nie jest thread Sade.
Wyjątek jasno Ci mówi, że nie możesz zacząć drugiej operacji na kontekście, dopóki poprzednia się nie skończy.

3

Nie wiem co za wyliczenia robisz na tych rekordach ale chyba lepiej to zrobić na bazie.

2

Tworz DbContext w kazdym paraller foreach

Lepiej:
Uzyj Task.Run, Task.WhenAll

Lepiej
Wrzuc zadanie do kolejki i przetworz je w BackgroundWorkerze czy w jakims nowym odpowiedniku i nie blokuj requesta http, bo timeouty itp

0

Dodam że wiem że jeżeli w kontrolerze używałbym dependency injection to poprzez zmianę w pliku startup.cs czyli parametru ServiceLifetime.Transient

services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")), ServiceLifetime.Transient);

otrzymywałbym pulę połączeń do bazy. Jednak w takim przypadku mógłbym operować tylko jedną konkretna klasę modelu danych.

0

Nie mozesz tak dbcontextu przekazywac https://stackoverflow.com/a/6126693/5381370

1

Tylko właściwie dlaczego mu wywala że robi coś na innym Threadzie jeżeli on w tym Parallel.ForEachu nic nie robi za pomocą EFC?

Dodaje jedynie do listy wyniki, a później po całym ForEachu dodaje to do do bazki, o co chodzi?

0

Problemem wydaje mi sie wielokrotne działanie na DbContext.

możliwe że to będzie rozwiązanie : https://stackoverflow.com/questions/48443567/adddbcontext-or-adddbcontextpool

0

Tak metody Get.... korzystają z DbContext

public int GetTheNumberOfFour(Tuple<byte, byte, byte, byte>[] four)
        {
            ...
            int allFour = 0;

            foreach (var f in four)
            {
                allFour = _context.Model4
                    .Where(x =>
                        x.N1 == f.Item1 &&
                        x.N2 == f.Item2 &&
                        x.N3 == f.Item3 &&
                        x.N4 == f.Item4)
                    .Select(x => x.Amount)
                    .FirstOrDefault();

                allFour += allFour;
            }
            ...
            return allFour;
        }
0
UglyMan napisał(a):

Nie wiem co za wyliczenia robisz na tych rekordach ale chyba lepiej to zrobić na bazie.

Czy procedury składowane będą wykonywane wielowątkowo?

2

Jak na moje oko, to tutaj bottleneckiem (tak jak napisałem wcześniej próbując zgadnąć) jest: N+1 query

Nie możesz wszystkich potrzebnych danych pobrać przed wejściem do tego ParallelFora?? i nie wywoływać w nim w pętli tych SQLi??

Spróbuj zrobić tak, aby te funkcje GetTheNumberOfBlabla otrzymały już gotowe dane, tak aby nie wykonywały żadnego zapytania do bazy lub aby nawet nie miały usinga do EF Core, uczyń je pure.

Śmiem twierdzić, że pozbycie się N+1 query zredukuje czas wykonywania tego o (strzelam se) 80%

1
piotrOlczyk napisał(a):
UglyMan napisał(a):

Nie wiem co za wyliczenia robisz na tych rekordach ale chyba lepiej to zrobić na bazie.

Czy procedury składowane będą wykonywane wielowątkowo?

Wszystko zależy co i jak liczysz i jak to zrobisz. Nie wiem, jaka to baza, ale są na to sposoby, żeby były wielowątkowe. Nie wiem jak potem ładujesz dane do bazy, ale ładowanie kilku milionów w jednej transakcji spowoduje urośniecie loga i tabeli tymczasowych do monstrualnych rozmiarów co też może mieć negatywny wpływ na wydajność. Wisz w ogóle gdzie masz wąskie gardło? Może się okazać, że jest to odczyt i zapis danych. Jak jest to operacja typowo na danych z bazy to powinna być tam zrobiona jak nie potrzebuje komunikować się ze Światem zewnętrznym.

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