System.Reflection.Emit有多大的线程安全性?

9
我正在涉足动态代码生成和System.Reflection.Emit。看起来一切都很简单明了,但是有一个问题我在网上找不到答案。
使用AssemblyBuilder构建动态程序集时,可以创建类型并立即开始使用它。如果以后需要向程序集添加另一种类型,也可以这样做,而且似乎一切正常(就我所知)。
但是,如果有两个线程正在尝试为该程序集构建类型呢?或者如果一个准备好的类型已经在使用中,而另一个正在制作中呢?是否会出现任何冲突或竞争条件?
添加:好吧,我认为需要更多信息。
我使用Reflection.Emit在运行时动态实现接口。基本上我有这样的东西:
class MagicClass
{
    public static T GetImplementation<T>();
}

需要注意的是,T 必须是一个接口,并且还有一些其他无关的要求。

每个接口都将有唯一的实现,而该实现将恰好有一个实例。它们都是线程安全的单例。

因此,当请求新的、以前未见过的接口时,我会实现它,创建一个实例,然后将其缓存到永久存储器中(这里定义为“直到我的程序停止”)。

但是,这将在 ASP.NET Web 应用程序中完成,因此我们有许多线程一直在请求接口。性能很重要,我正在尝试确定我可以承受多少多线程。

很明显,一个接口只会被一个线程实现。但是我能否同时在两个不同的线程中实现两个不同的接口呢?我想答案是“最好不要”。

好吧,我可以添加一个锁,只有一个线程同时执行实现。这将解决构建器相互干扰的问题。但是那些先前实现的接口呢?在制作新接口的同时,其他线程正在同时使用它们。我想它们没问题,除非它们试图在自己的程序集上使用某种反射?

当然,我可以制定每个接口实现一个程序集的策略,但这将大量占用“已加载模块”调试器窗口的空间。此外,我不知道拥有 100 个已加载程序集会产生什么性能影响(但我怀疑它们不会很好)。

第二次添加:好吧,我的语言肯定有问题,因为人们似乎不理解它。让我尝试举个代码例子:

var object1 = MagicClass.GetImplementation<I1>();
DoSomethingInAnotherThread(object1);
var object2 = MagicClass.GetImplementation<I2>();
< p>在DoSomethingInAnotherThread(object1)和MagicClass.GetImplementation<I2>()之间是否可能存在与线程相关的错误?

我们可以假设:

  • DoSomethingInAnotherThread(object1)不会调用MagicClass.GetImplementation<T>()
  • DoSomethingInAnotherThread(object1)不会对object1使用反射,object1本身也不会使用反射。

    基本上,问题是 - 因为另一个线程正在重新组织程序集,所以对object1.SomeMethod()进行无辜的调用是否会崩溃。


2
这个值得在这里引用吗?https://dev59.com/ZXXZa4cB1Zd3GeqPAv9o - Jason Evans
5
@leppie,你无法编写一个简单的测试用例来证明某个东西是线程安全的;大多数线程安全问题都是不常见的竞态条件,因此一个测试可以通过,而方法本质上是有缺陷的。 - Marc Gravell
2
@leppie 我的观点是:要进行那些精心安排的调用,通常需要深入了解您期望它失败的位置;否则,您只是随意地开火。 - Marc Gravell
@MarcGravell:没错 :) - leppie
顺便说一下:当在一个应用程序域中加载更多类型时(大约是10k以上的类型),性能会严重下降。如果你达到了这些数字,那可能会引起头痛。 - leppie
显示剩余5条评论
3个回答

7
唯一声明的注释是“此类型的任何公共静态成员(在Visual Basic中为Shared)都是线程安全的。任何实例成员都不能保证线程安全。”,因此我们不能做太多假设。
以下内容完全是通过检查IL获得的实现细节,并且除了轶事之外不能用于任何其他目的。
检查DefineDynamicModule,它使用锁,DefineType也是如此-实际上,它使用与父程序集构建器相同的SyncLock。
TypeBuilder内部的某些东西也是这样(DefineEvent、DefineMethod、DefineField等);但是有些东西,比如DefineCustomAttribute会进入extern代码,所以我们无法确定;但是,我看不到在外部方法中使用锁,并且它不会将锁传递给extern方法。
总体而言,看起来已经考虑了线程安全性,但最可能的情况是他们不想正式保证它。总的来说,大多数常见的东西应该没问题,特别是如果您从未让多个线程在同一类型上工作。
强调:这些都是完全特定于实现的内容,我们没有“此用例保证可行,但此用例未经考虑”的一套标准。
个人而言,我倾向于保持简单;通常一个构建器(每个程序集)就足够了。

我会确保每次只有一个线程在构建一个类型(由于其他无关要求,这也是必要的)。我更在意的是,在同时生成新类型时,使用已经完成的类型是否会发生冲突。 - Vilx-
@Vilx TypeBuilder?还是通过MakeType()(或其他方法)构建的Type?坦白地说,我建议“不要”。 - Marc Gravell
@Vilx-:如果一个类型没有引用正在构建的另一个类型,那么就不应该有问题。从我所看到的另一个问题中可以看出问题是在调用CreateType时引用的类型仍然不完整。 - leppie
好的,已经在问题中添加了澄清。 - Vilx-
如果代码已经内部锁定,我们可以在其外部包装一个锁来确保安全性,而不会损失可扩展性。 - usr

5

这是未指定的。

无论何时使用的代码未明确说明线程安全,则您必须做出选择。权衡不锁定的成本,出现错误并在完全随机的时间间隔内导致代码崩溃。相对于采取锁定并确保永远不会出错,只是有点慢的成本。

“有点慢”是您必须知道才能作出正确选择的内容。在这种情况下很容易选择,构建一个方法最多需要几微秒。您放置在其周围的锁定将不会受到争议。如果仍然存在,则产生的延迟对任何人都不可观察。线程上下文切换或页面故障是正常发生的事情,会导致代码运行变慢,所需的时间比那还长。

加锁吧,你永远不会后悔。


请阅读“已添加”部分。在建筑部分添加锁并没有问题。事实上,这正是我现在正在做的。问题是 - 我能否在同时建造新的东西时使用过去建造的东西? - Vilx-
此外,还有添加了 2 部分。 - Vilx-
1
我不清楚你还在担心什么。生成的实际代码的线程安全性遵循您编写任何代码的线程安全性的常规规则。如果您实现“每个线程一个接口”,那么就没有太多可担心的了。显然,如果对象的成员只被一个线程访问,那么您无法违反线程安全性。 - Hans Passant
嗯...我想是吧。 :) 谢谢。 :) - Vilx-

0
基本上问题是,如果在另一个线程上重新组织程序集,那么对object1.SomeMethod()的无辜调用是否会导致崩溃。
通过使用Castle.Core包,该包使用System.Reflection.Emit生成类型的经验,我非常确定答案是“是”,它总是可能并且有时会崩溃。我通常看到的症状是,在尝试使用生成的类型之一时,您会得到TypeLoadException或BadImageFormageException。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接