WeakReference – weak event pattern

Dziś jak obiecałem, praktyczny przykład wykorzystania WeakReference ( o którym mowa była kilka postów wcześniej). Zdarzenia są częstym powodem memory leak.  Na przykład:

public class ReportViewModel
{
    public void Init(EmployeesViewModel employeesVm)
    {
        employeesVm.PropertyChanged+=EmployeesPropertyChanged;
    }
    private void EmployeesPropertyChanged(object sender,EventArgs e)
    {
        // logika
    }
}

Następnie gdzieś w kodzie tworzymy ReportViewModel, wykonujemy jakieś operację i usuwamy na końcu referencje:

ReportViewModel reports = new ReportViewModel();
reports.Init(employeesViewModel);
// jakies operacje itp.
reports = null

Załóżmy, że nie mamy więcej referencji do ReportViewModel. Mimo to, powyższy kod poskutkuje memory leak – zapomnieliśmy usunąć zdarzenie. Zdarzenie to nic innego jak wskaźnik na funkcje. Jeśli nie usuniemy tego ręcznie wtedy EmployeesViewModel.PropertyChanged będzie wskazywało wciąż na ReportViewModel.  Jeśli jeszcze nie jest to jasne, warto poczytać o wzorcu projektowym obserwator.

Wiemy, że zdarzenie także tworzy referencje, która zapobiega usunięciu obiektu przez GC. W większości przypadków najlepiej samemu sprzątać zasoby. Czasami jednak jest to niemożliwe lub bardzo trudne. Na przykład w WPF wiele kontrolek wykorzystuje WeakReference ponieważ nie wiedzą kiedy obiekt zostanie usunięty z pamięci. Przyjrzyjmy się poniższemu prostemu przykładowi (wersja zaktualizowana, w poprzedniej był poważny błąd znaleziony przez apl):

sealed class WeakEvent
{
   private readonly INotifyPropertyChanged _sourceObject;
   private readonly WeakReference _targetWeakReference;

   public WeakEvent(INotifyPropertyChanged sourceObject,
                       ViewModel target)
   {
       _sourceObject = sourceObject;
       _targetWeakReference = new WeakReference(target);
       sourceObject.PropertyChanged += SourceObjectPropertyChanged;
   }

   void SourceObjectPropertyChanged(object sender, PropertyChangedEventArgs e)
   {
       ViewModel target = (ViewModel)_targetWeakReference.Target;
       if (target != null)
           target.RaisePropertyChanged(e.PropertyName);
       else
       {
           _sourceObject.PropertyChanged -= SourceObjectPropertyChanged;
       }
   }
}
public class SampleClass:INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged;
   public void RefreshProperty(string propName)
   {
       if(PropertyChanged!=null)
           PropertyChanged(this,new PropertyChangedEventArgs(propName));
   }
}

Powyżej został zdefiniowany wrapper na zdarzenie. Referencja do obiektu target jest przechowywana za pomocą weak reference. Dzięki temu, gdy target zostanie usunięty nie wystąpi memory leak:

public class ViewModel
{
    private readonly WeakEvent _weakEvent;

    public ViewModel(SampleClass sampleClass)
    {
        _weakEvent=new WeakEvent(sampleClass,this);
    }
   public void RaisePropertyChanged(string propName)
   {
       
   }
}
//
SampleClass sampleClass=new SampleClass();
ViewModel viewModel = new ViewModel(sampleClass);
sampleClass.RefreshProperty(null);
viewModel = null;
GC.Collect();
sampleClass.RefreshProperty(null);

Gdybyśmy nie mieli WeakReference, metoda RaisePropertyChanged zostałaby wykonana dwukrotnie. W momencie jednak, gdy zerujemy zmienną viewModel i wymuszamy posprzątanie za pomocą GC.Collect(), wywołanie RefreshProperty spowoduje wyłącznie usunięcie jednego handler’a. WeakEvent przechowuje słabe referencje więc jeśli target (obiekt docelowy) zostanie usunięty wtedy w aplikacji nie ma żadnych silnych powiązań. Powyższe rozwiązanie jest dobre kiedy trudno kontrolować samemu zdarzenia. W następnym poście pokaże bardziej generyczną klasę dla WeakReference. WeakEvent jest dość niewygodny w użyciu. W momencie kiedy nie nastąpi żadne wywołanie zdarzenia wtedy WeakEvent będzie powodował memory leak. Jest to tzw. sacrifice object i ma to tylko sens gdy prawdziwy obiekt jest dużo większy niż WeakEvent.

Code review: DispatcherTimer

Dziś miało być o praktycznym przykładzie wykorzystania WeakReference ale stwierdziłem, że najpierw powrócimy znów na chwilę do WPF.  Co myślicie o takiej prostej klasie:

class TimePresenterViewModel:BaseViewModel
{
   private readonly DispatcherTimer _timer;
   const int RefreshTime=6*1000;

   public TimePresenterViewModel()
   {
       _timer=new DispatcherTimer();       
       _timer.Interval = TimeSpan.FromMilliseconds(RefreshTime);
       _timer.Tick += TimerTick;
       _timer.Start();
   }
   void TimerTick(object sender, EventArgs e)
   {
       // jakas logika np.:
       OnPropertyChanged("CurrentTime");
   }
   public string CurrentTime
   {
       get { return DateTime.Now.ToString(); }
   }
}

Następnie gdy użytkownik naciska przycisk, usuwamy obiekt:

public partial class MainWindow : Window
{
   private TimePresenterViewModel _timePresenterViewModel;
   public MainWindow()
   {
       InitializeComponent();
       _timePresenterViewModel=new TimePresenterViewModel();
   }

   private void button_Click(object sender, RoutedEventArgs e)
   {
       _timePresenterViewModel = null;
   }        
}

Wszystko wydaje się OK ale niestety DispatcherTimer spowoduje memory leak. Zajrzyjmy do profiler’a:

