Jakiś czas temu na
blogu Piotrka Zielińskiego przeczytałem o TPL Dataflow Library czyli o bibliotece dostarczającej komponentów ułatwiających komunikację (przekazywanie danych) w środowisku wielowątkowym. Temat mnie zaciekawił i postanowiłem trochę pobawić się z tą technologią. Na tapecie nie miałem żadnego "prawdziwego" projektu, w którym dałoby się wykorzystać nową zabawkę, postanowiłem więc wykonać ćwiczenie umysłowe i rozwiązać klasyczny problem pięciu filozofów z użyciem TPL Dataflow.
W moim rozwiązaniu każda pojedyncza pałeczka do jedzenia ryżu reprezentowana jest przez instancję klasy
BufferBlock<T> gdzie
T to w tym przypadku klasa
Chopstick (klasa wydmuszka, nie zawiera żadnych właściwości ani metod).
BufferedBlock<T>to nic innego jak kolejka
FIFO, która może mieć wielu czytelników i wielu zapisujących.
Filozof potrzebuje jednak dwóch pałeczek aby rozpocząć jedzenie. Aby spełnić to wymaganie używam klasy
JoinBlock<T,Z> gdzie T i Z do znowu klasa
Chopstick.
JoinBlock działa w ten sposób, ze monitoruje dwa źródła danych i jeśli w obu źródłach równocześnie są dane to grupuje je i wysyła do słuchacza. W tym przypadku
JoinBlock czeka na dwie wolne pałeczki.
var chopsticks = new JoinBlock<Chopstick, Chopstick>(new GroupingDataflowBlockOptions { MaxNumberOfGroups = 1 });
_left.LinkTo(chopsticks.Target1);
_right.LinkTo(chopsticks.Target2);
_chopsticks = chopsticks.Receive();
Ustawienie właściwości
MaxNumberOfGroups jest konieczne, aby blok odczytał tylko dwa komunikaty. Odłożenie pałeczek na stół jest natomiast równoważne z wysłaniem komunikatu (pałeczki) z powrotem do bufora tak, aby oczekujący na nie filozofowie mogli rozpocząć jedzenie.
_left.SendAsync(_chopsticks.Item1);
_right.SendAsync(_chopsticks.Item2);
Do tego, aby filozofowie mogli informować świat zewnętrzny o tym, co robią, również użyłem klasy
BufferBlock<T>. Za każdym razem kiedy jeden z filozofów kończy/rozpoczyna jedzenie wysyła komunikat ze swoim aktualnym stanem. Ja napisałem prostą aplikację w WinForms, która nasłuchuje na te komunikaty i odpowiednio uaktualnia UI.
private readonly BufferBlock<PhilosopherState> _philosophersState = new BufferBlock<PhilosopherState>();
...
_philosophersState.LinkTo(new ActionBlock<PhilosopherState>(state => UpdateState(state)), new DataflowLinkOptions());
Każdy filozof modelowany jest przez instancję klasy
Philosopher i działa w swoim własnym wątku. Co jakiś losowy czas decyduje, co robić dalej tj.: kontynuować myślenie/jedzenie czy rozpocząć myślenie/jedzenie. Kiedy zbierzemy to wszystko do kupy, otrzymamy następujący kod.
Pokaż/Ukryj kod klasy Philosopher
namespace PhilosopherProblemWithDataFlows
{
public class Philosopher
{
private const int SleepTime = 100;
private readonly int _index;
private readonly BufferBlock<Chopstick> _left;
private readonly BufferBlock<Chopstick> _right;
private readonly BufferBlock<PhilosopherState> _philosophersState;
private bool _goHome;
private Tuple<Chopstick, Chopstick> _chopsticks;
public Philosopher(int index, BufferBlock<Chopstick> left, BufferBlock<Chopstick> right, BufferBlock<PhilosopherState> philosophersState)
{
_index = index;
_left = left;
_right = right;
_philosophersState = philosophersState;
}
public void TakeASeat()
{
var rand = new Random((int)DateTime.Now.Ticks);
while (true)
{
if (_goHome)
{
PutChopsticks();
return;
}
if (rand.Next() % 2 == 0)
Eat();
else
Think();
Thread.Sleep((rand.Next(10) + 1) * SleepTime);
}
}
public void GoHome()
{
_goHome = true;
}
private void Eat()
{
if (_chopsticks == null)
{
var chopsticks =
new JoinBlock<Chopstick, Chopstick >(new GroupingDataflowBlockOptions { MaxNumberOfGroups = 1 });
_left.LinkTo(chopsticks.Target1);
_right.LinkTo(chopsticks.Target2);
_chopsticks = chopsticks.Receive();
chopsticks.Complete();
}
_philosophersState.SendAsync(new PhilosopherState { Index = _index, IsEating = true });
}
private void Think()
{
PutChopsticks();
_philosophersState.SendAsync(new PhilosopherState { Index = _index, IsEating = false});
}
private void PutChopsticks()
{
if (_chopsticks != null)
{
_left.SendAsync(_chopsticks.Item1);
_right.SendAsync(_chopsticks.Item2);
_chopsticks = null;
}
}
}
public class Chopstick
{
}
public class PhilosopherState
{
public int Index { get; set; }
public bool IsEating { get; set; }
}
}
Pokaż/Ukryj kod okna Win Forms
Kod designer'a pominąłem bo jest trywialny i zawiera tylko 5 etykiet o nazwach philosopher1, philosopher2 itd.
Na koniec mała zagadka. Moja implementacja zawiera pewne uproszczenie oryginalnego problemu 5 ucztujących filozofów. Jakie?