流畅的接口 - 确保新的实例

时间:2012-09-01 08:10:41

标签: c# thread-safety fluent-interface method-chaining reentrancy

我有一个类,它暴露了一种流畅的界面风格,我也希望它是线程安全的。

目前,在类的实例上调用可链接方法会设置带有操作的各种集合(Func<T>)。

当要求结果时,实际工作就会发生。这允许用户以任何顺序链接方法调用,以便:

var result = myFluentThing
.Execute(() => serviceCall.ExecHttp(), 5) 
.IfExecFails(() => DoSomeShizzle())
.Result<TheResultType>();

(此处,5是重试失败的服务呼叫的次数。)

显然这不是线程安全的或可重入的。

有哪些常见的设计模式可以解决这个问题?

如果必须首先调用Execute方法,我可以简单地返回一个新的类实例,但是因为任何方法都可以在链中的任何一点被调用,你将如何解决这个问题?

我更感兴趣的是了解解决这个问题的各种方法,而不仅仅是为了“让它正常工作”。

我已经将完整的代码放在GitHub上,任何人都需要更广泛的背景来实现我的目标:https://github.com/JamieDixon/ServiceManager

2 个答案:

答案 0 :(得分:5)

我们可以将流畅的方法分为两种类型;变异和非变异。

在.NET中,变异案例并不常见(流利的应用程序通常直到Linq引入它并且非常大量地使用流畅的方法,相比之下,Java在属性制定者中大量使用它们其中C#使用属性为设置属性提供相同的语法,如设置字段)。一个例子是StringBuilder

StringBuilder sb = new StringBuilder("a").Append("b").Append("c");

基本形式是:

TypeOfContainingClass SomeMethod(/*... arguments ... */)
{
  //Do something, generally mutating the current object
  //though we could perhaps mix in some non-mutating methods
  //with the mutating methods a class like this uses, for
  //consistency.
  return this;
}

这是一种固有的非线程安全方法,因为它会改变有问题的对象,因此来自不同线程的两个调用将会发生干扰。当然,在面对这样的调用时,创建一个线程安全的类是可能的,因为它不会被置于一个不连贯的状态,但通常当我们采用这种方法时我们关心这些突变的结果,以及那些突变。例如。使用上面的StringbBuilder示例,我们关心sb最终持有字符串"abc",线程安全的StringBuilder将毫无意义,因为我们不会考虑保证它会成功结束,或者让"abc""acb"保持可接受 - 这样一个假设的类本身就是线程安全的,但调用代码不会。

(这并不意味着我们不能在线程安全的代码中使用这些类;我们可以在线程安全的代码中使用任何类,但它对我们没有帮助。)

现在,非变异形式本身就是线程安全的。这并不意味着所有用途都是线程安全的,但这意味着它们可以。请考虑以下LINQ代码:

var results = someSource
  .Where(somePredicate)
  .OrderBy(someOrderer)
  .Select(someFactory);

这是线程安全的,只要:

  1. 通过someSource迭代是线程安全的。
  2. 调用somePredicate是线程安全的。
  3. 调用someOrder是线程安全的。
  4. 调用someFactory是线程安全的。
  5. 这可能看起来像很多标准,但实际上,最后一个都是相同的标准:我们要求我们的Func个实例有效 - 它们没有副作用*,但是相反,他们返回的结果取决于他们的输入(我们可以弯曲一些关于功能的规则,同时仍然是线程安全的,但现在不要让事情复杂化)。好吧,这可能就是他们在提出名字Func时所考虑的那种情况。请注意,Linq最常见的案例符合此描述。 E.g:

    var results = someSource
      .Where(item => item.IsActive)//functional. Thread-safe as long as accessing IsActive is.
      .OrderBy(item => item.Priority)//functional. Thread-safe as long as accessing Priority is.
      .Select(item => new {item.ID, item.Name});//functional. Thread-safe as long as accessing ID and Name is.
    

    现在,99%的属性实现,只要我们没有另一个线程写入,从多个线程调用getter是线程安全的。这是一种常见的情况,因此我们在能够安全地满足该情况方面具有线程安全性,尽管我们在面对执行此类突变的另一个线程时不是线程安全的。

    同样,我们可以将someSource等来源分为四类:

    1. 内存中的一个集合。
    2. 针对数据库或其他数据源的调用。
    3. 一个可枚举的,可以对从某处获得的信息进行单次传递,但是源不具有在第二次迭代时再次检索该信息所需的信息。
    4. 其他。
    5. 第一个案例的绝大部分都是面向其他读者的线程安全的。对于并发编写者来说,有些是线程安全的。 对于第二种情况,它取决于实现 - 它是否在当前线程中根据需要获得连接等,或者使用在调用之间共享的连接? 对于第三种情况,除非我们考虑&#34;失去&#34;否则它绝对不是线程安全的。另一个线程而不是我们接受的那些项目。 好吧,&#34;其他&#34;是&#34;其他&#34;确实

      所以,从这一切来看,我们没有保证线程安全的东西,但我们确实有一些东西可以提供足够程度的线程安全性,如果与提供线程度的其他组件一起使用 - 我们需要的安全,我们得到它。

      面对所有可能的用途,100%的线程安全性?不,没有什么可以给你那个。实际上,没有数据类型是线程安全的,只有特定的操作组 - 将数据类型描述为&#34;线程安全&#34;我们说它的所有成员方法和属性都是treadsafe,反过来将方法或属性描述为线程安全我们说它本身就是线程安全的,因此可以成为一部分线程安全的操作组,但不是每组线程安全的操作都是线程安全的。

      如果我们想要实现这种方法,我们需要根据被调用的对象(如果是成员而不是扩展名)和参数创建一个创建对象的方法或扩展,但不会发生变异。 / p>

      让我们有两个单独的方法实现Enumerable.Select进行讨论:

      public static IEnumerable<TResult> SelectRightNow<TSource, TResult>(
        this IEnumerable<TSource> source,
        Func<TSource, TResult> selector)
        {
          var list = new List<TResult>();
          foreach(TSource item in source)
            list.Add(selector(item));
          return list;
        }
      
      public static IEnumerable<TResult> SelectEventually<TSource, TResult>(
        this IEnumerable<TSource> source,
        Func<TSource, TResult> selector)
        {
          foreach(TSource item in source)
            yield return selector(item);
        }
      

      在这两种情况下,该方法都会立即返回一个基于source内容的新对象。只有第二个虽然具有我们从linq获得的source的延迟迭代。第一个实际上让我们比第二个更好地处理一些多线程情况,但是这样做很明显(如果你想在持有锁作为并发管理的一部分时获得副本,那么通过在持有时获取副本来实现它)锁定,而不是在其他任何事情中。)

      在任何一种情况下,返回的对象都是我们可以提供的线程安全的关键。第一个获取了有关其结果的所有信息,因此只要它仅在本地引用到单个线程,它就是线程安全的。第二个具有生成这些结果所需的信息,因此只要它本地引用到单个线程,访问源是线程安全的,并且调用Func是线程安全的,它是&#39; s线程安全(以及那些也适用于创建第一个的第一个)。

      总而言之,如果我们有方法生成仅涉及源和Func的对象,我们可以像源和Func一样具有线程安全性,但是没有更安全。

      *作为优化,记忆会产生从外部看不到的副作用。如果我们的Func或他们调用的内容(例如getter)使用它,则必须以线程安全的方式实现memoisation,以便线程安全成为可能。

