7

我有一个类公开了一个流利的界面样式,我也希望它是线程安全的。

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

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

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

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

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

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

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

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

我已将完整代码放在 GitHub 上,以防有人需要更广泛的背景来了解我的目标:https ://github.com/JamieDixon/ServiceManager

4

2 回答 2

5

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

变异情况在 .NET 中并不是很常见(直到 Linq 引入了 fluent 方法,它才普遍使用流畅的方法,相比之下,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最终持有 string "abc",线程安全StringBuilder将毫无意义,因为我们不会考虑保证它会成功地最终持有"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只要我们没有另一个线程写入,从多个线程调用 s 是线程安全的。这是一个常见的场景,所以我们在能够安全地满足这种情况方面是线程安全的,尽管面对另一个线程进行此类突变时我们不是线程安全的。

同样,我们可以将源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. source只有第二个具有我们从 linq 获得的那种延迟迭代。第一个实际上让我们比第二个更好地处理一些多线程情况,但是这样做很糟糕(例如,如果您想在持有锁的同时获取副本作为并发管理的一部分,请通过在持有锁的同时获取副本来实现锁定,而不是在其他任何东西中)。

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

总而言之,如果我们有方法生成仅引用源和Funcs 的对象,我们可以像源和Funcs 一样是线程安全的,但并不安全。

*作为优化,记忆会导致从外部看不到的副作用。如果我们Func的 s 或他们调用的某些东西(例如 getter)使用它,那么必须以线程安全的方式实现记忆,以实现线程安全。

于 2012-09-01T19:36:53.757 回答
0

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

链接方法调用的“标准”方式是返回同一类的实例,在该实例上可以进行后续方法调用。

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

为了解决这个问题,我决定隐含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

于 2012-09-02T16:30:09.267 回答