This commit is contained in:
Beatriz Stollnitz 2019-08-05 18:12:03 -07:00
Родитель e6ad60b3cf
Коммит e06c47ff0a
14 изменённых файлов: 1008 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,9 @@
<Application x:Class="ChangesMultithreading.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window1.xaml"
>
<Application.Resources>
</Application.Resources>
</Application>

Просмотреть файл

@ -0,0 +1,17 @@
using System;
using System.Windows;
using System.Data;
using System.Xml;
using System.Configuration;
namespace ChangesMultithreading
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : System.Windows.Application
{
}
}

Просмотреть файл

@ -0,0 +1,89 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{7C19EE40-623C-4849-A404-1D3BDACB3884}</ProjectGuid>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<RootNamespace>ChangesMultithreading</RootNamespace>
<AssemblyName>ChangesMultithreading</AssemblyName>
<WarningLevel>4</WarningLevel>
<OutputType>winexe</OutputType>
<MinFrameworkVersionRequired>3.0</MinFrameworkVersionRequired>
<Install>true</Install>
<InstallFrom>Web</InstallFrom>
<UpdateEnabled>true</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>false</MapFileExtensions>
<ApplicationVersion>1.0.0.*</ApplicationVersion>
<IsWebBootstrapper>true</IsWebBootstrapper>
<BootstrapperEnabled>true</BootstrapperEnabled>
<PublishUrl>Publish\</PublishUrl>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>.\bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>false</DebugSymbols>
<Optimize>true</Optimize>
<OutputPath>.\bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="UIAutomationProvider" />
<Reference Include="UIAutomationTypes" />
<Reference Include="ReachFramework" />
<Reference Include="System.Printing" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.IdentityModel" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml" />
<Page Include="Window1.xaml" />
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="Window1.xaml.cs">
<DependentUpon>Window1.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<Compile Include="Properties\Resources.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.cs</LastGenOutput>
</None>
<Compile Include="Properties\Settings.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
<AppDesigner Include="Properties\" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildBinPath)\Microsoft.WinFX.targets" />
</Project>

Просмотреть файл

@ -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

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,54 @@
#region Using directives
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Resources;
using System.Globalization;
using System.Windows;
using System.Runtime.InteropServices;
#endregion
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("ChangesMultithreading")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ChangesMultithreading")]
[assembly: AssemblyCopyright("Copyright @ 2006")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
//In order to begin building localizable applications, set
//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
//inside a <PropertyGroup>. For example, if you are using US english
//in your source files, set the <UICulture> 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.*")]

Просмотреть файл

@ -0,0 +1,70 @@
//------------------------------------------------------------------------------
// <autogenerated>
// 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.
// </autogenerated>
//------------------------------------------------------------------------------
namespace ChangesMultithreading.Properties
{
using System;
using System.IO;
using System.Resources;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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()
{
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
public static System.Globalization.CultureInfo Culture
{
get
{
return _resCulture;
}
set
{
_resCulture = value;
}
}
}
}

Просмотреть файл

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

Просмотреть файл

@ -0,0 +1,42 @@
//------------------------------------------------------------------------------
// <autogenerated>
// 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.
// </autogenerated>
//------------------------------------------------------------------------------
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;
}
}
}
}

Просмотреть файл

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='iso-8859-1'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

Просмотреть файл

@ -0,0 +1,15 @@
<Window x:Class="ChangesMultithreading.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ChangesMultithreading" Height="380" Width="300"
>
<Border Margin="20">
<StackPanel>
<ListBox Name="lb" Height="170" Margin="5"/>
<Button Click="Throw_Click" Margin="5">Throw</Button>
<Button Click="DelegateUIThread_Click" Margin="5">DelegateUIThread</Button>
<Button Click="DelegateUIThreadNotWorking_Click" Margin="5">DelegateUIThreadNotWorking</Button>
<Button Click="LockingOperations_Click" Margin="5">LockingOperations</Button>
</StackPanel>
</Border>
</Window>

Просмотреть файл

@ -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<Place> beginInvokePlaces;
InvokeOC<Place> invokePlaces;
ObservableCollection<Place> 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<Place>();
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<Place>(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<Place>(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<Place>(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<Place> 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<T> : ObservableCollection<T>
{
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<T> : ObservableCollection<T>
{
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));
}
}
}
}

Двоичные данные
31-ChangesMultithreading/Images/31ChangesMultithreading.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 14 KiB

Просмотреть файл

@ -0,0 +1,216 @@
# How to propagate changes across threads
I hope you didn't get your hopes up too much when you read the title for this post. This time, instead of showing off what we support in our platform, I will explain to you what we don't. The scenario I have in mind is when you want to bind a control in the UI thread to a collection or property that is modified in a different worker thread. My goal for this post is to tell you what works today and what doesn't, and although no good workaround exists, I will discuss some ideas around this topic.
Here is the quick version of this post:
- We do not support collection change notifications across threads.
- We support property change notifications across threads.
Now the long version:
(Disclaimer: I will try to make the content as easy to read as possible, but you will only take full advantage of this post if you're comfortable with multithreading, the Avalon Dispatcher and some basic data binding.)
## Collection change notifications
In this scenario, I have a ListBox that is data bound to a collection of Place objects:
<ListBox Name="lb"/>
<Button Click="Throw_Click">Throw</Button>
ObservableCollection<Place> throwPlaces;
private void Throw_Click(object sender, RoutedEventArgs e)
{
throwPlaces = new ObservableCollection<Place>();
AddPlaces(throwPlaces);
lb.ItemsSource = throwPlaces;
lb.DisplayMemberPath = "Name";
(...)
}
private void AddPlaces(ObservableCollection<Place> 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&lt;T&gt;, 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<T> : ObservableCollection<T>
{
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:
<ListBox Name="lb"/>
<Button Click="DelegateUIThread_Click">DelegateUIThread</Button>
BeginInvokeOC<Place> beginInvokePlaces;
private void DelegateUIThread_Click(object sender, RoutedEventArgs e)
{
beginInvokePlaces = new BeginInvokeOC<Place>(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<Place>(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:
<ListBox Name="lb"/>
<Button Click="LockingOperations_Click">LockingOperations</Button>
InvokeOC<Place> invokePlaces;
object lockObject;
public Window1()
{
InitializeComponent();
lockObject = new object();
}
private void LockingOperations_Click(object sender, RoutedEventArgs e)
{
invokePlaces = new InvokeOC<Place>(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 <a href="http://www.interact-sw.co.uk/iangblog/">Ian Griffiths</a> 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)