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.
Pokaż/Ukryj kod klasy Philosopher
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?
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; } } }
namespace PhilosopherProblemWithDataFlows { public partial class Form1 : Form { private readonly Color EatingColor = Color.Red; private readonly Color ThinkingColor = Color.Green; private readonly List<Label> _stateLabels = new List<Label>(); private readonly List<Philosopher> _philosophers = new List<Philosopher>(); private readonly BufferBlock<PhilosopherState> _philosophersState = new BufferBlock<PhilosopherState>(); public Form1() { InitializeComponent(); Closing += (sender, args) => { _philosophersState.Complete(); _philosophers.ForEach(p => p.GoHome()); }; _stateLabels.Add(philosopher1); _stateLabels.Add(philosopher2); _stateLabels.Add(philosopher3); _stateLabels.Add(philosopher4); _stateLabels.Add(philosopher5); _stateLabels.ForEach(l => l.BackColor = ThinkingColor); Start(); } private void Start() { _philosophersState.LinkTo(new ActionBlock<PhilosopherState>(state => UpdateState(state)), new DataflowLinkOptions()); var chopsticks = new[] { new BufferBlock<Chopstick>(), new BufferBlock<Chopstick>(), new BufferBlock<Chopstick>(), new BufferBlock<Chopstick>(), new BufferBlock<Chopstick>() }; foreach (var ch in chopsticks) ch.Post(new Chopstick()); for (var i = 0; i < 5; ++i) _philosophers.Add(new Philosopher( i, chopsticks[i], chopsticks[(i + 1) % 5], philosophersState)); for (var i = 0; i < 5; ++i) { var th = new Thread(_philosophers[i].TakeASeat); th.Start(); } } private void UpdateState(PhilosopherState state) { var label = _stateLabels[state.Index]; label.Invoke((MethodInvoker)delegate { label.BackColor = state.IsEating ? EatingColor : ThinkingColor; }); } } }
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?