Równoległe wywołanie różnych interfejsów ze wspólnym dbContext

0

hej, mam pewien problem z DBContextem w entity
jeden z serwisów ma kilka metod do synchronizacji - każda z nich komunikuje się z innym api, następnie używajac wspólnego dbContextu (wstrzykniętego w konstruktor) zapisuje dane.

Rodzi to pewien problem, gdybym chciał wywołać wszystkie te metody równolegle - mam wtedy konflikt, ze dbcontext jest już użyty w innym wątku
Najprostsze rozwiązanie, to zmienić działanie metod, aby tylko pobierały dane z API, a a na końcu leciał foreach i zapis po kolei (nierównolegle) - ale czy jest może jakaś inna możliwość?

dbContext rejestrruje przez services.AddDbContext(...)

4

Możesz wstrzykiwać DbContextFactory i potem tworzyć naraz tyle DbContextów ile jednoczesnych operacji potrzebujesz.

5

Ja bym to robił tak, że te metody do komunikacji powinny się wykonywać równolegle (tam nie wstrzykujesz dbcontext) i czekasz az wszystkie pobiorą dane a potem dopiero odpalasz dbcontext. Tylko te metody do synchronizacji nie powiiny robić niż innego jak pobierać dane.

Np masz Handler co dostaje DBContext oraz listę Serwisów do komunikacji. Odpalasz wszystkie serwisy i robisz Task.WhenAll. I potem wyniki zapisujesz.

1

Rejestruj DbContext jako transient, np:

services.AddDbContext<MyContext>(ServiceLifetime.Transient);
0

Mógłbyś dać tutaj jakiś przykładowy kod gdzie występuje problem? Bo robiłem już najgłupsze dziwactwa jak się uczyłem ef core i nie zdarzyło mi się to 😁 Może trzeba po prostu inaczej do tego podejść

1

no przykładowo



class SomeClass
{
readonly DbContext _ctx
(DbContext ctx) => _ctx  = ctx;

async Task Method1()
{
 _ctx.SomeCollection.Add(entity);
 await _ctx.SaveChangesAsync();
}
async Task Method2()
{
 _ctx.SomeCollection.Add(entity);
 await_ctx.SaveChangesAsync();
}

async Task Synchronize()
{
 var tasks = new Task[]
{
 ()=> Method1(),
 ()=> Method2()
};

await Task.WhenAll(tasks);

}
}

nie wiem czy to zadziala, tam mam dosc duze dane (po 10k rekordow na metode), ale chodzi o to, ze w tym samym momencie nastapi zapis na tym samym dbcontexcie (rownolegle) to wyleci

BTW. nie wiem czy to ma znaczenie - ja do tego doszedłem na testach - nie próbowałem na żywo - wiec uzywalem tutaj InMemory dla sql servera!

5

Równoległe dodawanie wielu danych do tej samej tabeli może spowolnić proces (eskalacja locków po stronie sql servera). Rozważ wielowątkowe zgromadzenie danych, a potem AddRange i zapis na jednym kontekście (vide link) albo zapis paczkami (jeden worker produkuje dane, drugi worker zapisuje je). Możesz też tworzyć wiele kontekstów via DbContextFactory, tak jak poradził @snowflake2137..

1

Nie próbowałem nigdy czegoś takiego, nie było mi potrzebne. Ale poszperałem w necie i może ten link Cię zainteresuje:
https://learn.microsoft.com/en-us/answers/questions/44125/c-multiple-thread-connect-to-one-sql-server-databa

1

Można jeszcze użyć lock-a:

class SomeClass
{
    readonly DbContext _ctx
    (DbContext ctx) => _ctx = ctx;

    async Task Method1()
    {
        lock (_ctx)
        {
            _ctx.SomeCollection.Add(entity);
            await _ctx.Save();
        }
    }
    async Task Method2()
    {
        lock (_ctx)
        {
            _ctx.SomeCollection.Add(entity);
            await_ctx.Save();
        }
    }

    async Task Synchronize()
    {
        var tasks = new Task[]
       {
 ()=> Method1(),
 ()=> Method2()
       };

        await Task.WhenAll(tasks);

    }
}

6
Wilktar napisał(a):

Można jeszcze użyć lock-a:

No dobra, a taki await w lock to właściwie co ma zrobić? Albo raczej co lock ma zrobić z awaitowanym kodem wewnątrz siebie?
Poważnie pytam, bo ja sobie tego ani wyobrazić ani nawet rozrysować nie umiem. Goście z MS chyba też nie... No chyba, że to po prostu jakiś automatyczny deadlock ma być. Ale to wtedy można po prostu dodać taki keyword do języka, żeby człowiek się sam nie musiał męczyć z pisaniem tego.

1

Sorry pośpieszyłem się w tym poście, usunąłem Async z nazwy metody ale nie usunąłem await. @somekind masz rację tak to nie zadziała :)

Zamiast:

  await_ctx.Save();

Powinno być:

  _ctx.SaveChanges();

