65.9K
CodeProject 正在变化。 阅读更多。
Home

线程安全的 Silverlight Cairngorm

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (9投票s)

2008年9月14日

CDDL

6分钟阅读

viewsIcon

50212

downloadIcon

352

一些代码更改和改进,使 Silverlight Cairngorm 线程安全

SilverlightCairngorm

引言

在我之前的文章《Silverlight Cairngorm》中,我提供了 Silverlight 中 essential 的 Cairngorm 结构,例如抽象的 ModelLocatorFrontControllerCairngormDelegateCairngormEventDispatcher 等。由于它是从 Flex ActionScript 移植过来的,而 Flex 不直接支持多线程编程,因此它没有考虑线程相关的问题。如果您计划使用的 RIA 遵循与 Flex 类似的编程模型,不创建工作线程或线程池线程来执行后台进程,那么使用上一版本的 Silverlight Cairngorm 就足够了,您无需担心线程和同步问题。

但是,如果您的 Silverlight 应用程序是多线程的,那么框架——Silverlight Cairngorm 就需要是线程安全的。特别是,ModelLocator 单例的创建和访问需要是线程安全的,并且所有基类中的数据绑定事件派发都需要在正确的线程上引发,否则会抛出异常,或者创建错误的对象的引用。本文详细介绍了上一版本的 Silverlight Cairngorm 为实现线程安全所做的改进,并提供了一个更新的演示项目,该项目使用线程池线程来执行数据转换和数据绑定更新。

Silverlight Cairngorm 中的线程安全单例

在 Silverlight Cairngorm 框架中,CairngormEventDispatcher 是一个内部单例对象。虽然应用程序代码无法访问它,但为了确保派发的 CairngormEvent 能正确路由到关联的命令,它仍然需要是线程安全的。下面是 CairngormEventDispatcher 常用的非线程安全单例代码:

namespace SilverlightCairngorm.Control
{
    /// <summary>
    /// Used to dispatch system events, by raising an event that the
    /// controller class subscribes to every time any system event is
    /// raised.
    /// Client code has no need to use this class. (internal class)
    /// </summary>
    internal class CairngormEventDispatcher
    {
        private static CairngormEventDispatcher instance;

        /// <summary>
        /// Returns the single instance of the dispatcher
        /// </summary>
        /// <returns>single instance of the dispatcher</returns>
        public static CairngormEventDispatcher getInstance()
        {
            if ( instance == null )
                instance = new CairngormEventDispatcher();

            return instance;
        }

        /// <summary>
        /// private constructor
        /// </summary>
        private CairngormEventDispatcher()
        {
        }

        //...Other code omitted to focus on singleton
     }
}

在多线程应用程序中,不同的线程可能同时执行到 if ( instance == null ) 这一行,并都判断为 true,然后 getInstance() 中就可能创建多个实例;直接的影响是,某个 CairngormEvent 被派发了,但对应的命令并未执行。下面是线程安全但未使用锁的实现:

//Thread-safe singleton implementation that not using lock
namespace SilverlightCairngorm.Control
{
    /// <summary>
    /// Used to dispatch system events, by raising an event that the
    /// controller class subscribes to every time any system event is
    /// raised.
    /// Client code has no need to use this class. (internal class)
    /// </summary>
    internal class CairngormEventDispatcher
    {
        private static readonly CairngormEventDispatcher _instance =
                       new CairngormEventDispatcher();

        // Explicit static constructor to tell C# compiler
        // not to mark type as beforefieldinit
        static CairngormEventDispatcher()
        {
        }

        /// <summary>
        /// private constructor
        /// </summary>
        private CairngormEventDispatcher()
        {
        }

        /// Returns the single instance of the dispatcher
        public static CairngormEventDispatcher
               Instance { get { return _instance; } }

        //...other code omitted to focus on singleton
    }
}

static 构造函数有助于 C# 编译器确保类型初始化的延迟性,从而正确且仅一次地创建 private static 字段 _instance;C# 中的 static 构造函数被指定为仅在类实例化或引用 static 成员时执行,并且每个 AppDomain 只执行一次。

通常,在 Silverlight Cairngorm 应用程序中(如演示项目中),应用程序的 ModelLocatorFrontController 都派生自基类,并实现为单例。上述简单的实现可以很容易地应用于应用程序中那些派生类。这些单例的所有代码更改都包含在演示项目下载中。

使用空的匿名方法初始化事件委托

同样,在内部类 CairngormEventDispatcher 中,EventDispatched 事件的实现方式要求 dispatchEvent 方法在调用委托之前检查委托是否为 null(以查看是否有事件监听器);这是之前的代码:

