我有一个类,它暴露了一种流畅的界面风格,我也希望它是线程安全的。
目前,在类的实例上调用可链接方法会设置带有操作的各种集合(Func<T>
)。
当要求结果时,实际工作就会发生。这允许用户以任何顺序链接方法调用,以便:
var result = myFluentThing
.Execute(() => serviceCall.ExecHttp(), 5)
.IfExecFails(() => DoSomeShizzle())
.Result<TheResultType>();
(此处,5是重试失败的服务呼叫的次数。)
显然这不是线程安全的或可重入的。
有哪些常见的设计模式可以解决这个问题?
如果必须首先调用Execute方法,我可以简单地返回一个新的类实例,但是因为任何方法都可以在链中的任何一点被调用,你将如何解决这个问题?
我更感兴趣的是了解解决这个问题的各种方法,而不仅仅是为了“让它正常工作”。
我已经将完整的代码放在GitHub上,任何人都需要更广泛的背景来实现我的目标:https://github.com/JamieDixon/ServiceManager
答案 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);
这是线程安全的,只要:
这可能看起来像很多标准,但实际上,最后一个都是相同的标准:我们要求我们的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
等来源分为四类:
第一个案例的绝大部分都是面向其他读者的线程安全的。对于并发编写者来说,有些是线程安全的。 对于第二种情况,它取决于实现 - 它是否在当前线程中根据需要获得连接等,或者使用在调用之间共享的连接? 对于第三种情况,除非我们考虑&#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 -> 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();
}
}
为简洁起见,删除了私有方法。 完整的源代码可以在这里找到: