我在多线程应用程序中有一个奇怪的错误:
public class MyClass
{
private readonly Hashtable HashPrefs;
public MyClass(int id)
{
HashPrefs = new Hashtable();
}
public void SomeMethodCalledFromAnotherThread(string hashKey,string hashValue)
{
if (HashPrefs.Contains(hashKey)) // <-- throws NullReferenceException
{
}
}
}
一个线程执行:
SomeQueue.Add(new MyClass(1));
另一个主题是:
SomeQueue.Dequeue().SomeMethodCalledFromAnotherThread(SomeClass.SomeMethod(),"const value");
但是第二个线程如何在构造函数完成之前调用该方法?
编辑:我添加了带有函数参数的部分,因为它似乎可能是相关的。 据我所知,传递的hashKey不能为null,因为SomeMethod()总是返回一个相关的字符串。
正如其他人所指出的,如果问题是传递给Contains()的null haskKey参数,则异常将是ArgumentNullException。
答案 0 :(得分:11)
反思是实现这一目标的一种方式(SetField
)。
获得好奇心的另一种方法是,如果过早地通过传递this
(或涉及隐式this
的某些东西,例如捕获到匿名方法/ lambda中的字段)过早地放弃引用).ctor
:
public MyClass(int id)
{
Program.Test(this); // oopsie ;-p
HashPrefs = new Hashtable();
}
或者更有可能(给出问题):
SomeQueue.Add(this);
另一个相关问题可能是 - 首先不能分配吗?答案是肯定的,特别是如果你使用序列化。与流行的看法相反,你实际上可以绕过构造函数,如果你有理由; DataContractSerializer
就是这样做的好例子......
以下内容将创建一个带有空字段的MyClass
:
MyClass obj = (MyClass)
System.Runtime.Serialization.FormatterServices.GetUninitializedObject(
typeof(MyClass));
答案 1 :(得分:7)
是的,只读字段可以通过反射访问并更改其值。所以你的问题的答案是肯定的,这是可能的。
然而,还有许多其他因素也可能为您带来问题。多线程代码难以编写,可能出现的问题难以诊断。
作为附注,你确定没有得到ArgumentNullException
吗?如果somekey
为null
,则Hashtable.Contains
方法会抛出ArgumentNullException
。也许这就是问题?
答案 2 :(得分:3)
首先,在构造函数完成之前,其他线程无法访问实例,因为在构造函数完成之前不会分配实例本身。话虽这么说,另一个线程可以在构造函数完成之前访问保存实例的变量,但那时它将是null
。这将生成NullReferenceException
,但它将来自访问实例的方法,而不是来自 in 实例的方法。
所有这一切,我能看到的唯一真正的解决方案是使用外部锁来同步检索实例的代码。如果您正在访问Hashset
(或实例成员不能保证线程安全的任何其他类型),您无论如何都需要执行操作锁定。
将以下类视为包含对MyClass
的引用和其他线程的创建者的类:
public class MasterClass
{
private MyClass myClass;
private object syncRoot = new object(); // this is what we'll use to synchronize the code
public void Thread1Proc()
{
lock(syncRoot)
{
myClass = new MyClass();
}
}
public void Thread2Proc()
{
lock(syncRoot)
{
myClas.SomeMethodCalledFromAnotherThread();
}
}
}
这是一个非常(可能过于简单)的关于如何进行同步的简单视图,但是这种方法的一般思想是包装与lock
块内的共享对象交互的所有代码。你真的应该研究如何编写多线程进程,以及旨在在多个线程之间共享资源的各种方法。
修改强>
完全不同的可能性是实例变量的值以特定于线程的方式缓存。如果您没有锁定(这会创建内存屏障),请尝试将实例变量标记为volatile
,这将确保读取和写入以正确的顺序发生。
答案 3 :(得分:3)
您的指令可能会被重新排序,以便在完全构造之前将对MyClass实例的引用添加到队列中。 Here's一篇关于双重检查锁定的文章,涉及到这一主题。来自文章:
CLR JIT团队的一个开发人员Vance解释说,问题出在CLR内存模型周围......基本上,内存模型允许重新排序非易失性读取/写入,只要从该点无法注意到该更改单线程的视图。
看一下System.Threading.Thread.MemoryBarrier.你可能需要这样做:
MyClass temp = new MyClass(1);
System.Threading.Thread.MemoryBarrier();
SomeQueue.Add(temp);
MemoryBarrier确保在继续执行之前执行所有指令。
答案 4 :(得分:1)
你确定它是抛出异常的HashPrefs.Contains,而不是“somekey”代表什么吗?如果你不是,你能否发布完整的异常细节,如ToString方法所返回的那样?
答案 5 :(得分:1)
只做
HashPrefs = Hashtable.Synchronized(new Hashtable());
无需重新发明轮子。
答案 6 :(得分:1)
你绝对相信HashPrefs.Contains
正在抛弃NPE吗?我认为SomeQueue.Dequeue().SomeMethodCalledFromAnotherThread()
更可能是候选人......因为当你试图将元素出列队列时,机会是构造函数,而Add操作还没有完成。
事实上,在排队元素之前检查SomeQueue.Count
*可能不是一个坏主意。
编辑:
或者甚至更好,在调用SomeMethodCalledFromAnotherThread之前进行null检查,假设你的另一个线程在循环中检查队列...因为如果不是,lock
在Add和Dequeue操作之前的队列。
MyClass mine = SomeQueue.Dequeue();
if (null != mine)
mine.SomeMethodCalledFromAnotherThread();
* .NET 3.5中添加的扩展方法。
答案 7 :(得分:0)
你可以尝试几件事:
尝试将Hashtable初始化为成员变量(在构造函数之外),并使用相同名称的公共属性,查看是否有人调用它来进行赋值,如下所示:
public class MyClass {
private readonly Hashtable hashPrefs = new Hashtable();
public MyClass(int id)
{
}
public Hashtable HashPrefs
{
set
{
throw new InvalidOperationException("This shouldn't happen");
}
}
}
其次,什么类型的“SomeQueue”?这只是一个常规列表&lt;&gt ;?或者它是一些依赖于某些XML /二进制序列化的特殊自实现队列?如果是这样,您是否将HashPrefs标记为[Serializable]?或者作为[DataMember]?
答案 8 :(得分:0)
想到几个想法: