流畅接口 - 确保新实例

7
我有一个类,它提供了一种流畅接口风格,并且我也希望它是线程安全的。
目前,对该类实例上的可链接方法的调用会设置各种带操作(Func<T>)的集合。
当请求结果时,才会进行真正的工作。这使得用户可以以任何顺序链接方法调用。
var result = myFluentThing
.Execute(() => serviceCall.ExecHttp(), 5) 
.IfExecFails(() => DoSomeShizzle())
.Result<TheResultType>();

(这里的5是重试失败服务调用的次数。)

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

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

如果必须先调用Execute方法,我可以每次返回一个新的实例以便处理,但由于任何方法都可以在链中的任何时刻被调用,你会如何解决这个问题?

我更感兴趣的是理解解决这个问题的各种方式,而不是为了“正确地使其工作”而给出单一的答案。

我已经将完整的代码放在GitHub上,以防有人需要更广泛的上下文: https://github.com/JamieDixon/ServiceManager


线程安全是针对哪些并发操作在哪些方面?通过观察,我看不出足够的信息来知道它可以安全地并发处理哪些情况,也不知道你关心哪些情况。 - Jon Hanna
嗨@JonHanna,目前所有的方法都在类中设置了私有字段集合,这意味着该类本身是不可重入的(或线程安全的)。在某个时候,我需要定义一个单独的类实例,以便可以独立处理集合,而不是像现在这样全局处理。有哪些额外的信息可以使这个问题更容易回答? - Jamie Dixon
你是否在使用调用时改变对象本身而不是从中获取结果? - Jon Hanna
虽然还不是很确定问题的意思,不过关于流畅编程方式方法的线程安全性方面,我有一些想法,可能能帮到你。 - Jon Hanna
2个回答

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;
}

这是一种本质上不安全的方式,因为它会改变所涉及的对象,因此不同线程中的两个调用会互相干扰。当我们采取这种方法时,通常关心的是这些变化的结果。例如,在上面的StringBuilder示例中,我们关心sb最终是否持有字符串"abc",如果一个线程安全的StringBuilder能保证成功地将sb设置为"abc"或"acb",那么这个类本身是线程安全的,但对于调用代码来说并不是可接受的。

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

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

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

只要满足以下条件就是线程安全的:

  1. 遍历 someSource 是线程安全的。
  2. 调用 somePredicate 是线程安全的。
  3. 调用 someOrder 是线程安全的。
  4. 调用 someFactory 是线程安全的。

这可能看起来需要很多标准,但实际上,最后一个标准都是相同的:我们要求我们的 Func 实例是函数式的 - 它们没有副作用*,而是返回一个依赖于它们的输入的结果(虽然我们可以在仍然保持线程安全的情况下打破一些关于函数式的规则,但现在让我们不要复杂化问题)。嗯,那大概就是他们想到名称 Func 时的情况。请注意,Linq 中最常见的情况符合此描述。例如:

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.其他。
绝大多数情况下,第一种情况是仅面向其他读者时线程安全的。有些情况下,即使涉及并发写入,也是线程安全的。对于第二种情况,这取决于实现方式 - 是否根据当前线程需要获取连接等,还是在调用之间使用共享连接?对于第三种情况,除非我们认为“失去”另一个线程得到而不是我们得到的那些项目是可接受的,否则肯定不是线程安全的。其他情况就看具体情况了。
因此,从所有这些内容中,我们没有保证线程安全的东西,但是如果与提供所需程度线程安全的其他组件一起使用,则可以获得足够的线程安全程度。
关于所有可能的用途100%的线程安全?不,没有任何东西可以保证这一点。实际上,没有数据类型是线程安全的,只有特定的操作组 - 描述数据类型为“线程安全”,我们表示其所有成员方法和属性都是线程安全的,反过来,描述方法或属性为线程安全 我们说它本身是线程安全的,因此可以成为一组线程安全操作的一部分,但并不是每组线程安全操作都是线程安全的。
如果我们想要实现这种方法,我们需要创建一个基于调用对象(如果是成员而不是扩展)和参数的对象的方法或扩展,但不能进行变异。
让我们分别讨论两种类似于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也是线程安全的(这也适用于首先创建第一个对象的情况)。
因此,总之,如果我们有生成仅引用源和Func的对象的方法,我们可以像源和Func一样线程安全,但不能更安全。
*备忘录化会产生一个在外部不可见的副作用,作为一种优化。如果我们的Func或它们调用的某些东西(例如getter)使用备忘录化,则备忘录化必须以线程安全的方式实现,以使线程安全成为可能。

谢谢Jon。这真的很有帮助。目前我所做的是定义一个属性,让我知道是否触发了Execute方法,并通过内部构造函数返回一个新版本的自身,其中包含额外的设置。这意味着我必须考虑在执行Execute之前使用其他方法,但现在还好。如果某些条件尚未满足,我可以实现所有方法通过此内部构造函数返回相同类的新实例。这将适用于以任何顺序执行的方法。 - Jamie Dixon
另外一种方法是让每个方法返回一个新的实例,该实例具有复制过的所有属性。 - Jamie Dixon
你肯定希望返回一个新的 something 实例。你所改变的,必须要考虑到线程问题,但是你创建的只存在于创建时的一个线程中。(更广义地说,大多数多线程问题归结为让线程相互避免干扰,或者让它们相互通信;对于前者,不可变性是一个很好的工具——无论是永久的还是仅针对多个线程看到的时间段——而后者以某种方式涉及可变性,但是这是一组不同的问题)。不要改变应该是你的座右铭。 - Jon Hanna

0
为了在解决这个问题时添加一些额外的信息,我认为发布一个关联答案会很有用。
“标准”链接方法调用的方式是返回同一类的实例,随后可以进行其他方法调用。
我的原始代码通过直接返回“this”来实现这一点,但是,由于我的方法通过构建“Func<T>”的集合来改变字段,这使得消费者容易受到线程和重入问题的影响。
为了解决这个问题,我决定实现“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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接