我们可以将流畅的方法分为两种类型:可变和不可变。
在.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);
只要满足以下条件就是线程安全的:
- 遍历 someSource 是线程安全的。
- 调用 somePredicate 是线程安全的。
- 调用 someOrder 是线程安全的。
- 调用 someFactory 是线程安全的。
这可能看起来需要很多标准,但实际上,最后一个标准都是相同的:我们要求我们的 Func
实例是函数式的 - 它们没有副作用*,而是返回一个依赖于它们的输入的结果(虽然我们可以在仍然保持线程安全的情况下打破一些关于函数式的规则,但现在让我们不要复杂化问题)。嗯,那大概就是他们想到名称 Func
时的情况。请注意,Linq 中最常见的情况符合此描述。例如:
var results = someSource
.Where(item => item.IsActive)
.OrderBy(item => item.Priority)
.Select(item => new {item.ID, item.Name});
现在,对于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)使用备忘录化,则备忘录化必须以线程安全的方式实现,以使线程安全成为可能。