//previous null event delegate checking that has potential race condition
/// <summary>
/// The subscriber to a system event must accept as argument
/// the CairngormEvent raised (within a CairngormEventArgs object)
/// </summary>
public delegate void EventDispatchDelegate(object sender,
                     CairngormEventArgs args);
/// <summary>
/// The single event raised whenever a Cairngorm system event occurs
/// </summary>
public event EventDispatchDelegate EventDispatched;

/// <summary>
/// dispatchEvent raises a normal .net event, containing the
/// instance of the CairngormEvent raised - to be handled by
/// the Controller Class
/// </summary>
public void dispatchEvent(CairngormEvent cairngormEvent)
{
  if (EventDispatched != null)
  {
      CairngormEventArgs args = new CairngormEventArgs(cairngormEvent);
      EventDispatched(null, args);
  }
}

Juval Lowy 在他的书《Programming .NET Components, Second Edition》中提供了一个很好的技巧,可以在声明时将一个空的匿名方法挂接到 delegate 上;这样,我们就永远不需要在调用 delegate 之前检查 null,因为它的监听器列表中总会有一个无操作(no-op)的订阅者。它还可以避免在多线程应用程序中取消订阅时可能出现的竞争条件。下面是 EventDispatchDelegate 的更新代码:

// Leveraging empty anonymous methods for event delegate
/// <summary>
/// The subscriber to a system event must accept as argument
/// the CairngormEvent raised (within a CairngormEventArgs object)
/// </summary>

/// <param name=""""args"""" />a CairngormEventArgs object,
/// containing the raised event object
public delegate void EventDispatchDelegate(object sender, CairngormEventArgs args);
/// <summary>
/// The single event raised whenever a Cairngorm system event occurs
/// </summary>
public event EventDispatchDelegate EventDispatched = delegate { };

/// <summary>
/// dispatchEvent raises a normal .net event, containing the
/// instance of the CairngormEvent raised - to be handled by
/// the Controller Class
/// </summary>
/// <param name=""""cairngormEvent"""" />the raised Cairngorm Event
public void dispatchEvent(CairngormEvent cairngormEvent)
{
  CairngormEventArgs args = new CairngormEventArgs(cairngormEvent);
  EventDispatched(null, args);
}

同样,ModelLocator 使用相同的方法来使代码更简洁、更安全;详细信息请参见源代码。

线程安全的数据绑定事件

ModelLocator 旨在作为应用程序数据绑定的 DataContext(当然,您也可以有其他数据作为 DataContext),这就是为什么 abstract 基类实现了 INotifyPropertyChange 接口。在单线程应用程序中,它工作得很好(即使是更新集合数据,如演示项目中所示,每个搜索结果都会更新 FlickRPhoto 对象的集合)。由于 Silverlight 在更新 UI 方面遵循与 WPF 或 Windows Forms 相同的规则(需要在创建 UI 元素的同一线程上执行),因此对于多线程应用程序来说,使 INotifyPropertyChange 的实现线程安全至关重要,否则,如果模型从 UI 线程以外的线程更新,并且 PropertyChanged 事件在非 UI 线程上引发,Silverlight 将抛出 UnauthorizedAccessException 异常(WPF 会出于同样的原因抛出 InvalidOperationException 异常)。

在 Silverlight 中使 PropertyChanged 事件线程安全的思想是在调用事件委托之前检查执行线程是否是 UI 线程;如果是在同一线程上,则直接调用;如果不是,则将其分派到正确的线程,然后 UI(数据绑定)更新将不会出现异常。

这是 ModelLocatorINotifyPropertyChange 实现的线程安全版本:

