From e06c47ff0a73eafb9cb216912d9d9ce8974f454f Mon Sep 17 00:00:00 2001 From: Beatriz Stollnitz Date: Mon, 5 Aug 2019 18:12:03 -0700 Subject: [PATCH] Added 31st post. --- .../ChangesMultithreading/App.xaml | 9 + .../ChangesMultithreading/App.xaml.cs | 17 + .../ChangesMultithreading.csproj | 89 +++++ .../ChangesMultithreading.sln | 20 + .../ChangesMultithreading.suo | Bin 0 -> 19456 bytes .../Properties/AssemblyInfo.cs | 54 +++ .../Properties/Resources.cs | 70 ++++ .../Properties/Resources.resx | 117 ++++++ .../Properties/Settings.cs | 42 +++ .../Properties/Settings.settings | 7 + .../ChangesMultithreading/Window1.xaml | 15 + .../ChangesMultithreading/Window1.xaml.cs | 352 ++++++++++++++++++ .../Images/31ChangesMultithreading.png | Bin 0 -> 14550 bytes 31-ChangesMultithreading/README.md | 216 +++++++++++ 14 files changed, 1008 insertions(+) create mode 100644 31-ChangesMultithreading/ChangesMultithreading/App.xaml create mode 100644 31-ChangesMultithreading/ChangesMultithreading/App.xaml.cs create mode 100644 31-ChangesMultithreading/ChangesMultithreading/ChangesMultithreading.csproj create mode 100644 31-ChangesMultithreading/ChangesMultithreading/ChangesMultithreading.sln create mode 100644 31-ChangesMultithreading/ChangesMultithreading/ChangesMultithreading.suo create mode 100644 31-ChangesMultithreading/ChangesMultithreading/Properties/AssemblyInfo.cs create mode 100644 31-ChangesMultithreading/ChangesMultithreading/Properties/Resources.cs create mode 100644 31-ChangesMultithreading/ChangesMultithreading/Properties/Resources.resx create mode 100644 31-ChangesMultithreading/ChangesMultithreading/Properties/Settings.cs create mode 100644 31-ChangesMultithreading/ChangesMultithreading/Properties/Settings.settings create mode 100644 31-ChangesMultithreading/ChangesMultithreading/Window1.xaml create mode 100644 31-ChangesMultithreading/ChangesMultithreading/Window1.xaml.cs create mode 100644 31-ChangesMultithreading/Images/31ChangesMultithreading.png create mode 100644 31-ChangesMultithreading/README.md diff --git a/31-ChangesMultithreading/ChangesMultithreading/App.xaml b/31-ChangesMultithreading/ChangesMultithreading/App.xaml new file mode 100644 index 0000000..45259f9 --- /dev/null +++ b/31-ChangesMultithreading/ChangesMultithreading/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/31-ChangesMultithreading/ChangesMultithreading/App.xaml.cs b/31-ChangesMultithreading/ChangesMultithreading/App.xaml.cs new file mode 100644 index 0000000..64b35f9 --- /dev/null +++ b/31-ChangesMultithreading/ChangesMultithreading/App.xaml.cs @@ -0,0 +1,17 @@ +using System; +using System.Windows; +using System.Data; +using System.Xml; +using System.Configuration; + +namespace ChangesMultithreading +{ + /// + /// Interaction logic for App.xaml + /// + + public partial class App : System.Windows.Application + { + + } +} \ No newline at end of file diff --git a/31-ChangesMultithreading/ChangesMultithreading/ChangesMultithreading.csproj b/31-ChangesMultithreading/ChangesMultithreading/ChangesMultithreading.csproj new file mode 100644 index 0000000..42fa3ca --- /dev/null +++ b/31-ChangesMultithreading/ChangesMultithreading/ChangesMultithreading.csproj @@ -0,0 +1,89 @@ + + + Debug + AnyCPU + {7C19EE40-623C-4849-A404-1D3BDACB3884} + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + ChangesMultithreading + ChangesMultithreading + 4 + winexe + 3.0 + true + Web + true + Foreground + 7 + Days + false + false + false + 1.0.0.* + true + true + Publish\ + + + true + full + false + .\bin\Debug\ + DEBUG;TRACE + + + false + true + .\bin\Release\ + TRACE + + + + + + + + + + + + + + + + + + + + + App.xaml + Code + + + Window1.xaml + Code + + + + + + ResXFileCodeGenerator + Resources.cs + Designer + + + True + Resources.resx + + + SettingsSingleFileGenerator + Settings.cs + + + True + Settings.settings + + + + + + \ No newline at end of file diff --git a/31-ChangesMultithreading/ChangesMultithreading/ChangesMultithreading.sln b/31-ChangesMultithreading/ChangesMultithreading/ChangesMultithreading.sln new file mode 100644 index 0000000..26614a5 --- /dev/null +++ b/31-ChangesMultithreading/ChangesMultithreading/ChangesMultithreading.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 9.00 +# Visual Studio 2005 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChangesMultithreading", "ChangesMultithreading.csproj", "{7C19EE40-623C-4849-A404-1D3BDACB3884}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7C19EE40-623C-4849-A404-1D3BDACB3884}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C19EE40-623C-4849-A404-1D3BDACB3884}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C19EE40-623C-4849-A404-1D3BDACB3884}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C19EE40-623C-4849-A404-1D3BDACB3884}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/31-ChangesMultithreading/ChangesMultithreading/ChangesMultithreading.suo b/31-ChangesMultithreading/ChangesMultithreading/ChangesMultithreading.suo new file mode 100644 index 0000000000000000000000000000000000000000..0346bb457a3bf476bedbf7af7282ead9e9fd2ace GIT binary patch literal 19456 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 in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.*")] diff --git a/31-ChangesMultithreading/ChangesMultithreading/Properties/Resources.cs b/31-ChangesMultithreading/ChangesMultithreading/Properties/Resources.cs new file mode 100644 index 0000000..174f04c --- /dev/null +++ b/31-ChangesMultithreading/ChangesMultithreading/Properties/Resources.cs @@ -0,0 +1,70 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:2.0.50727.42 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ChangesMultithreading.Properties +{ + using System; + using System.IO; + using System.Resources; + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the Strongly Typed Resource Builder + // class via a tool like ResGen or Visual Studio.NET. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + class Resources + { + + private static System.Resources.ResourceManager _resMgr; + + private static System.Globalization.CultureInfo _resCulture; + + /*FamANDAssem*/ + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + public static System.Resources.ResourceManager ResourceManager + { + get + { + if ((_resMgr == null)) + { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Resources", typeof(Resources).Assembly); + _resMgr = temp; + } + return _resMgr; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + public static System.Globalization.CultureInfo Culture + { + get + { + return _resCulture; + } + set + { + _resCulture = value; + } + } + } +} diff --git a/31-ChangesMultithreading/ChangesMultithreading/Properties/Resources.resx b/31-ChangesMultithreading/ChangesMultithreading/Properties/Resources.resx new file mode 100644 index 0000000..3e18af9 --- /dev/null +++ b/31-ChangesMultithreading/ChangesMultithreading/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/31-ChangesMultithreading/ChangesMultithreading/Properties/Settings.cs b/31-ChangesMultithreading/ChangesMultithreading/Properties/Settings.cs new file mode 100644 index 0000000..f3edc8d --- /dev/null +++ b/31-ChangesMultithreading/ChangesMultithreading/Properties/Settings.cs @@ -0,0 +1,42 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:2.0.50727.42 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ChangesMultithreading.Properties +{ + public partial class Settings : System.Configuration.ApplicationSettingsBase + { + private static Settings m_Value; + + private static object m_SyncObject = new object(); + + public static Settings Value + { + get + { + if ((Settings.m_Value == null)) + { + System.Threading.Monitor.Enter(Settings.m_SyncObject); + if ((Settings.m_Value == null)) + { + try + { + Settings.m_Value = new Settings(); + } + finally + { + System.Threading.Monitor.Exit(Settings.m_SyncObject); + } + } + } + return Settings.m_Value; + } + } + } +} diff --git a/31-ChangesMultithreading/ChangesMultithreading/Properties/Settings.settings b/31-ChangesMultithreading/ChangesMultithreading/Properties/Settings.settings new file mode 100644 index 0000000..4024694 --- /dev/null +++ b/31-ChangesMultithreading/ChangesMultithreading/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/31-ChangesMultithreading/ChangesMultithreading/Window1.xaml b/31-ChangesMultithreading/ChangesMultithreading/Window1.xaml new file mode 100644 index 0000000..19b1f64 --- /dev/null +++ b/31-ChangesMultithreading/ChangesMultithreading/Window1.xaml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/31-ChangesMultithreading/ChangesMultithreading/Window1.xaml.cs b/31-ChangesMultithreading/ChangesMultithreading/Window1.xaml.cs new file mode 100644 index 0000000..652bb90 --- /dev/null +++ b/31-ChangesMultithreading/ChangesMultithreading/Window1.xaml.cs @@ -0,0 +1,352 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; +using System.Collections.ObjectModel; +using System.Windows.Threading; +using System.ComponentModel; +using System.Threading; +using System.Collections; +using System.Diagnostics; + + +namespace ChangesMultithreading +{ + public partial class Window1 : Window + { + Thread workerThread1; + Thread workerThread2; + BeginInvokeOC beginInvokePlaces; + InvokeOC invokePlaces; + ObservableCollection throwPlaces; + object lockObject; + + public Window1() + { + InitializeComponent(); + lockObject = new object(); + } + + // Avalon will throw if it receives a collection change notification + // from a collection that was changed by a different thread. + private void Throw_Click(object sender, RoutedEventArgs e) + { + throwPlaces = new ObservableCollection(); + AddPlaces(throwPlaces); + lb.ItemsSource = throwPlaces; + lb.DisplayMemberPath = "Name"; + workerThread1 = new Thread(new ThreadStart(CrashMe)); + workerThread1.Start(); + } + + void CrashMe() + { + throwPlaces.RemoveAt(0); + } + + // An attempt to solve the problem: use the UI thread's dispatcher + // to delegate collection changes to the UI thread. The exception + // is gone and it seems to work at first sight. + private void DelegateUIThread_Click(object sender, RoutedEventArgs e) + { + beginInvokePlaces = new BeginInvokeOC(lb.Dispatcher); + AddPlaces(beginInvokePlaces); + lb.ItemsSource = beginInvokePlaces; + lb.DisplayMemberPath = "Name"; + workerThread1 = new Thread(new ThreadStart(DontCrashMe)); + workerThread1.Start(); + } + + void DontCrashMe() + { + beginInvokePlaces.RemoveAt(0); + } + + // A scenario that shows how the previous attempt can cause your + // application to be in a bad state. + private void DelegateUIThreadNotWorking_Click(object sender, RoutedEventArgs e) + { + beginInvokePlaces = new BeginInvokeOC(lb.Dispatcher); + AddPlaces(beginInvokePlaces); + lb.ItemsSource = beginInvokePlaces; + lb.DisplayMemberPath = "Name"; + workerThread1 = new Thread(new ThreadStart(DelegateUIThreadNotWorking_Thread1)); + workerThread1.Start(); + workerThread2 = new Thread(new ThreadStart(DelegateUIThreadNotWorking_Thread2)); + workerThread2.Start(); + } + + void DelegateUIThreadNotWorking_Thread1() + { + int count = beginInvokePlaces.Count; + Thread.Sleep(500); // do a bunch of work (or be really unlucky to be interrupted by another thread here) + Place newPlace = beginInvokePlaces[count - 1]; + } + + void DelegateUIThreadNotWorking_Thread2() + { + Thread.Sleep(100); // do a little work + beginInvokePlaces.RemoveAt(0); + } + + // If you get all your locks right, this solution won't get you in + // a bad state, but it has a few other disadvantages and unknowns. + private void LockingOperations_Click(object sender, RoutedEventArgs e) + { + invokePlaces = new InvokeOC(lb.Dispatcher); + AddPlaces(invokePlaces); + lb.ItemsSource = invokePlaces; + lb.DisplayMemberPath = "Name"; + workerThread1 = new Thread(new ThreadStart(LockingOperations_Thread1)); + workerThread1.Start(); + workerThread2 = new Thread(new ThreadStart(LockingOperations_Thread2)); + workerThread2.Start(); + } + + void LockingOperations_Thread1() + { + lock (lockObject) + { + int count = invokePlaces.Count; + Thread.Sleep(500); // do a bunch of work + Place newPlace = invokePlaces[count - 1]; + } + } + + void LockingOperations_Thread2() + { + lock (lockObject) + { + Thread.Sleep(100); // do a little work + invokePlaces.RemoveAt(0); + } + } + + private void AddPlaces(ObservableCollection places) + { + places.Add(new Place("Seattle", "WA")); + places.Add(new Place("Redmond", "WA")); + places.Add(new Place("Bellevue", "WA")); + places.Add(new Place("Kirkland", "WA")); + places.Add(new Place("Portland", "OR")); + places.Add(new Place("San Francisco", "CA")); + places.Add(new Place("Los Angeles", "CA")); + places.Add(new Place("San Diego", "CA")); + places.Add(new Place("San Jose", "CA")); + places.Add(new Place("Santa Ana", "CA")); + places.Add(new Place("Bellingham", "WA")); + } + } + + public class BeginInvokeOC : ObservableCollection + { + private Dispatcher dispatcherUIThread; + + private delegate void SetItemCallback(int index, T item); + private delegate void RemoveItemCallback(int index); + private delegate void ClearItemsCallback(); + private delegate void InsertItemCallback(int index, T item); + private delegate void MoveItemCallback(int oldIndex, int newIndex); + + public BeginInvokeOC(Dispatcher dispatcher) + { + this.dispatcherUIThread = dispatcher; + } + + protected override void SetItem(int index, T item) + { + if (dispatcherUIThread.CheckAccess()) + { + base.SetItem(index, item); + } + else + { + dispatcherUIThread.BeginInvoke(DispatcherPriority.Send, + new SetItemCallback(SetItem), index, new object[] { item }); + } + } + + protected override void RemoveItem(int index) + { + if (dispatcherUIThread.CheckAccess()) + { + base.RemoveItem(index); + } + else + { + dispatcherUIThread.BeginInvoke(DispatcherPriority.Send, + new RemoveItemCallback(RemoveItem), index); + } + } + + protected override void ClearItems() + { + if (dispatcherUIThread.CheckAccess()) + { + base.ClearItems(); + } + else + { + dispatcherUIThread.BeginInvoke(DispatcherPriority.Send, + new ClearItemsCallback(ClearItems)); + } + } + + protected override void InsertItem(int index, T item) + { + if (dispatcherUIThread.CheckAccess()) + { + base.InsertItem(index, item); + } + else + { + dispatcherUIThread.BeginInvoke(DispatcherPriority.Send, + new InsertItemCallback(InsertItem), index, new object[] { item }); + } + } + + protected override void MoveItem(int oldIndex, int newIndex) + { + if (dispatcherUIThread.CheckAccess()) + { + base.MoveItem(oldIndex, newIndex); + } + else + { + dispatcherUIThread.BeginInvoke(DispatcherPriority.Send, + new MoveItemCallback(MoveItem), oldIndex, new object[] { newIndex }); + } + } + } + + public class InvokeOC : ObservableCollection + { + private Dispatcher dispatcherUIThread; + + private delegate void SetItemCallback(int index, T item); + private delegate void RemoveItemCallback(int index); + private delegate void ClearItemsCallback(); + private delegate void InsertItemCallback(int index, T item); + private delegate void MoveItemCallback(int oldIndex, int newIndex); + + public InvokeOC(Dispatcher dispatcher) + { + this.dispatcherUIThread = dispatcher; + } + + protected override void SetItem(int index, T item) + { + if (dispatcherUIThread.CheckAccess()) + { + base.SetItem(index, item); + } + else + { + dispatcherUIThread.Invoke(DispatcherPriority.Send, + new SetItemCallback(SetItem), index, new object[] { item }); + } + } + + protected override void RemoveItem(int index) + { + if (dispatcherUIThread.CheckAccess()) + { + base.RemoveItem(index); + } + else + { + dispatcherUIThread.Invoke(DispatcherPriority.Send, + new RemoveItemCallback(RemoveItem), index); + } + } + + protected override void ClearItems() + { + if (dispatcherUIThread.CheckAccess()) + { + base.ClearItems(); + } + else + { + dispatcherUIThread.Invoke(DispatcherPriority.Send, + new ClearItemsCallback(ClearItems)); + } + } + + protected override void InsertItem(int index, T item) + { + if (dispatcherUIThread.CheckAccess()) + { + base.InsertItem(index, item); + } + else + { + dispatcherUIThread.Invoke(DispatcherPriority.Send, + new InsertItemCallback(InsertItem), index, new object[] { item }); + } + } + + protected override void MoveItem(int oldIndex, int newIndex) + { + if (dispatcherUIThread.CheckAccess()) + { + base.MoveItem(oldIndex, newIndex); + } + else + { + dispatcherUIThread.Invoke(DispatcherPriority.Send, + new MoveItemCallback(MoveItem), oldIndex, new object[] { newIndex }); + } + } + } + + public class Place : INotifyPropertyChanged + { + private string name; + + private string state; + + public string Name + { + get { return name; } + set + { + name = value; + OnPropertyChanged("Name"); + } + } + + public string State + { + get { return state; } + set + { + state = value; + OnPropertyChanged("State"); + } + } + + public Place(string name, string state) + { + this.name = name; + this.state = state; + } + + public event PropertyChangedEventHandler PropertyChanged; + + private void OnPropertyChanged(string propertyName) + { + if (PropertyChanged != null) + { + PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); + } + } + } +} \ No newline at end of file diff --git a/31-ChangesMultithreading/Images/31ChangesMultithreading.png b/31-ChangesMultithreading/Images/31ChangesMultithreading.png new file mode 100644 index 0000000000000000000000000000000000000000..0b59eb07dc657f1dda08fe68462ee8912376a986 GIT binary patch literal 14550 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 + + + ObservableCollection throwPlaces; + + private void Throw_Click(object sender, RoutedEventArgs e) + { + throwPlaces = new ObservableCollection(); + AddPlaces(throwPlaces); + lb.ItemsSource = throwPlaces; + lb.DisplayMemberPath = "Name"; + (...) + } + + private void AddPlaces(ObservableCollection places) + { + places.Add(new Place("Seattle", "WA")); + places.Add(new Place("Redmond", "WA")); + places.Add(new Place("Bellevue", "WA")); + (...) + } + +Pretty simple. Next, I want to make sure that any changes to my collection are propagated to the UI. Typically, if you are using ObservableCollection<T>, this comes for free because it already implements INotifyCollectionChanged. However, this time I want to change the collection from a different thread: + + Thread workerThread1; + + private void Throw_Click(object sender, RoutedEventArgs e) + { + (...) + workerThread1 = new Thread(new ThreadStart(CrashMe)); + workerThread1.Start(); + } + + void CrashMe() + { + throwPlaces.RemoveAt(0); + } + +Unfortunately, this code results in an exception: "NotSupportedException - This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread." I understand this error message leads people to think that, if the CollectionView they're using doesn't support cross-thread changes, then they have to find the one that does. Well, this error message is a little misleading: none of the CollectionViews we provide out of the box supports cross-thread collection changes. And no, unfortunately we can not fix the error message at this point, we are very much locked down. + +If you understand the Avalon Dispatcher, you're probably already working on a master plan to delegate all collection change operations to the UI thread. You can do this by deriving from ObservableCollection, making sure your constructor takes a dispatcher as a parameter, and overriding all collection change operations. Here is my implementation of this collection: + + public class BeginInvokeOC : ObservableCollection + { + private Dispatcher dispatcherUIThread; + + private delegate void SetItemCallback(int index, T item); + private delegate void RemoveItemCallback(int index); + private delegate void ClearItemsCallback(); + private delegate void InsertItemCallback(int index, T item); + private delegate void MoveItemCallback(int oldIndex, int newIndex); + + public BeginInvokeOC(Dispatcher dispatcher) + { + this.dispatcherUIThread = dispatcher; + } + + protected override void SetItem(int index, T item) + { + if (dispatcherUIThread.CheckAccess()) + { + base.SetItem(index, item); + } + else + { + dispatcherUIThread.BeginInvoke(DispatcherPriority.Send, new SetItemCallback(SetItem), index, new object[] { item }); + } + } + + // Similar code for RemoveItem, ClearItems, InsertItem and MoveItem + (...) + } + +When you create this collection, make sure you pass the dispatcher from the UI thread as a parameter to the constructor. Now imagine you change this collection from a worker thread. The first time SetItem is called, CheckAccess will return false because we are not in the UI thread. We will then add a call to this same method to the UI thread's dispatcher queue, at priority Send. Once the dispatcher finishes processing the current job (and any other higher priority jobs), it picks up the one we added and SetItem is called again, this time on the UI thread. CheckAccess is called again, but this time it returns true, and we call SetItem on the collection. In plain english, this code means "Use the UI thread to set an item in the collection." + +Here is the code that uses this collection: + + + + + BeginInvokeOC beginInvokePlaces; + + private void DelegateUIThread_Click(object sender, RoutedEventArgs e) + { + beginInvokePlaces = new BeginInvokeOC(lb.Dispatcher); + AddPlaces(beginInvokePlaces); + lb.ItemsSource = beginInvokePlaces; + lb.DisplayMemberPath = "Name"; + workerThread1 = new Thread(new ThreadStart(DontCrashMe)); + workerThread1.Start(); + } + + void DontCrashMe() + { + beginInvokePlaces.RemoveAt(0); + } + +If you click this button, you will see that the data will actually be changed (the item at index 0 will be removed) without any crashes. This will probably work OK if you only have the UI thread and a worker thread, but it may get you into trouble if you have two or more worker threads. This has nothing to do with Avalon, it's just a plain multithreading problem. Let's take a look at this same solution, but with two worker threads this time: + + private void DelegateUIThreadNotWorking_Click(object sender, RoutedEventArgs e) + { + beginInvokePlaces = new BeginInvokeOC(lb.Dispatcher); + AddPlaces(beginInvokePlaces); + lb.ItemsSource = beginInvokePlaces; + lb.DisplayMemberPath = "Name"; + workerThread1 = new Thread(new ThreadStart(DelegateUIThreadNotWorking_Thread1)); + workerThread1.Start(); + workerThread2 = new Thread(new ThreadStart(DelegateUIThreadNotWorking_Thread2)); + workerThread2.Start(); + } + + void DelegateUIThreadNotWorking_Thread1() + { + int count = beginInvokePlaces.Count; + Thread.Sleep(500); // do a bunch of work (or be really unlucky to be interrupted by another thread here) + Place newPlace = beginInvokePlaces[count - 1]; + } + + void DelegateUIThreadNotWorking_Thread2() + { + Thread.Sleep(100); // do a little work + beginInvokePlaces.RemoveAt(0); + } + +Look at the DelegateUIThreadNotWorking_Thread1() method. If you are unlucky enough to have execution switch from thread 1 to thread 2 between the calculation of the count and the use of indexer, and if you're even more unlucky to have thread 2 change your collection, you're in trouble. In this particular scenario, count is initially 11 in thread 1, then thread 2 removes an item and it becomes 10. However, when execution goes back to thread 1, the indexer still thinks count is 11, and will look for the item in index 11 - 1, which will throw an ArgumentOutOfRangeException. In a real world scenario, the probability of this happening would increase with the amount of work you would do in place of the Thread.Sleep(500) call. + +If we're getting synchronization problems, the next logical step is to lock any atomic operations on these threads. Here is how I did that: + + + + + InvokeOC invokePlaces; + object lockObject; + + public Window1() + { + InitializeComponent(); + lockObject = new object(); + } + + private void LockingOperations_Click(object sender, RoutedEventArgs e) + { + invokePlaces = new InvokeOC(lb.Dispatcher); + AddPlaces(invokePlaces); + lb.ItemsSource = invokePlaces; + lb.DisplayMemberPath = "Name"; + workerThread1 = new Thread(new ThreadStart(LockingOperations_Thread1)); + workerThread1.Start(); + workerThread2 = new Thread(new ThreadStart(LockingOperations_Thread2)); + workerThread2.Start(); + } + + void LockingOperations_Thread1() + { + lock (lockObject) + { + int count = invokePlaces.Count; + Thread.Sleep(500); // do a bunch of work + Place newPlace = invokePlaces[count - 1]; + } + } + + void LockingOperations_Thread2() + { + lock (lockObject) + { + Thread.Sleep(100); // do a little work + invokePlaces.RemoveAt(0); + } + } + +Because I am locking all atomic sequences of operations and all changes to the collection, I know that the logic in the worker threads will never lead to the synchronization problem in the previous example. The code that does the item generation in ItemsControl has a handle to the collection, but I can tell you for sure that it never modifies the collection (it only reads it), so there should be no conflicts with the UI thread either. I also couldn't think of a scenario where this code would lead to a deadlock (although it's easier to prove the existence of a deadlock than the lack of one...) There was one possible problem I was able to identify: if the last operation of a locked block does a BeginInvoke to the UI thread, but the execution is transfered to the other worker thread before that operation is able to execute, we could get in a bad state. I solved this by replacing all BeginInvoke calls (asynchronous) with Invoke calls (synchronous). This way, we guarantee that, by the time we exit the lock on one thread, all operations inside that lock have finished executing in the UI thread. + +This solution sounds pretty good, but I can think of a couple of reasons why you should NOT change your million dollar application to use it: + +- Imagine the current job in the dispatcher is a lengthy layout pass, followed by several input operations (which have high priority). The dispatcher will not interrupt the current job, not even for a higher priority job, so we will have to let the layout pass finish. Also, since the worker threads are delegating to the dispatcher with priority Send, we will have to wait for all higher priority dispatcher items before the change operations are allowed to run. Delegating the worker thread operations at a priority higher than Send is not a good idea because your UI may become unresponsive. Basically, the worker thread needs to wait for the UI to catch up, and this is not efficient. + +- It hasn't been tested. I make absolutely no guarantees about a solution that I have only seen running on my machine. + +If you do decide to go ahead and use this solution (at your own risk), there are a few things for you to keep in mind: + +- You should never add thread-bound objects to the ObservableCollection (such as UIElements). This solution can only be used with your usual data items (and frozen Freezables) because they are not thread-bound. + +- The one advantage this solution provides is parallelism between the UI thread and one of the worker threads. Because the UI thread doesn't take any locks, it can be running at the same time as one other thread that takes locks. This is a big advantage if you have lengthy computations in the place of the Sleep calls that don't require delegating to the UI thread. However, if most of the work you do in the worker threads is collection change operations (which delegate to the UI thread), then this solution will not provide any advantage to you. If this is your scenario, you should start by asking yourself whether you really need a multithreaded solution. If you realize you do need it, then you should consider delegating the sequence of change operations as a whole, instead of delegating one by one like in my solution. + +- With great power comes great responsibility. Feel free to use the ObservableCollection that delegates all operations to the UI thread, but you are still responsible for locking all critical operations. + +We really wanted to make it easier to develop multithreaded applications that use data binding, but unfortunately we ran out of time in V1. We do realize that it shouldn't be so complex. Hopefully we will be able to revisit this topic in V2. + +## Property change notifications + +Property change notifications across multiple threads work pretty well. When a UI element is data bound to a property that gets changed by a worker thread, Avalon will receive notification of the property change on the UI thread. One thing that may surprise you is that if there are many property changes happening very quickly in your data source, Avalon won't update your target dependency property at the same rate. This was a conscious decision. Although this behavior may prevent you from creating an accurate graph of all data changes over time, it has the advantage of keeping the UI responsive. The UI will always get updated when the data has changed, just not for every single change. + +This is common sense, but I'll mention it anyway: if your setter is not atomic, don't forget to use a lock around your operations. Typically this is not a problem because most setters are atomic. + +Talking about the one work item I so wished we had finished for V1 is tough. Thanks to all the customers who have asked me this question in the past. Thanks to Ian Griffiths for getting me to stop procrastinating and write this blog post about it. Thanks to David Jenni and Dwayne Need for listening to me ramble about multithreading when they had better things to do. Thanks to Sam Bent and Eric Stollnitz for going the extra mile of reviewing my sample code. + +The screenshot for today's post isn't all that interesting, but here it is anyway: + +![](Images/31ChangesMultithreading.png)