Przy czym to ma sens jeżeli operację pobierania z innych serwisów jest faktycznie długotrwałą operacją, wtedy jest jakiś zysk wykonywania pobierania asynchronicznie a zapisu synchronicznie.

0

mam duzo async/await, wiec czy dobrym rozwiazaniem byłoby np dodac semafor:

private SemaphoreSlim _repoSemaphore = new SemaphoreSlim(1, 1);

i przed kazdym zapisanem do bazy (na poziomie repo)
robic :

await _repoSemaphore.WaitAsync();
try
{
 await _dbcontext.items.addrangeasync(..):
 await _dbcontext.savechangesasync();
} 
finally
{
_repoSemaphore.Release();
}

?

jak sprobówalem to dziala :) tylko, ze szczezrze mowiac pierwszy raz mam stycznosc z semaforami, dlatego wole spytac czy to jest ok

1

Na poziomie repo to zły pomysł, taki semafor to kolejna operacja do wykonania która obniża czas wykonania (Nigdy nie sprawdzałem ile to czasu). W większości przypadków ta operacja będzie zbędna.

Widzę to tak, jeżeli tylko w jednej metodzie kontrolera potrzebujesz się odwołać do dwóch zewnętrznych serwisów to użycie semafora na poziomie kontrolera ma sens, ale jeżeli ten problem pojawia się częściej to dużo lepszym rozwiązaniem jest DbContextFactory - bo wtedy te wszystkie operację będą wykonywane asynchronicznie.

0

jest to wykorzystywane tylko przy synchronizacji (tj. backgroundowcy proces sobie chodzi co X minut i patrzy czy powinien robić synchro) - takie coś pozwala mi uniknąć większego refaktora no i dzieje sie tylko przy samym zapisie (dane z zew. źródła dalej pobierane są równolegle) - wiec i tak jest lepiej, bo do tej pory było : pobierz dane-> zapisz -> pobierz nastepne dane -> zapisz (i tak z 10 razy) co potrafiło trwać 2 minuty, teraz to jakies 15 sekund (to tylko miara na oko, ale widac róznice)

2
heyyou napisał(a):

jak sprobówalem to dziala :) tylko, ze szczezrze mowiac pierwszy raz mam stycznosc z semaforami, dlatego wole spytac czy to jest ok

W przypadku kodu asynchronicznego i wielu wątków, to semafor to chyba jedyne wyjście. Tylko pytanie, jaki to wszystko ma sens, i gdzie tu zysk z asynchroniczności?

1

Zysk jest taki że wszystko co jest poza sekcją krytyczną wykonuje się asynchronicznie.

    class SemaphoreTest
    {
        private SemaphoreSlim SemaphoreSlim = new SemaphoreSlim(1, 1);
       
        async Task Method1()
        {
            await Task.Delay(TimeSpan.FromSeconds(10));
            //var data = SomeService.GetData();

            await SemaphoreSlim.WaitAsync();
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(1));
                //_ctx.SomeCollection.Add(entity);
                //await _ctx.Save();
            }
            finally
            {
                SemaphoreSlim.Release();
            }
        }

        async Task Method2()
        {
            await Task.Delay(TimeSpan.FromSeconds(10));
            //var data = SomeService.GetData();

            await SemaphoreSlim.WaitAsync();
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(1));
                //_ctx.SomeCollection.Add(entity);
                //await _ctx.Save();
            }
            finally
            {
                SemaphoreSlim.Release();
            }
        }

        public async Task Synchronize()
        {
            Stopwatch stopwatch = Stopwatch.StartNew(); 
            var tasks = new Task[] { Method1(), Method2() };

            await Task.WhenAll(tasks);
            Console.WriteLine(stopwatch.Elapsed);
        }
    }

Czas wykonania to około 12 sekund a nie 22.

1
Wilktar napisał(a):

Zysk jest taki że wszystko co jest poza sekcją krytyczną wykonuje się asynchronicznie.

To zupełnie tak samo jakby mieć locka i synchroniczną sekcję krytyczną ;)

Przy czym nie mówię żeby tak robić, bo synchroniczne operacje IO całkowicie zablokują wątek.

1

Tak :), ale jak masz lock to nie możesz użyć await, a jak masz repozytorium w którym wszystkie metody są Async np:

        Task<List<EntityType>> GetAllAsync();

        Task<EntityType?> GetByIdAsync(IdType id);

        Task AddAsync(EntityType entity);

        Task RemoveAsync(EntityType entity);

        Task UpdateAsync(EntityType entity);

to musisz przerabiać kod.

Korzystając z semafora - nie musisz przerabiać kodu (dodajesz tylko semapfor) i zyskujesz częściową asynchroniczność.

0

Tylko w środku locka nie możesz użyć await. Poza lockiem możesz nawet jak masz metodę async i inne operacje asynchroniczne.

1

@Wilktar:

Wilktar napisał(a):

Tak :), ale jak masz lock to nie możesz użyć await, a jak masz repozytorium w którym wszystkie metody są Async np:

Tak to się właśnie kończy jak się robi overengenering w pastaci REPO i EntityFramework :P

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.