namespace SilverlightCairngorm.Model
{
/// <summary>
/// modestyZ: 2008.10: derived from BindableBase, ModelLocator can be 
/// initialized/instantiated before RootVisual loaded
/// Custom ModelLocator will derive from this abstract class.
///
/// Custom ModelLocator usually implements Singleton pattern,
/// please see the example of CairgormEventDispatcher about 
/// how to make the Singleton thread-safe
/// </summary>
public abstract class ModelLocator : BindableBase
{
      protected ModelLocator()
      {
      }
}

抽象的 ModelLocator 的主体几乎是空的,它是基类型 BindableBase,它确保派生的 ModelLocator 类型必须实现 INotifyPropertyChange 接口,并且实现也是线程安全的。

namespace SilverlightCairngorm
  {
  /// <summary>
  /// modestyZ: 2008.10
  /// Abstract class that makes INotifyPropertyChanged implementation thread-safe
  /// Any type that serves as DataContext for data binding in a 
  /// multi-threaded application should derive from this type
  /// </summary>
  public abstract class BindableBase : INotifyPropertyChanged
  {
  //the dispatcher to ensure the PropertyChange event will raised for 
  //UI thread to update bound data
  protected Dispatcher currentDispatcher;
 public BindableBase() : this(null) { }
 public BindableBase(Dispatcher uiDispatcher)
  {
  currentDispatcher = uiDispatcher;
  }
 #region INotifyPropertyChanged Members
 public event PropertyChangedEventHandler PropertyChanged = delegate { };
  protected void NotifyPropertyChanged(string propertyName)
  {
  //the dispatcher initialization is deferred till the 
  //first PropertyChanged event raised
  CheckDispatcher();

  //check if we are on the Dispatcher thread if not switch
  if (currentDispatcher.CheckAccess())
  PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
  else
  currentDispatcher.BeginInvoke(new Action<string>
			(NotifyPropertyChanged), propertyName);
  }
 #endregion
  /// <summary>
  /// check and initialize the UI thread dispatcher when 
  /// the constructor passed in null dispatcher instance
  /// </summary>
  protected void CheckDispatcher()
  {
  if (null != currentDispatcher)
  return;
 if (Application.Current != null &&
  Application.Current.RootVisual != null &&
  Application.Current.RootVisual.Dispatcher != null)
  {
  currentDispatcher = Application.Current.RootVisual.Dispatcher;
  }
  else // can't get the Dispatcher, throw an exception
  {
  throw new InvalidOperationException
	("CheckDispatcher must be invoked after that the RootVisual has been loaded");
  }
  }
  }
  }

BindableBase 类型分离出来的另一个原因是使其可以重用于其他潜在的模型数据类型。例如,如果应用程序有一个视图需要数据绑定到自己的 ViewModel,而不是 ModelLocator,那么 ViewModel 类型可以派生自 BindableBase,它将从 BindableBase 获得相同的线程安全优势。

UI 线程调度器的初始化被推迟到第一个 NotitifyPropertyChange 事件引发时;受保护的 CheckDispatcher 方法将从 RootVisual 检索调度器,以确保它实际上引用的是 UI 线程的调度器。这种延迟实例化使得 ModelLocator 可以在 RootVisual 加载之前被实例化。

线程安全的 ObservableCollection 包装器

在多线程应用程序中,当将动态数据集合作为 DataContext 进行数据绑定时,我们还需要确保 INotifyCollectionChanged 事件也在 UI 线程上派发。在 Silverlight Cairngorm v0.0.1.3 中,新类型 BindableCollection 包装了 ObservableCollection 并派生自 BindableBase,使得 INotifyCollectionChange INotifyPropertyChange 的实现都是线程安全的。

namespace SilverlightCairngorm
  {
/// <summary>
/// modestyZ@hotmail.com: 2008.10
/// Wrapper class that makes ObservableCollection thread-safe
/// Any ObservableCollection that serves as DataContext for 
/// data binding in multi-threaded application should be wrapped by this type
/// </summary>
/// <typeparam name="T"></typeparam>
public class BindableCollection<T> : BindableBase, INotifyCollectionChanged,
IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable
{
private ObservableCollection<T> list;
public BindableCollection(ObservableCollection<T> list) : this(list, null) { }
 public BindableCollection
	(ObservableCollection<T> list, Dispatcher dispatcher) : base(dispatcher)
  {
  if (list == null)
  {
  throw new ArgumentNullException
	("The list must not be null for BindableCollection constructor");
  }
 this.list = list;
 INotifyCollectionChanged collectionChanged = list as INotifyCollectionChanged;
  collectionChanged.CollectionChanged += 
	delegate(Object sender, NotifyCollectionChangedEventArgs e)
  {
  NotifyCollectionChanged(this, e);
  };
 INotifyPropertyChanged propertyChanged = list as INotifyPropertyChanged;
  propertyChanged.PropertyChanged += delegate(Object sender, PropertyChangedEventArgs e)
  {
  NotifyPropertyChanged(e.PropertyName);
  };
  }

 #region INotifyCollectionChanged Members
 public event NotifyCollectionChangedEventHandler CollectionChanged = delegate { };
  protected void NotifyCollectionChanged
		(object sender, NotifyCollectionChangedEventArgs e)
  {
  //the dispatcher initialization could be deferred till the 
  //first PropertyChanged event raised
  CheckDispatcher();
  //check if we are on the Dispatcher thread if not switch
  if (currentDispatcher.CheckAccess())
  CollectionChanged(this, e);
  else
  currentDispatcher.BeginInvoke
	(new NotifyCollectionChangedEventHandler(CollectionChanged), this, e);
  }
}

BindableCollection 中,在使数据绑定事件线程安全的同时,它还通过 8 个接口实现了 ObservableCollection 公开的所有 public 方法。所有实现细节都可以在可下载的源代码中找到。

测试线程安全

以上都是 Silverlight Cairngorm 为实现线程安全所做的更改;现在,让我们更新演示项目来测试一下。演示项目的主要变化在于 SearchPhotoCommandonResult 方法。新的 ProcessResultFromThreadPool 方法将解析 FlickR API 返回的 XML,将其转换为 FlickRPhoto 对象集合,然后通知数据绑定填充新数据;所有这些都在线程池线程(非 UI 线程)中完成。这是代码:

//Thread-safe testing in Command
namespace SilverlightCairngormDemo.Command
{
    public class SearchPhotoCommand : ICommand, IResponder
    {
        private SilverPhotoModel model = SilverPhotoModel.Instance;