image

 

Nie wystarczy usunięcie wszystkich referencji. Łatwo w kodzie o taki błąd ponieważ żadna z naszych referencji nie wskazuje na timer’a a mimo wszystko jest memory leak. Spowodowane jest to wewnętrzną budową timer’a.

Rozwiązaniem jest ręczne zatrzymywanie timer’a przed wyzerowaniem wszystkich referencji.

class TimePresenterViewModel
{
   private DispatcherTimer _timer;
   const int RefreshTime=6*1000;
   
   public void Init()
   {
       if(_timer!=null)
           return;

       _timer = new DispatcherTimer();
       _timer.Interval = TimeSpan.FromMilliseconds(RefreshTime);
       _timer.Tick += TimerTick;
       _timer.Start();
   }
   void TimerTick(object sender, EventArgs e)
   {
         // jakas logika np.:
       OnPropertyChanged("CurrentTime")
   }
   public void Release()
   {
       if(_timer!=null)
       {
           _timer.Stop();
           _timer-=TimerTick;
           _timer=null;
       }
   }
   public string CurrentTime
   {
       get { return DateTime.Now.ToString(); }
   }
}

Sposób użycia:

public partial class MainWindow : Window
{
   private TimePresenterViewModel _timePresenterViewModel;
   public MainWindow()
   {
       InitializeComponent();
       _timePresenterViewModel=new TimePresenterViewModel();
       _timePresenterViewModel.Init();
   }

   private void button_Click(object sender, RoutedEventArgs e)
   {
       _timePresenterViewModel.Release();
       _timePresenterViewModel = null;
   }        
}

Należy zatem pilnować DispatcherTimer’a aby nie okazało się, że tracimy wszystkie referencje a nie wywołaliśmy Stop.

WeakReference – wprowadzenie

Chciałbym poświęcić kilka postów na opisanie WeakReference oraz praktycznych przykładów wykorzystania tej klasy. W dzisiejszym wpisie strona teoretyczna i opis klasy.

Referencje w środowisku .NET można podzielić na słabe (weak references) oraz silne\mocne (strong references).  Silne referencje są wszystkim doskonale znane np:

var sampleClass = new SampleClass();

Innymi słowy jest to standardowy typ powiązania. W momencie gdy wszystkie silne referencje zostaną usunięte wtedy GC może zacząć usuwać taki obiekt z pamięci:

var sampleClass = new SampleClass();
sampleClass = null
// w tej chwili nie ma silnych referencji i GC 
// w odpowiednim czasie usunie zasoby z pamięci.

Słabą referencje można utworzyć wyłącznie za pomocą klasy WeakReference. GC określa obiekt jako niepotrzebny gdy wszystkie silne referencje zostaną usunięte. Nie bierze pod uwagę słabych referencji. Z tego względu jest możliwe posiadanie weak reference do obiektu, który jest uznawany przez GC jako niepotrzebny i może zostać w każdej chwili usunięty. Przykład definiowania słabej referencji do obiektu SampleClass:

WeakReference reference=new WeakReference(new SampleClass("text"));            

var sampleClass = (SampleClass) reference.Target;

if(sampleClass==null)
 Console.WriteLine("Obiekt został usuniety przez GC");
else
 Console.WriteLine("Obiekt wciąż istnieje:{0}",sampleClass.Text);

Weak reference definiuje się poprzez stworzenie instancji klasy WeakReference i przekazanie w konstruktorze danego obiektu. Właściwość Target zawiera wskazywany obiekt. Gdy GC usunięcie obiekt wtedy Target zwraca NULL. W przeciwnym przypadku właściwość zwróci wskazywany obiekt, w naszym przypadku jest to SampleClass.

Poświeciłem kiedyś kilka postów o działaniu GC. Wynika z nich, że GC nie usuwa pamięci natychmiast gdy wszystkie silne referencje zostały wyzerowane.  Nie da się określić momentu w którym pamięć fizycznie zostanie zwolniona na rzecz innego obiektu. Wszystko zależy od wielu czynników jak np. częstość alokowania obiektów czy przede wszystkim aktualne obciążenie pamięci.

Klasa GC ma metodę Collect wymuszającą usunięcie niepotrzebnych elementów. Zwykle odradzam stosowanie tej metody ale na potrzeby dzisiejszego wpisu warto wymusić to aby zobaczyć, że słaba referencja nie jest brana pod uwagę przez GC:

WeakReference reference=new WeakReference(new SampleClass("text"));            

GC.Collect();

var sampleClass = (SampleClass) reference.Target;

if(sampleClass==null)
 Console.WriteLine("Obiekt został usuniety przez GC");
else
 Console.WriteLine("Obiekt wciąż istnieje:{0}",sampleClass.Text);

W tej chwili na ekranie powinna pojawić się wiadomość, że obiekt został usunięty przez GC. WeakReference zawiera również właściwość IsAlive dzięki której możemy sprawdzić czy obiekt nie został usunięty:

WeakReference reference=new WeakReference(new SampleClass("text"));            

GC.Collect();

Console.WriteLine(reference.IsAlive);

Czy to znaczy, że możemy napisać kod następująco?

WeakReference reference=new WeakReference(new SampleClass("text"));            

if(reference.IsAlive)            
    Console.WriteLine(((SampleClass)reference.Target).Text);
else
    Console.WriteLine("Obiekt został usuniety.");

Niestety nie… Gdy IsAlive zwraca false wtedy zawsze mamy  pewność, że obiekt już nie istnieje. Natomiast gdy właściwość zwróci True wtedy powyższy kod  nie jest thread-safe ponieważ co jeśli obiekt zostanie usunięty w momencie odczytu Target (czyli IsAlive zwrócił true a potem natychmiast obiekt został usunięty)? W celu wyjaśnienia problemu dodałem komentarz do powyższego antywzorca:

WeakReference reference=new WeakReference(new SampleClass("text"));            

