Parallel.ForEach z dodawaniem do listy

80

Próbuję uruchomić wiele funkcji, które łączą się ze zdalną lokalizacją (przez sieć) i zwracają listę ogólną. Ale chcę je uruchamiać jednocześnie.

Na przykład:

public static List<SearchResult> Search(string title)
{
    //Initialize a new temp list to hold all search results
    List<SearchResult> results = new List<SearchResult>();

    //Loop all providers simultaneously
    Parallel.ForEach(Providers, currentProvider =>
    {
        List<SearchResult> tmpResults = currentProvider.SearchTitle((title));

        //Add results from current provider
        results.AddRange(tmpResults);
    });

    //Return all combined results
    return results;
}

Jak widzę, wiele wstawień do „wyników” może mieć miejsce w tym samym czasie ... co może spowodować awarię mojej aplikacji.

Jak mogę tego uniknąć?

shaharmor
źródło
Której wersji .NET używasz?
SLL
4
Musiałby to być co najmniej .Net 4; Wprowadzono tam paralelę.
Matt Mills

Odpowiedzi:

58
//In the class scope:
Object lockMe = new Object();    

//In the function
lock (lockMe)
{    
     results.AddRange(tmpResults);
}

Zasadniczo blokada oznacza, że ​​tylko jeden wątek może mieć dostęp do tej krytycznej sekcji w tym samym czasie.

Haedrian
źródło
1
Ale co się stanie, jeśli PODCZAS dodawania tych wyników, spróbują dodać wyniki od innego dostawcy? czy ZAWODĄ, czy CZEKA, aż będzie to możliwe?
shaharmor
4
Kiedy pojawi się blokada, wątek będzie czekał, aż będzie mógł go uzyskać.
Haedrian
Więc w zasadzie to tak, jakby powiedzieć: Poczekaj, aż wyniki.
shaharmor
8
Drobna uwaga: thisnie jest najbezpieczniejszym wyborem dla obiektu zamka. Lepiej użyć specjalnego prywatny obiekt: lock(resultsLock).
Henk Holterman
2
locksmoże jednak spowolnić ogólny czas realizacji .. równoległe kolekcje wydają się lepsze, aby tego uniknąć
Piotr Kula
155

Możesz użyć kolekcji współbieżnej .

Przestrzeń System.Collections.Concurrentnazw udostępnia kilka klas kolekcji bezpiecznych dla wątków, które powinny być używane zamiast odpowiednich typów w przestrzeniach nazw System.Collections i, System.Collections.Genericgdy wiele wątków jednocześnie uzyskuje dostęp do kolekcji.

Możesz na przykład użyć, ConcurrentBagponieważ nie masz gwarancji, w jakiej kolejności pozycje zostaną dodane.

Reprezentuje bezpieczną wątkowo, nieuporządkowaną kolekcję obiektów.

Mark Byers
źródło
Tak, to jest właściwa odpowiedź. Uzyskasz lepszą wydajność (ogólnie) przy jednoczesnych kolekcjach.
lkg
32

Dla tych, którzy wolą kod:

public static ConcurrentBag<SearchResult> Search(string title)
{
    var results = new ConcurrentBag<SearchResult>();
    Parallel.ForEach(Providers, currentProvider =>
    {
        results.Add(currentProvider.SearchTitle((title)));
    });

    return results;
}
Matas Vaitkevicius
źródło
Muszę użyć pętli: foreach (var item in currentProvider.SearchTitle((title))) results.Add(item);
Anthony McGrath
25

Kolekcje współbieżne są nowością w .Net 4; są zaprojektowane do pracy z nową funkcją równoległą.

Zobacz kolekcje współbieżne w .NET Framework 4 :

Przed .NET 4 trzeba było zapewnić własne mechanizmy synchronizacji, jeśli wiele wątków mogło uzyskiwać dostęp do jednej udostępnionej kolekcji. Trzeba było zablokować kolekcję ...

... [...] [nowe] klasy i interfejsy w System.Collections.Concurrent [dodane w .NET 4] zapewniają spójną implementację dla [...] wielowątkowych problemów programistycznych obejmujących współdzielone dane między wątkami.

Matt Mills
źródło
14

Można to zwięźle wyrazić za pomocą PLINQ AsParalleli SelectMany:

public static List<SearchResult> Search(string title)
{
    return Providers.AsParallel()
                    .SelectMany(p => p.SearchTitle(title))
                    .ToList();
}
Douglas
źródło
linq selectMany jest świetny, niestety linq jest wolniejszy niż zwykle foreach. :(
Kugan Kumar
6
Nie mikrooptymalizuj. OP sugerował, że SearchTitlełączy się ze zdalną lokalizacją. Jego opóźnienie będzie o kilka rzędów wielkości wolniejsze niż różnica między LINQ i foreach.
Douglas