        #region ICommand Members

        public void execute(CairngormEvent cairngormEvent)
        {
            //disable the "go" button
            model.ReadySearchAgain = false;

            //get search term from model
            string toSearch = model.SearchTerm;

            //begin talk to web service
            SearchPhotoDelegate cgDelegate =
                               new SearchPhotoDelegate(this);
            cgDelegate.SendRequest(toSearch);
        }

        #endregion

        #region IResponder Members

        public void onResult(object result)
        {
            //enable the "go" button
            model.ReadySearchAgain = true;

            model.SelectedIdx = -1;

            string resultStr = (string)result;
            if (String.IsNullOrEmpty(resultStr))
            {
                onFault("Error! (Server returns empty string)");
                return;
            }

            //ProcessResult(resultStr);
            ProcessResultFromThreadPool(resultStr);
        }

        /// <summary>
        /// could be executed in UI thread when invoked directly from onResult
        /// or will run from a threadpool thread when called by
        /// ProcessResultFromThreadPool
        /// </summary>
        /// <param name=""""resultStr"""" />
        private void ProcessResult(string resultStr)
        {
            XDocument xmlPhotos = XDocument.Parse(resultStr);
            if ((null == xmlPhotos) ||
                xmlPhotos.Element("rsp").Attribute(
                                  "stat").Value == "fail")
            {
                onFault("Error! (" + resultStr + ")");
                return;
            }

            //update the photoList data in model
            model.PhotoList =
                  xmlPhotos.Element("rsp").Element(
                  "photos").Descendants().Select(p => new FlickRPhoto
            {
                Id = (string)p.Attribute("id"),
                Owner = (string)p.Attribute("owner"),
                Secret = (string)p.Attribute("secret"),
                Server = (string)p.Attribute("server"),
                Farm = (string)p.Attribute("farm"),
                Title = (string)p.Attribute("title"),
            }).ToList<flickrphoto>();

            if (model.PhotoList.Count > 0)
                model.SelectedIdx = 0; //display the 1st image
            else
                onFault("No such image, please search again.");
        }

        public void onFault(string errorMessage)
        {
            //enable the "go" button
            model.ReadySearchAgain = true;

            //display the error message in PhotoList
            model.SelectedIdx = -1;
            model.PhotoList = new List<flickrphoto>() { new FlickRPhoto() {
                               Title = errorMessage } };
        }

        private void ProcessResultFromThreadPool(string resultStr)
        {
            ThreadPool.QueueUserWorkItem(new WaitCallback(NonUIThreadWork),
                                resultStr);
        }

        private void NonUIThreadWork(object resultObj)
        {
            ProcessResult((string)resultObj);
        }

        #endregion
    }
}

您可以在 ModelLocator 基类中的 currentDispatcher.BeginInvoke(new Action <string>(NotifyPropertyChanged), propertyName); 这一行设置断点,看看它是如何工作的。然后,注释掉 onResult 中的 ProcessResultFromThreadPool(resultStr);,取消注释 ProcessResult(resultStr);(使其在同一线程中运行),看看它的行为如何。

历史

  • 2008.09.14 - 基于 Silverlight Cairngorm 的首次发布
  • 2008.10.05 - Silverlight Cairngorm v.0.0.1.2 (下载源代码和演示项目更新)
    • 更新了 Silverlight Cairngorm FrontController — — 每个注册的 Cairngorm 事件都将由一个新实例的相应 Cairngorm Command 处理,这将使 Silverlight Cairngorm FrontController 的工作方式与 Flex 的 Cairngorm 2.2.1 的 FrontController 相同。
    • 同时更新了演示项目,以反映新的 addCommand 签名,该签名通过传递 Command 的类型而不是 Command 的实例来工作。
    • 演示项目也已更新为使用 Silverlight 2 RC0。
  • 2008.10.18 - Silverlight Cairngorm v.0.0.1.3 (下载源代码和演示项目更新)
    • 添加了线程安全的 BindableBaseBindableCollection
    • ModelLocator 改为派生自 BindableBase
    • 验证所有源代码均可与 Silverlight 2 RTW 配合使用
© . All rights reserved.