我们可以将流利的方法分为两种类型;变异和非变异。
变异情况在 .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);
这是线程安全的,只要:
- 遍历 someSource 是线程安全的。
- 调用 somePredicate 是线程安全的。
- 调用 someOrder 是线程安全的。
- 调用 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
分为四类:
- 记忆中的一个集合。
- 针对数据库或其他数据源的调用。
- 一个可枚举的对象,它将通过从某处获得的信息进行单次传递,但源不具有在第二次迭代中再次检索该信息所需的信息。
- 其他。
第一种情况的绝大多数仅在其他读者面前都是线程安全的。面对并发编写者,有些也是线程安全的。对于第二种情况,它取决于实现 - 它是根据当前线程的需要获得连接等,还是使用调用之间共享的连接?对于第三种情况,它绝对不是线程安全的,除非我们认为“丢失”其他线程而不是我们获得的那些项目是可以接受的。好吧,“其他”就像“其他”一样。
因此,从所有这些来看,我们没有保证线程安全的东西,但我们确实有一些东西可以为我们提供足够程度的线程安全,如果与提供我们需要的线程安全程度的其他组件一起使用,我们得到它。
面对所有可能的用途,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
是线程安全的,它是线程安全的(以及那些也适用于首先创建第一个)。
总而言之,如果我们有方法生成仅引用源和Func
s 的对象,我们可以像源和Func
s 一样是线程安全的,但并不安全。
*作为优化,记忆会导致从外部看不到的副作用。如果我们Func
的 s 或他们调用的某些东西(例如 getter)使用它,那么必须以线程安全的方式实现记忆,以实现线程安全。