if(reference.IsAlive) 
//T1: W tej chwili obiekt jeszcze istnieje więc
// przechodzimy dalej.
{
    //T2: Obiekt został w międzyczasie usunięty. 
    //Target zawiera NULL!
    Console.WriteLine(((SampleClass)reference.Target).Text);
}

Adorner w WPF na przykładzie zmiany rozmiaru kontrolki

W WPF Adorner to element pozwalający dołączać do innych kontrolek jakieś efekty graficzne. Na przykład jeśli użytkownik wpiszę nieprawidłową wartość w pole edycyjne, wtedy Adorner może dołączyć do takiego pola ikonkę ostrzegając, że wpisano niepoprawną wartość. Adorner zatem doczepiany jest do jakieś kontrolki a rysowany jest na tzw. AdornerLayer.  Spróbujmy zaimplementować adroner, który po doczepieniu do kontrolki pozwala zmienić jej rozmiar. Zacznijmy od zadeklarowania naszego adorner’a:

public class ResizingAdorner:Adorner
{
   public ResizeAdorner(UIElement adornedElement) : base(adornedElement)
   {
   }
}

Konstruktor przyjmuje kontrolkę do której doczepiony będzie ResizeAdorner umożliwiający zmianę jej rozmiaru.

Następnie musimy dodać 4 małe kwadraciki, które później zostaną rozmieszczone w narożnikach kontrolki. WPF dostarcza klasę Thumb, która jest kontrolką dostarczającą potrzebne zdarzenia. Dzięki Thumb w łatwy sposób będziemy mogli uchwycić zdarzenie kiedy użytkownik klika na wspomniany kwadracik i przesuwa myszką w celu powiększenia lub pomniejszenia kontrolki. Przykład:

public class ResizingAdorner:Adorner
{
   private readonly Thumb[] _thumbs;
   private readonly VisualCollection _visualCollection;
   private const float ThumbSize = 10;

   public ResizeAdorner(UIElement adornedElement) : base(adornedElement)
   {
       _visualCollection=new VisualCollection(this);

       _thumbs=new Thumb[4];
       _thumbs[0] = CreateThumb();
       _thumbs[1] = CreateThumb();
       _thumbs[2] = CreateThumb();
       _thumbs[3] = CreateThumb();

   }
   private Thumb CreateThumb()
   {
       Thumb thumb=new Thumb();
       thumb.Width = thumb.Height = ThumbSize;
       thumb.BorderThickness=new Thickness(1);
       thumb.BorderBrush = Brushes.Black;
       _visualCollection.Add(thumb);

       return thumb;
   }
   protected override Visual GetVisualChild(int index)
   {
       return _visualCollection[index];
   }
   protected override int VisualChildrenCount
   {
       get { return _visualCollection.Count; }
   }   
}

Powyższy kod wyłącznie tworzy 4 thumb’y.  VisualCollection oraz metody GetVisualChild, VisualChildrenCount powinny być znane z poprzedniego postu. Musimy je jednak rozmieścić w czterech narożnikach:

public class ResizingAdorner:Adorner
{
   private readonly Thumb[] _thumbs;
   private readonly VisualCollection _visualCollection;
   private const float ThumbSize = 10;

