2013-01-24 11 views
6

Próbuję wprowadzić wzór Parallel.ForEach i śledzić postępy, ale brakuje mi czegoś związanego z blokowaniem. Poniższy przykład zlicza do 1000, gdy threadCount = 1, ale nie wtedy, gdy threadCount> 1. Jaki jest prawidłowy sposób to zrobić?Jak poprawnie wykonać Parallel.ForEach, blokowanie i raportowanie postępu

class Program 
{ 
    static void Main() 
    { 
     var progress = new Progress(); 
     var ids = Enumerable.Range(1, 10000); 
     var threadCount = 2; 

     Parallel.ForEach(ids, new ParallelOptions { MaxDegreeOfParallelism = threadCount }, id => { progress.CurrentCount++; }); 

     Console.WriteLine("Threads: {0}, Count: {1}", threadCount, progress.CurrentCount); 
     Console.ReadKey(); 
    } 
} 

internal class Progress 
{ 
    private Object _lock = new Object(); 
    private int _currentCount; 
    public int CurrentCount 
    { 
     get 
     { 
     lock (_lock) 
     { 
      return _currentCount; 
     } 
     } 
     set 
     { 
     lock (_lock) 
     { 
      _currentCount = value; 
     } 
     } 
    } 
} 

Odpowiedz

16

Zwykle problem z wywołaniem coś podobnego count++ z wielu wątków (które podzielają zmienną count) jest to, że ta sekwencja zdarzeń może się zdarzyć:

  1. nawlec czyta wartość count. Wątek B odczytuje wartość count.
  2. Gwint A zwiększa inkrement kopii lokalnej.
  3. Wątek B inkrementuje jego kopię lokalną.
  4. Wątek A zapisuje zwiększoną wartość z powrotem na count.
  5. Wątek B zapisuje zwiększoną wartość z powrotem na count.

W ten sposób wartość zapisana przez wątek A zostanie zastąpiona przez wątek B, więc wartość zostanie zwiększona tylko raz.

Twój kod dodaje blokady wokół operacji 1, 2 (get) i 5, 6 (set), ale to nic nie robi, aby zapobiec problematycznej sekwencji zdarzeń.

Co trzeba zrobić, aby zablokować całą operację, tak że podczas wątek A jest zwiększający wartość, gwint B nie może uzyskać do niego dostęp w ogóle:

lock (progressLock) 
{ 
    progress.CurrentCount++; 
} 

Jeśli wiesz, że tylko Ty potrzebujesz inkrementacji, możesz stworzyć metodę na Progress, która to hermetyzuje.

+0

Dzięki, dokładnie to, co się dzieje. Dodałem dodatkową metodę Postępu, aby zwiększyć licznik z zastosowanym blokowaniem. –

+0

Myślę, że zmienna progressLock powinna być "postęp"? – eschneider

0

oświadczenia usunięcie blokady z właściwości i zmodyfikować główny korpus:

object sync = new object(); 
     Parallel.ForEach(ids, new ParallelOptions {MaxDegreeOfParallelism = threadCount}, 
         id => 
          { 
           lock(sync) 
           progress.CurrentCount++; 
          }); 
+0

Czy możesz wyjaśnić, dlaczego? – svick

+0

Myślę, że instrukcja lock w treści pętli jest atomowa. Instrukcja lock we właściwościach jest oddzielona, ​​ponieważ propery jest skompilowana w metodzie. – chameleon86

+0

Ponieważ operator inkrementacji obejmuje zarówno wywołanie gettera, jak i ustawiającego - x ++ jest skrótem dla x = x + 1 - więc blokada jest pobierana przed odczytaniem i zwolnieniem wartości po jej aktualizacji. Ale nadal uważam, że pole będzie lepsze. :) –

0

Kwestia jest taka, że ​​++ nie jest atomowy - jeden wątek może czytać i zwiększyć wartość między innym wątku odczytu wartości i je przechowywanie (teraz niepoprawnej) zwiększonej wartości. Jest to prawdopodobnie spotęgowane faktem, że jest twoja własność owijająca twoje int.

np.

Thread 1  Thread 2 
reads 5   . 
.    reads 5 
.    writes 6 
writes 6!  . 

Zamki wokół setter i getter nie pomagają tym, jak nie ma nic, aby zatrzymać lock blokuje themseves miano poza kolejnością.

Zazwyczaj sugeruję użycie Interlocked.Increment, ale nie można tego używać z właściwością.

Zamiast tego można wyeksponować _lock i mieć blok lock wokół połączenia progress.CurrentCount++;.

+0

Wydaje mi się, że wystawienie zamka jest złym pomysłem, ponieważ oznacza to, że każdy może go użyć, co może łatwo doprowadzić do zakleszczenia. Myślę, że lepszym rozwiązaniem byłoby posiadanie blokady bezpośrednio w 'Main()'. – svick

+0

Jeśli masz kod gdzie indziej, który robi coś innego z 'int', łatwiej jest ten kod blokować ten sam obiekt, niż próbować udostępnić oddzielny obiekt blokady między wszystkim. – Rawling

1

Najprostszym rozwiązaniem byłoby rzeczywiście zostały zastąpić nieruchomość z pola, i

lock { ++progress.CurrentCount; } 

(ja osobiście wolę wygląd preincrement nad postincrement jako „++”. Starcia rzecz mój umysł, ale oczywiście postincrement działałby tak samo.)

Miałoby to dodatkową zaletę zmniejszania kosztów ogólnych i rywalizacji, ponieważ aktualizacja pola jest szybsza niż wywoływanie metody aktualizującej pole.

Oczywiście zamykanie go jako własności może również przynieść korzyści. IMO, ponieważ składnia pola i właściwości jest identyczna, JEDYNA zaletą korzystania z właściwości nad polem, gdy właściwość jest automatycznie implementowana lub równoważna, jest sytuacja, w której można wdrożyć jeden zespół bez konieczności budowania i wdrażania zależnych od nowa. W przeciwnym razie możesz użyć szybszych pól! Jeśli zajdzie potrzeba sprawdzenia wartości lub dodania efektu ubocznego, wystarczy przekształcić to pole na właściwość i ponownie zbudować. Dlatego w wielu praktycznych przypadkach nie ma kary za korzystanie z pola.

Żyjemy jednak w czasach, w których wiele zespołów programistycznych działa dogmatycznie i używa narzędzi takich jak StyleCop do egzekwowania ich dogmatyzmu. Takie narzędzia, w przeciwieństwie do programistów, nie są wystarczająco inteligentne, aby ocenić, kiedy użycie pola jest akceptowalne, więc niezmiennie "reguła, która jest wystarczająco prosta, aby nawet sprawdzić w StyleCop" staje się "enkapsulować pola jako właściwości", "nie używać pól publicznych" et cetera ...

15

Stare pytanie, ale myślę, że jest lepsza odpowiedź.

Możesz raportować postępy, używając Interlocked.Increment(ref progress) w ten sposób, że nie musisz się martwić o blokowanie operacji zapisu w celu postępu.

0

Lepiej jest przechowywać dowolną operację bazy danych lub systemu plików w lokalnej zmiennej buforowej zamiast ją blokować. blokowanie zmniejsza wydajność.

Powiązane problemy