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

Jam*_*xon 6 c# fluent-interface thread-safety reentrancy method-chaining

我有一个类,它暴露了一个流畅的界面风格,我也想要线程安全.

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

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

var result = myFluentThing
.Execute(() => serviceCall.ExecHttp(), 5) 
.IfExecFails(() => DoSomeShizzle())
.Result<TheResultType>();
Run Code Online (Sandbox Code Playgroud)

(这里,5是重试失败的服务呼叫的次数.)

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

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

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

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

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

Jon*_*nna 5

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

变异的情况在.NET中并不常见(直到Linq介绍它非常大量使用流畅的方法之前,流畅的apporach通常不会,相比之下,Java在属性设置器中大量使用它们,其中C#使用属性为相同的语法提供相同的语法将属性设置为设置字段).一个例子是StringBuilder.

StringBuilder sb = new StringBuilder("a").Append("b").Append("c");
Run Code Online (Sandbox Code Playgroud)

基本形式是:

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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

var results = someSource
  .Where(somePredicate)
  .OrderBy(someOrderer)
  .Select(someFactory);
Run Code Online (Sandbox Code Playgroud)

这是线程安全的,只要:

  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.
Run Code Online (Sandbox Code Playgroud)

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

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

  1. 内存中的集合.
  2. 对数据库或其他数据源的调用.
  3. 一个可枚举的,它将对从某个地方获得的信息进行单次传递,但是源不具有在第二次迭代时再次检索该信息所需的信息.
  4. 其他.

第一个案例绝大多数都是面向其他读者的线程安全的.有些在并发编写者面前也是线程安全的.对于第二种情况,它取决于实现 - 它是否在当前线程中根据需要获得连接等,或者使用在调用之间共享的连接?对于第三种情况,它绝对不是线程安全的,除非我们考虑"丢失"另一个线程而不是我们接受的那些项目.而且,"其他"就像"其他"一样.

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

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

如果我们想要实现这种方法,我们需要创建一个方法或扩展来创建一个对象,基于被调用的对象(如果是成员而不是扩展)和参数,但不会发生变异.

让我们有两个单独的方法实现,如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);
  }
Run Code Online (Sandbox Code Playgroud)

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

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

总而言之,如果我们有方法生成仅涉及源和Funcs的对象,那么我们可以像源和Funcs 一样具有线程安全性,但不会更安全.

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