答案 1 :(得分:0)

要添加一些关于我如何解决这个问题的额外信息,我认为发布一个关联答案会很有用。

链接方法调用的“标准”方法是返回同一个类的实例,可以在其上进行子方法调用。

我的原始代码通过直接返回this来做到这一点,但是,由于我的方法是通过构建Func<T>的集合来改变字段,这使得消费者对线程和重新进入问题持开放态度。 / p>

为了解决这个问题,我决定提出ICloneable并让它通过object.MemberwiseClone()返回同一个类的新实例。这个浅层克隆在这种情况下运行正常,因为添加的字段是在重复克隆过程中复制的值类型。

我班级中的每个公共方法现在都执行实例Clone方法,并在返回克隆之前更新私有字段,以便:

public class ServiceManager : IServiceManager
    {
        /// <summary>
        /// A collection of Funcs to execute if the service fails.
        /// </summary>
        private readonly List<Func<dynamic>> failedFuncs = 
                                             new List<Func<dynamic>>();

        /// <summary>
        /// The number of times the service call has been attempted.
        /// </summary>
        private int count;

        /// <summary>
        /// The number of times to re-try the service if it fails.
        /// </summary>
        private int attemptsAllowed;

        /// <summary>
        /// Gets or sets a value indicating whether failed.
        /// </summary>
        public bool Failed { get; set; }

        /// <summary>
        /// Gets or sets the service func.
        /// </summary>
        private Func<dynamic> ServiceFunc { get; set; }

        /// <summary>
        /// Gets or sets the result implimentation.
        /// </summary>
        private dynamic ResultImplimentation { get; set; }

        /// <summary>
        /// Gets the results.
        /// </summary>
        /// <typeparam name="TResult">
        /// The result.
        /// </typeparam>
        /// <returns>
        /// The TResult.
        /// </returns>
        public TResult Result<TResult>()
        {
            var result = this.Execute<TResult>();

            return result;
        }

        /// <summary>
        /// The execute service.
        /// </summary>
        /// <typeparam name="TResult">
        /// The result.
        /// </typeparam>
        /// <param name="action">
        /// The action.
        /// </param>
        /// <param name="attempts">
        /// The attempts.
        /// </param>
        /// <returns>
        /// ServiceManager.IServiceManager.
        /// </returns>
        public IServiceManager ExecuteService<TResult>(
                                   Func<TResult> action, int attempts)
        {
            var serviceManager  = (ServiceManager)this.Clone();
            serviceManager.ServiceFunc = (dynamic)action;
            serviceManager.attemptsAllowed = attempts;

            return serviceManager;
        }

        /// <summary>
        /// The if service fails.
        /// </summary>
        /// <typeparam name="TResult">
        /// The result.
        /// </typeparam>
        /// <param name="action">
        /// The action.
        /// </param>
        /// <returns>
        /// ServiceManager.IServiceManager`1[TResult -&gt; TResult].
        /// </returns>
        public IServiceManager IfServiceFailsThen<TResult>(
                                      Func<TResult> action)
        {
            var serviceManager = (ServiceManager)this.Clone();
            serviceManager.failedFuncs.Add((dynamic)action);
            return serviceManager;
        }


        /// <summary>
        /// Clones the current instance of ServiceManager.
        /// </summary>
        /// <returns>
        /// An object reprisenting a clone of the current ServiceManager.
        /// </returns>
        public object Clone()
        {
            return this.MemberwiseClone();
        }        
    }

为简洁起见,删除了私有方法。 完整的源代码可以在这里找到:

https://github.com/JamieDixon/ServiceManager