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?

orcid.org/0000-0002-6838-2135