   public ResizeAdorner(UIElement adornedElement) : base(adornedElement)
   {
       _visualCollection=new VisualCollection(this);

       _thumbs=new Thumb[4];
       _thumbs[0] = CreateThumb();
       _thumbs[1] = CreateThumb();
       _thumbs[2] = CreateThumb();
       _thumbs[3] = CreateThumb();

   }
   private Thumb CreateThumb()
   {
       Thumb thumb=new Thumb();
       thumb.Width = thumb.Height = ThumbSize;
       thumb.BorderThickness=new Thickness(1);
       thumb.BorderBrush = Brushes.Black;
       _visualCollection.Add(thumb);

       return thumb;
   }
   protected override Visual GetVisualChild(int index)
   {
       return _visualCollection[index];
   }
   protected override int VisualChildrenCount
   {
       get { return _visualCollection.Count; }
   }
   protected override Size ArrangeOverride(Size finalSize)
   {
       double controlWidth = AdornedElement.DesiredSize.Width;
       double controlHeight = AdornedElement.DesiredSize.Height;
       
       _thumbs[0].Arrange(new Rect(-ThumbSize / 2, -ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[1].Arrange(new Rect(controlWidth - ThumbSize / 2, -ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[2].Arrange(new Rect(-ThumbSize / 2, controlHeight - ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[3].Arrange(new Rect(controlWidth - ThumbSize / 2, controlHeight - ThumbSize / 2, ThumbSize, ThumbSize));

       return finalSize;
   }
}


ArrangeOverride jest wywoływane gdy nastąpi potrzeba ponownego rozmieszczenia kontrolek – np. gdy rodzic zmieni rozmiar. W ciele metody pozycjonujemy 4 thumb’y do narożników. Gdyby teraz odpalić aplikację i umieścić zaimplementowany adorner na przycisku, to powinniśmy zaobserwować coś takiego:

image

W tej chwili thumb’y nic nie robią – po prostu osiągnęliśmy efekt wizualny. Naszym celem jest jednak umożliwienie zmiany rozmiaru poprzez kliknięcie na jednym z 4 thumbow. Sprawę znacząco uproszcza zdarzenie Thumb.DragDelta:

public class ResizingAdorner:Adorner
{
   private readonly Thumb[] _thumbs;
   private readonly VisualCollection _visualCollection;
   private const float ThumbSize = 10;

   public ResizeAdorner(UIElement adornedElement) : base(adornedElement)
   {
       _visualCollection=new VisualCollection(this);

       _thumbs=new Thumb[4];
       _thumbs[0] = CreateThumb();
       _thumbs[1] = CreateThumb();
       _thumbs[2] = CreateThumb();
       _thumbs[3] = CreateThumb();

       _thumbs[2].DragDelta += BottomLeftThumbDrag;
   }
   private Thumb CreateThumb()
   {
       Thumb thumb=new Thumb();
       thumb.Width = thumb.Height = ThumbSize;
       thumb.BorderThickness=new Thickness(1);
       thumb.BorderBrush = Brushes.Black;
       _visualCollection.Add(thumb);

       return thumb;
   }
   protected override Visual GetVisualChild(int index)
   {
       return _visualCollection[index];
   }
   protected override int VisualChildrenCount
   {
       get { return _visualCollection.Count; }
   }
   protected override Size ArrangeOverride(Size finalSize)
   {
       double controlWidth = AdornedElement.DesiredSize.Width;
       double controlHeight = AdornedElement.DesiredSize.Height;
       
       _thumbs[0].Arrange(new Rect(-ThumbSize / 2, -ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[1].Arrange(new Rect(controlWidth - ThumbSize / 2, -ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[2].Arrange(new Rect(-ThumbSize / 2, controlHeight - ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[3].Arrange(new Rect(controlWidth - ThumbSize / 2, controlHeight - ThumbSize / 2, ThumbSize, ThumbSize));

       return finalSize;
   }
   void BottomLeftThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0,adornedElement.Width - args.HorizontalChange);
       adornedElement.Height = Math.Max(0,args.VerticalChange + adornedElement.Height);
   }
}

DragDelta jest wywoływane gdy użytkownik klika na thumb i rusza myszką. Przesunięcie, które dokonał jest zapisywane we właściwości VerticalChange oraz HortizontalChange. Analogicznie należy obsłużyć pozostałe 3 thumb’y:

public class ResizingAdorner:Adorner
{
   private readonly Thumb[] _thumbs;
   private readonly VisualCollection _visualCollection;
   private const float ThumbSize = 10;

   public ResizeAdorner(UIElement adornedElement) : base(adornedElement)
   {
       _visualCollection=new VisualCollection(this);

       _thumbs=new Thumb[4];
       _thumbs[0] = CreateThumb();
       _thumbs[1] = CreateThumb();
       _thumbs[2] = CreateThumb();
       _thumbs[3] = CreateThumb();

       _thumbs[0].DragDelta += TopLeftThumbDrag;
       _thumbs[1].DragDelta += TopRightThumbDrag;
       _thumbs[2].DragDelta += BottomLeftThumbDrag;
       _thumbs[3].DragDelta += BottomRightThumbDrag;
   }
   private Thumb CreateThumb()
   {
       Thumb thumb=new Thumb();
       thumb.Width = thumb.Height = ThumbSize;
       thumb.BorderThickness=new Thickness(1);
       thumb.BorderBrush = Brushes.Black;
       _visualCollection.Add(thumb);

       return thumb;
   }
   protected override Visual GetVisualChild(int index)
   {
       return _visualCollection[index];
   }
   protected override int VisualChildrenCount
   {
       get { return _visualCollection.Count; }
   }
   protected override Size ArrangeOverride(Size finalSize)
   {
       double controlWidth = AdornedElement.DesiredSize.Width;
       double controlHeight = AdornedElement.DesiredSize.Height;
       
       _thumbs[0].Arrange(new Rect(-ThumbSize / 2, -ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[1].Arrange(new Rect(controlWidth - ThumbSize / 2, -ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[2].Arrange(new Rect(-ThumbSize / 2, controlHeight - ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[3].Arrange(new Rect(controlWidth - ThumbSize / 2, controlHeight - ThumbSize / 2, ThumbSize, ThumbSize));

       return finalSize;
   }
   void BottomLeftThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0,adornedElement.Width - args.HorizontalChange);
       adornedElement.Height = Math.Max(0,args.VerticalChange + adornedElement.Height);
   }
   void BottomRightThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0, adornedElement.Width + args.HorizontalChange);
       adornedElement.Height = Math.Max(0, args.VerticalChange + adornedElement.Height);
   }
   void TopLeftThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0, adornedElement.Width - args.HorizontalChange);
       adornedElement.Height = Math.Max(0, adornedElement.Height - args.VerticalChange);
   }
   void TopRightThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0, adornedElement.Width + args.HorizontalChange);
       adornedElement.Height = Math.Max(0, adornedElement.Height - args.VerticalChange);
   }
}

Kolejne udoskonalenie to zmiana kursora w zależności od tego, który thumb jest pod myszką. Ostateczna wersja gotowa do użycia wygląda następująco:

public class ResizingAdorner:Adorner
{
   private readonly Thumb[] _thumbs;
   private readonly VisualCollection _visualCollection;
   private const float ThumbSize = 10;

   public ResizingAdorner(UIElement adornedElement) : base(adornedElement)
   {
       _visualCollection=new VisualCollection(this);

       _thumbs=new Thumb[4];
       _thumbs[0] = CreateThumb(TopLeftThumbDrag,Cursors.SizeNWSE);
       _thumbs[1] = CreateThumb(TopRightThumbDrag,Cursors.SizeNESW);
       _thumbs[2] = CreateThumb(BottomLeftThumbDrag,Cursors.SizeNESW);
       _thumbs[3] = CreateThumb(BottomRightThumbDrag,Cursors.SizeNWSE);            
   }
   private Thumb CreateThumb(DragDeltaEventHandler dragDeltaEventHandler,Cursor cursor)
   {
       Thumb thumb=new Thumb();
       thumb.Width = thumb.Height = ThumbSize;
       thumb.BorderThickness=new Thickness(1);
       thumb.BorderBrush = Brushes.Black;
       thumb.DragDelta += dragDeltaEventHandler;
       thumb.Cursor = cursor;

       _visualCollection.Add(thumb);

       return thumb;
   }
   protected override Visual GetVisualChild(int index)
   {
       return _visualCollection[index];
   }
   protected override int VisualChildrenCount
   {
       get { return _visualCollection.Count; }
   }
   protected override Size ArrangeOverride(Size finalSize)
   {
       double controlWidth = AdornedElement.DesiredSize.Width;
       double controlHeight = AdornedElement.DesiredSize.Height;
       
       _thumbs[0].Arrange(new Rect(-ThumbSize / 2, -ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[1].Arrange(new Rect(controlWidth - ThumbSize / 2, -ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[2].Arrange(new Rect(-ThumbSize / 2, controlHeight - ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[3].Arrange(new Rect(controlWidth - ThumbSize / 2, controlHeight - ThumbSize / 2, ThumbSize, ThumbSize));

       return finalSize;
   }
   void BottomLeftThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0,adornedElement.Width - args.HorizontalChange);
       adornedElement.Height = Math.Max(0,args.VerticalChange + adornedElement.Height);
   }
   void BottomRightThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0, adornedElement.Width + args.HorizontalChange);
       adornedElement.Height = Math.Max(0, args.VerticalChange + adornedElement.Height);
   }
   void TopLeftThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0, adornedElement.Width - args.HorizontalChange);
       adornedElement.Height = Math.Max(0, adornedElement.Height - args.VerticalChange);
   }
   void TopRightThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0, adornedElement.Width + args.HorizontalChange);
       adornedElement.Height = Math.Max(0, adornedElement.Height - args.VerticalChange);
   }
}

Na koniec przyszedł czas na umieszczenie adorner’a na kontrolce. Najpierw zadeklarujmy przycisk wewnątrz AdornerDecorator:

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="160" Width="397" xmlns:my="clr-namespace:WpfApplication2">
    <AdornerDecorator>
        <Button Name="button" Width="100" Height="50">Hello World</Button>
    </AdornerDecorator>
</Window>

AdornerDecorator umożliwi nam uzyskanie AdornerLayer, który z kolei będzie służył do rysowania adorner’a. W Code-Behind musimy umieścić zaimplementowany adorner na przycisku:

public MainWindow()
{
  InitializeComponent();
  AdornerLayer layer = AdornerLayer.GetAdornerLayer(button);
  layer.Add(new ResizingAdorner(button));
}    

Efekt końcowy:

image

W dość prosty sposób napisaliśmy adorner’a, który z łatwością i bez żadnych modyfikacji można stosować do wszystkich kontrolek. Implementacja jest kompletnie odizolowana od specyficznej kontrolki.

Klasa VisualCollection

Ostatnio było dużo o wielowątkowości więc może zmieńmy temat i zajmijmy się przez dwa posty tematyką WPF. Celem dzisiejszego wpisu jest wyjaśnienie klasy VisualCollection, która będzie niezbędna aby omówić tzw. Adorner. W większości przypadków, XAML w zupełności wystarcza jeśli chodzi o tworzenie interfejsu użytkownika. W WPF istnieje jednak drugie podejście pozwalające rysować elementy i kontrolki. Można manualnie stworzyć kolekcje VisualCollection oraz dodać do niej wszelkie obiekty. Na przykład:

public class SampleControl:Control
{
   private readonly VisualCollection _visualCollection;
   public SampleControl()
   {
       _visualCollection=new VisualCollection(this);
       _visualCollection.Add(CreateButton());
       _visualCollection.Add(CreateRectangle());
   }
   private DrawingVisual CreateRectangle()
   {
       var drawing=new DrawingVisual();
       using (DrawingContext dc = drawing.RenderOpen())
       {
           dc.DrawRectangle(Brushes.Red,null,new Rect(0,0,50,50));
       }
       return drawing;
   }
   private Button CreateButton()
   {
       var button = new Button();
       button.Content = "Hello world";
       button.Width = button.Height = 300;
       return button;
   }
   protected override Visual GetVisualChild(int index)
   {
       return _visualCollection[index];
   }
   protected override int VisualChildrenCount
   {
       get { return _visualCollection.Count; }
   }
}

VisualCollection to jak sama nazwa mówi, zawiera kolekcję obiektów wizualnych. Mogą być to kontrolki oraz shape’y. Konstruktor kolekcji przyjmuje element stanowiący obiekt nadrzędny (rodzic). Następnie należy dodać wszelkie elementy, które chcemy wyświetlać. Niezbędne jest również przeładowanie dwóch metod: GetVisualChild oraz VisualChildrenCount. Pierwsza z nich zwraca element wizualny pod danym indeksem, druga  z kolei zwraca liczbę obiektów, które kontrolka\element zawiera. Jeśli umieścimy tak utworzoną kontrolkę na formatce wtedy wyświetli się przycisk i kwadrat. Oczywiście to samo moglibyśmy zdefiniować za pomocą XAML lub Controls. VisualCollection to nic innego jak lista obiektów do renderowania. W powyższym przypadku stworzyliśmy ją po prostu manualnie.

Powyższy przykład nie ma w praktyce większego sensu. VisualCollection jest jednak bardzo praktyczny w przypadku implementacji Adorner’a  o czym przekonamy się już w następnym poście.

ConcurrentDictionary

W dzisiejszym poście kolejna thread-safe kolekcja – słownik danych. Jeśli musimy modyfikować słownik z kilku wątków naraz wtedy ConcurrentDictionary stanowi doskonały wybór. W przypadku gdy chcemy raz uzupełnić słownik a potem tylko czytać z niego dane, wtedy oczywiście nie ma potrzeby wykorzystywania ConcurrentDictionary. Zacznijmy od spisu najważniejszych metod:

  1. TryAdd – dodawanie nowego elementu.
  2. TryUpdate – aktualizacja wpisu.
  3. TryRemove – usuwanie klucza i wartości.
  4. AddOrUpdate – ciekawy twór. Za jednym razem sprawdza czy klucz istnieje i w zależności od tego aktualizuje wartość lub dodaje nowy klucz wraz z wartością.
  5. GetOrAdd – jeśli element istnieje zwraca go, w przeciwnym razie najpierw tworzy wpis.
  6. Count , ToArray, GetEnumerator  – o tym pisałem już przy okazji omawiania poprzednich struktur danych.  Należy zaznaczyć, że w przypadku słownika GetEnumerator nie tworzy snapshot’a i tym samym możemy spodziewać się dirty reads.

TryAdd próbuje dodać nowy wpis. Jeśli klucz już istnieje, metoda zwróci po prostu false.

var dict = new ConcurrentDictionary<string,int>();
if(dict.TryAdd("key", 1))
{
    // dodano element
}
else
{
   // nie dodano elementu poniewaz juz takowy klucz istnieje
}

Analogicznie wygląda sprawa z TryRemove:

ar dict = new ConcurrentDictionary<string,int>();
int removedValue;
if(dict.TryRemove("key",out removedValue))
{
 // usunieto element
}
else
{
 // nie usunieto wartosci poniewaz takowy klucz nie istnieje
}

Warto zwrócić uwagę, że TryRemove również zwraca usuniętą wartość.

TryUpdate jest trochę bardziej skomplikowany. Oprócz podania klucza oraz nowej wartości musimy wskazać jakiej wartości spodziewamy się w słowniku:

var dict = new ConcurrentDictionary<string,int>();
int newValue = 5;
int expectedPreviusValue = 1;
if(dict.TryUpdate("key",newValue,expectedPreviusValue))
{               
}
else
{

}

Wpis zostanie zaktualizowany wyłącznie w sytuacji w której takowy klucz istnieje oraz wartość w nim zapisana jest równa expectedPreviusValue.

Kolejne metody to połączenia Add z Update itp. AddOrUpdate dodaje nowy element lub aktualizuje aktualny jeśli dany klucz już istnieje. Przykład:

var dict = new ConcurrentDictionary<string, int>();
int newValue=dict.AddOrUpdate(key: "key", addValue: 5, updateValueFactory: (key, value) => value + 1);

Pierwszy parametr to klucz, drugi to wartość jaką chcemy dodać. Ostatni to delegate, który jest wywołany w momencie gdy wpis już istnieje. Reasumując powyższe wywołanie sprawdzi czy “key” istnieje w słowniku. Jeśli tak, zostanie wywołana delegata, która zwiększy wartość o jeden. Jeśli takowego wpisu (“key”) nie ma, wtedy jest on dodawany z wartością 5.  Funkcja zwraca wartość jaka została użyta (w naszym przypadku 5 albo ta z delegaty).

GetOrAdd zwraca element a jeśli on nie istnieje to najpierw go dodaje:

var dict = new ConcurrentDictionary<string, int>();
int value = dict.GetOrAdd("key", 5);

Drugie przeładowanie pozwala przekazać wskaźnik na jakąś metodę, która zostanie wykonana w przypadku gdy wpis nie istnieje. Pozwala to w sposób thread-safe wygenerować wartość wyłącznie wtedy gdy nie ma jej w słowniku:

var dict = new ConcurrentDictionary<string, int>();
int value = dict.GetOrAdd("key", (key) => GetValue());

Powyższe operacje są bezpieczne jeśli chodzi o zakleszczenia, zagłodzenie, livelock oraz sekcję krytyczną. Ze względów na optymalizacje nie są one atomowe. W praktyce oznacza to, że metoda, która pierwsza została wykonana wcale nie musi się jako pierwsza zakończyć. Operacje wewnątrz np. AddOrUpdate nie są atomowe. GetOadd wywołuje m.in. delegate w przypadku gdy wpis nie istnieje i należy go wygenerować. W przypadku gdy 2 wątki wywołują GetOrAdd może się zdarzyć, że wątek A wywoła delegate ponieważ wpis nie istnieje a wątek B w tym czasie już zrobi to samo i zakończy działanie przed wątkiem A. Wtedy wątek A,  mimo, że wywołał delegate, zwróci wartość wygenerowaną przez wątek B.

Po mimo kilku dziwnych zachować, kolekcja jest bardzo praktyczna. Szczególnie fajne są metody typu AddorUpdate. Używając zwykłego słownika zawsze trzeba najpierw sprawdzać klucz a tutaj API dostarcza dwie najczęściej używane operacje na raz.

Wielowątkowe ConcurrentQueue oraz ConcurrentStack

Dziś kolejny post o kolekcjach, które mogą być używanie swobodnie w środowisku współbieżnym. Tak jak ConcurrentBag, obiekty przedstawione w tym poście również są mocno zoptymalizowane. ConcurrentQueue to po prostu kolejka. Przykład:

ConcurrentQueue<int> queue = new ConcurrentQueue<int>();
queue.Enqueue(5);
queue.Enqueue(6);

int result;
if(queue.TryDequeue(out result))
{
 Console.WriteLine(result);
}
if (queue.TryPeek(out result))
{
 Console.WriteLine(result);
}

Kolejka posiada 3 najważniejsze  metody. Dodawanie nowych elementów odbywa się za pomocą Enqueue. TryPeek zwraca element bez jego usunięcia z kolei TryDequeue ma podobną strukturę i zachowanie z tym, że usuwa element z kolejki. Jak widać na przykładzie, sam sposób użycia jest analogiczny do zwykłej kolejki.

Korzystanie ze stosu  nie powinno stwarzać problemów:

ConcurrentStack<int> stack = new ConcurrentStack<int>();
stack.Push(5);
stack.Push(6);

int result;
if(stack.TryPeek(out result))
{
    Console.WriteLine(result);
}
if (stack.TryPop(out result))
{
    Console.WriteLine(result);
}

Ponadto można zwrócić ze stosu kilka elementów za pomocą tablicy i metody TryPopRange:

ConcurrentStack<int> stack = new ConcurrentStack<int>();
stack.Push(5);
stack.Push(6);

int[] elements=new int[2];

stack.TryPopRange(elements);
Console.WriteLine(elements[0]);
Console.WriteLine(elements[1]);

Analogicznie można dodać kilka elementów naraz:

int[] elements=new int[2];

stack.PushRange(elements);

Bardzo dobrą informacją o przedstawionych wyżej 2 kolekcjach jest fakt, że nie używają one blokad ani nawet spinning’u. Wszystko odbywa się za pomocą Interlocked – klasy dostarczającej operacje atomowe.

Podobnie jak w przypadku ConcurrentBag do dyspozycji mamy właściwości Count, IsEmpty oraz metodę Clear. Myślę, że nazwy mówią same za siebie. Należy jednak pamiętać, że w środowisku współbieżnym nie ma sensu porównywać Count z liczbą, ponieważ jest to operacja nieatomowa. Podobnie z IsEmpty – nie ma pewności, że za chwile stos nie będzie pusty. Obie kolekcje wspierają foreach, z tym, że jest to tylko snapshot danych. Jeśli w momencie wykonywania pętli stos lub kolejka zostaną zmodyfikowane, nie zostanie to odzwierciedlone.

Wprowadzenie do współbieżnych kolekcji danych na przykładzie ConcurrentBag

Dziś po długim wprowadzeniu teoretycznym, mającym na celu wyjaśnienie “zaawansowanych” mechanizmów synchronizacji czas przyszedł na pokazanie pierwszej struktury danych. Przed pojawieniem się asynchronicznych kolekcji, najczęściej korzystało się z prostego lock’a jak:

lock(_Sync)
{
    _list.Add(newElement);
}

Rozwiązanie mało wygodne i przede wszystkim niewydajne. Nowe kolekcje zawierają mechanizmy synchronizacji omówione w poprzednich postach takie jak:

  1. SpinLock
  2. SpinWait
  3. SemaphoreSlim
  4. CountdownEvent

Jeśli nie są one znane, zachęcam do przeczytania najpierw poprzednich postów. Jak widać z powyższej listy, synchronizacja kolekcji polega głównie na użyciu mechanizmu Spinning, który dla szybkich operacji jest dużo bardziej wydajny,  ponieważ nie wymaga zmiany kontekstu. W przypadku długotrwałych operacji, mechanizmy synchronizacji są na tyle sprytne, że potrafią przejść ze stanu spinning do waiting. Ponadto pewne struktury danych takie jak ConcurrentQueue(T) albo ConcurrentStack(T) kompletnie nie korzystają z lock’ow – wszystko wykonują za pomocą operacji atomowych Interlocked.

Przejdźmy teraz do pierwszej kolekcji – ConcurrentBag. ConcurrentBag służy do przechowywania dowolnych obiektów i nadaje się do scenariuszy gdzie kolejność przechowywania nie ma znaczenia. W przeciwieństwie do zbiorów danych, możliwe jest przechowywanie kilku takich samych obiektów.  Struktura stanowi więc “torbę” na elementy, która zawiera nieuporządkowane elementy. Wewnętrzna implementacja jest zoptymalizowania pod kątem wielowątkowości. Istnieje wewnętrzna lista ThreadLocalList, która przechowuje oddzielne dane dla każdego wątku. Dzięki temu w większości przypadków nie musimy używać locków czy spinning, ponieważ każdy z wątków operuje na oddzielnej liście. W momencie gdy wątek A, nie maj już elementów na swojej liście wtedy przechodzi do listy innego wątku i tym samym będzie musiała nastąpić synchronizacja (za pomocą spinning a potem jeśli zajdzie potrzeba to zostanie założona zwykła blokada). Z tego względu ConcurrentBag zawiera nieuporządkowane elementy – w końcu nie mamy jednej listy a wszystkie elementy są wewnętrznie rozdzielane na podlisty przyporządkowane do każdego wątku. Zgodnie z MSDN, kolekcja jest optymalizowana dla przypadków gdzie ten sam wątek produkuje i konsumuje te same elementy – dlatego koncepcja podlist jest bardzo wydajna. Zaznaczyłem, że najkorzystniejsza sytuacja jest gdy każdy wątek operuje na własnej liście ThreadLocalList.  W przypadku gdy już wyczerpał wszystkie elementy znajdujące się na niej, wtedy sytuacja się komplikuje. Mimo to, Microsoft wprowadził optymalizacje jeśli w danej liście jest więcej niż 3 elementy. Wtedy pierwszy wątek czyta elementy na początku listy a drugi od końca. Wniosek z tego jest taki, że synchronizacja ma miejsce tylko wtedy gdy jakaś lista ma mniej niż 3 elementów i dwa wątki muszą na tej podliście operować!

Przykład:

ConcurrentBag<int> numbers = new ConcurrentBag<int>();
numbers.Add(5);
numbers.Add(1);

int number1;
numbers.TryTake(out number1);

int number2;
numbers.TryTake(out number2);

Console.WriteLine(number1);
Console.WriteLine(number2);

Prosty przykład pokazuje dwie podstawowe metody: Add do dodawania nowych elementów oraz TryTake do ich zwracania. TryTake zwraca wartość bool określającą czy element udało się zwrócić z lity. Z kolei za pomocą argumentu OUT zwraca obiekt z pojemnika – w naszym przypadku będzie to liczba całkowita. Gdybyśmy wywołali TryTake trzeci raz wtedy zwróciła by ona wartość false a number przyjąłby default ( T ).

TryTake usuwa element z listy. Istnieje metoda TryPeek, która tylko zwraca element, bez jego usunięcia (podgląda go):

ConcurrentBag<int> numbers = new ConcurrentBag<int>();
numbers.Add(5);
numbers.Add(1);            

int number1;
numbers.TryPeek(out number1);

int number2;
numbers.TryPeek(out number2);

Console.WriteLine(number1);
Console.WriteLine(number2);

Kod zwróci dwa razy tą samą wartość  1 ponieważ w przeciwieństwie do TryTake, TryPeek nie modyfikuje listy.

Add, TryTake oraz TryPeek to najważniejsze metody ConcurrentBag. Oprócz tego istnieją dwie przydatne właściwości:

  1. IsEmpty
  2. Count

Umożliwiają sprawdzenie czy lista jest pusta oraz zwrócenie liczby aktualnie przechowanych elementów. Należy jednak uważać na Count np.:

if(bag.Count == 0)
{

}

Powyższy kod jest niebezpieczny.  Istnieje ryzyko, że pojemnik zostanie zmodyfikowany przez inny wątek natychmiast po zwróceniu liczby elementów (przed porównaniem z zero). Z tego względu wprowadzono właściwość IsEmpty, która zwraca liczbę elementów a następnie porównuje ją z wartością zero za pomocą thread-safe operacji.Mimo to, nie mamy gwarancji, że po zwróceniu IsEmpty coś nie będzie dodane tym samym czyniąc naszą logikę błędną.

ConcurrentBag implementuje interfejs IEnumerable , zatem można użyć pętli foreach:

ConcurrentBag<int> numbers = new ConcurrentBag<int>();
numbers.Add(5);
numbers.Add(1);

foreach (var number in numbers)
{
    Console.WriteLine(number);
}

Ponadto nie musimy się martwić czy numbers zostało zmodyfikowane przez inny wątek. Operacja jest w pełni thread-safe. Jeśli byłaby to kolekcja List wtedy modyfikacja kolekcji spowodowałby wyjątek. Należy zaznaczyć, że elementy dodane w trakcie wykonywania foreach nie będą widziane. Innymi słowy, w momencie rozpoczęcia foreach, wykonywana jest kopia elementów. Dzięki temu mamy gwarancję, że nic z tymi elementami w trakcie nie stanie się, ale niestety nowych elementów nie będziemy widzieć – wyłącznie te, która istniały już w momencie wykonywania wspomnianej kopii.

Struktura SpinWait i synchronizacja bez użycia blokad

W ostatnim poście pisałem o różnych mechanizmach opartych o Spin. Zachęcam do przejrzenia ostatnich wpisów ponieważ bez tego trudno będzie zrozumieć dzisiejszy post.

SpinWait jest strukturą, w której najważniejsza metoda to SpinOnce. SpinOnce przez pierwsze 10 wywołań wykonuje  klasyczny Spin (patrz poprzednie posty) dzięki czemu nie musimy obawiać się koszty związanego z uśpieniem wątku, zmianą kontekstu itp. SpinOnce jest jednak na tyle inteligentny, że po 10 wywołaniach zmienia swoje zachowanie:

  1. Po 10 wywołaniach SpinOnce zaczyna używać metody Thread.Yield (patrz ostatni wpis), która oddaje wątek z powrotem do CPU i umożliwia zaplanowanie nowego wątku, o wyższym lub takim samym priorytecie, wyłącznie na tym samym procesorze. Yield będzie wywoływany za każdym razem z wyjątkiem  co piątego i dwudziestego wywołania SpinOnce.
  2. Thread.Sleep ( 0 ) – podobnie jak Thread.Yield z tym, że wątek, może być wykonany na dowolnym rdzeniu (nie tylko na tym samym procesorze). Thread.Sleep ( 0 ) jest wywołany co piąty raz po 10 wywołaniu Spinonce.
  3. Thread.Sleep ( 1 ) – oddanie wątku na rzez innego o dowolnym priorytecie, wykonywanym na dowolnym procesorze. Sleep(1) wywołany jest co dwudziesty raz po 10 wykonaniu SpinOnce.

A co dokładnie dzieje się przez 10 wywołaniem? SpinOnce wykonuje Thread.SpinWait( N ) przekazując jako parametr najpierw 4, potem 8 a kończąc przy ostatnim, 10 wykonaniu na 2048.

Tym sposobem można zaimplementować wolną od blokad strukturę danych (źródło MSDN):

public class LockFreeStack<T>
{
    private volatile Node m_head;

    private class Node { public Node Next; public T Value; }

    public void Push(T item)
    {
        var spin = new SpinWait();
        Node node = new Node { Value = item }, head;
        while (true)
        {
            head = m_head;
            node.Next = head;
            if (Interlocked.CompareExchange(ref m_head, node, head) == head) break;
            spin.SpinOnce();
        }
    }
    public bool TryPop(out T result)
    {
        result = default(T);
        var spin = new SpinWait();

        Node head;
        while (true)
        {
            head = m_head;
            if (head == null) return false;
            if (Interlocked.CompareExchange(ref m_head, head.Next, head) == head)
            {
                result = head.Value;
                return true;
            }
            spin.SpinOnce();
        }
    }
}

Przyjrzymy się metodzie Push. Push ma za zadanie dołączyć nowy element na początku listy jednokierunkowej. Musimy zatem ustawić wskaźnik Next na aktualny początek (head) listy a następnie nowy początek listy ustawić na nowy element. Wszystko jest wykonywane w pętli while. Jeśli operację Push wykonuje wyłącznie jeden wątek, całość skończy się po jednej iteracji. W przypadku gdy mamy kolizje i dwa wątki wykonały Push wtedy zostanie wykonanych więcej iteracji. Interlocked.CompareExchange ma za zadanie ustawić początek listy (m_head) na nowo dodany element node. Interlocked jest oczywiście operacją atomową więc porównanie m_head z head oraz w przypadku gdy są one równe, ustawienie m_head na node  zostanie wykonane w jednej, atomowej operacji (bezpiecznej z punktu widzenia współbieżności). W przypadku gdy drugi wątek zmienił w międzyczasie m_head, wtedy CompareExchange zwróci false  i SpinOnce zostanie wywołany . Po krótkim przeczekaniu, wszystkie operacje zaczynają się od nowa. W praktyce powyższy algorytm jest szybszy niż lock. W sytuacjach gdy tylko jeden wątek wywołuje Push wtedy oczywiście widać największą przewagę Push nad implementacji z lock.