递归调用函数使用的线程安全计数器
递归调用函数使用的线程安全计数器
在使用名为GlobalID
的全局整数作为计数器时,以下代码运行正常:
MyFunction(myClass thisClass, Int ID) { ListOfMyClass.add(thisClass) If (thisClass.IsPropertyValueAboveZero) { ID = GlobalID; } GlobalID++; Foreach (class someClass in ListOfClasses) { MyClass newClass = new MyClass(GlobalID, ID) MyFunction(newClass, ID) //递归调用 } }
我基本上将GlobalID
用作每次调用函数时递增的计数器。该计数器在第一次执行时被分配一个起始位置。我使用全局变量是因为我希望确保每次传递时ID都会准确增加,而不管执行是否进入或离开递归调用。该函数(第一次......)是从ForEach循环中调用的,该循环为全局变量赋予起始位置。
我的目标是在初始调用中使用Parallel.ForEach而不是常规的ForEach循环。我的问题涉及计数器。我不知道如何在多个线程中管理该计数器。如果我将其作为变量传递给函数,我认为在离开递归循环时会得到一个不准确的较低/已使用数字。全局变量确保下一个数字比上一个数字更高。 thisClass.IsPropertyValueAboveZero
只是一种根据条件语句描述操作的任意方式。它与代码的其余部分没有有意义的关联。
如果我有多个具有不同起始位置的线程,如何实现线程安全?我眼下唯一能想到的方法是手动编写多个相同函数和计数器的版本,并使用TaskFactory
。
在多线程环境下,对于一个单一计数器进行线程安全的整数递增,可以考虑使用Interlocked.Increment方法。首先将计数器变量存储在一个静态变量中,然后在静态函数中递增它。每当需要递增时,调用IncrementBob函数。
那么,为什么需要使用线程安全的计数器以及为什么需要使用Interlocked.Increment方法呢?
在多线程环境下,多个线程可能同时访问和修改同一个计数器变量。如果不采取适当的线程安全措施,可能会导致计数器的值不正确。例如,如果两个线程同时读取计数器值并递增,然后将结果写回计数器,这两个线程可能会覆盖彼此的递增结果,导致计数器的递增结果不正确。
为了解决这个问题,可以使用Interlocked.Increment方法。这个方法是一个原子操作,可以确保在一个线程执行递增操作时,没有其他线程可以同时访问和修改计数器变量。这样可以保证线程安全,并且保证计数器的递增操作不会被打断。
下面是使用Interlocked.Increment方法实现线程安全计数器的示例代码:
public static int Bob; public static int IncrementBob() { return Interlocked.Increment(ref Bob); }
在这个例子中,首先将计数器变量Bob声明为一个静态变量。然后定义了一个静态函数IncrementBob,该函数使用Interlocked.Increment方法对Bob进行递增操作,并返回递增后的值。
通过这种方式,可以确保在多线程环境下对计数器进行递增操作时,不会出现线程安全问题。任何时候需要递增计数器时,只需要调用IncrementBob函数即可。
希望这个简单的解决方法能帮助你在多线程环境下使用线程安全的计数器。如果你对Bob不熟悉,可以参考这个电影的链接:imdb.com/title/tt0103241
问题的出现原因是在递归调用函数时,需要使用一个线程安全的计数器。在原始代码中,使用了Interlocked.Increment方法来实现计数器的递增,但是由于需要读取计数器的值,所以必须在读取时进行保护。
解决这个问题的方法是,在递增计数器的同时,不要直接使用计数器的值或者GlobalID,因为在多线程环境下,计数器的值可能已经被其他线程递增了。可以将计数器的值先保存在一个变量中,然后再根据需要进行操作。具体的代码如下:
var ID = someValue; var newValue = Interlocked.Increment(ref GlobalID); if (thisClass.IsPropertyValueAboveZero) { // 因为GlobalID的值可能已经被递增了,所以不能直接使用GlobalID ID = newValue - 1; // 注意-1以匹配原始代码的逻辑。 }
另外,如果只有在递增计数器时才需要使用计数器的值,可以直接使用Interlocked.Increment方法。但是需要注意,在其他线程可能会修改计数器的情况下,不能直接读取或者使用计数器的值或者GlobalID。
需要注意的是,如果使用场景不仅仅是“递增计数器时使用计数器的值”,可能需要重新考虑实现的目标,可能需要使用其他数据结构。使用lock来保护对计数器的读写操作并不能解决线程安全问题,因为无法预测即将读取的值(可能已经被其他线程多次递增)。
需要注意的是,原始代码中的示例代码显示了一些不清楚的使用旧的ID值和最新的计数器值的情况。如果代码实际反映了您要执行的操作,那么将“调用数目”的计数与“对象ID”分离可能会很有用。
多线程中使用线程安全的计数器是一个常见的需求。为了实现线程安全的计数,可以使用Interlocked方法中的Increment函数:System.Threading.Interlocked.Increment(ref globalID)
。
在进行计数器操作时,使用属性来公开计数器的值是一个容易出错的方式,因此最好使用Interlocked方法或显式的lock语句来确保对计数器值进行同步操作。通常,在逻辑处理中使用Increment、Decrement等方法的返回值会更加安全。
有人在提问中表示他在处理计数器时遇到了问题。他不知道如何在多个线程中管理计数器。如果将计数器作为变量传递给函数,他担心会导致计数的不准确,从而使得递归循环在较低的计数值时退出。而全局变量确保了下一个计数值比前一个计数值要高。
如果有多个线程在不同的起始位置上有自己的计数器,如何实现线程安全呢?目前我唯一能想到的办法是手动编写多个相同函数和计数器的版本,并使用TaskFactory来执行。
对于递归调用MyFunction(newClass, ID)
,ID的值是传递进来的,但是我不确定If (thisClass.IsPropertyValueAboveZero)
的作用是什么。它是用来确保起始点为非零吗?如果是这样,最好在此函数之外的初始调用之前确保其非零。同时,foreach循环中的逻辑对我来说也不太合理。在MyClass newClass = new MyClass(GlobalID, ID)
中,ID将是参数值,或者如果IsPropertyValueAboveZero
为true时,它将是GlobalID
的当前值。因此,ID通常会小于GlobalID,因为GlobalID在foreach循环之前被递增。我认为你在不需要的时候传递了GlobalID。
// 如果IsPropertyValueAboveZero是为了从非零值开始计数 // 那么你只需要将计数器初始化为1 private static int globalID = 1; public void MyFunction(myClass thisClass, int id) { // ListOfMyClass和ListOfClasses可能不是线程安全的 // 你可能需要在Add方法周围添加锁,并在foreach循环之前获取ListOfClasses的副本 // 你可以参考https://stackoverflow.com/a/6601832/29762 ListOfMyClass.Add(thisClass); foreach (class someClass in ListOfClasses) { int newId = System.Threading.Interlocked.Increment(ref globalID); MyClass newClass = new MyClass(newId); MyFunction(newClass, newId); //递归调用 } }
回答中提到了一些很好的观点,但是“你可以通过属性公开该字段”是一个糟糕的建议。你可以使用public int GlobalID { get { return random.Next(1000); } }
来代替。在多个线程递增计数器的情况下,没有办法获得有意义的变量值。
(thisClass.IsPropertyValueAboveZero)
只是一个任意的条件语句,与代码的其余部分没有实际关联。我将更新问题以反映这一点。
对于字段/属性的返回值,你提到的是一个很好的观点。然而,它只是在那一刻的值。在确定性代码中,你永远不应该使用这个值(而应该使用其中一个Interlocked方法,如Interlocked.CompareExchange)。但有时在调试中,获取相对值和了解情况可能是有用的。顺便说一下,对于Int32类型的get是线程安全的,但对于Int64类型,你需要使用Interlocked.Read。我已根据你的评论更新了我的回答。
我喜欢你的编辑。CompareExchange对于获取有用的值帮助不大——正如你所指出的,它是“那一刻的值”,与随机值一样好。 (确实,用于调试的只读属性很有用,但很容易开始在代码中使用它...也许使用一些更可怕的属性名称会有所帮助。)
在if条件语句IsPropertyValueAboveZero
中的逻辑很重要,因为你试图在块中使用GlobalID
的当前值,而我之前提到过这是不安全的。不能以这种方式使用GlobalID,你需要额外的锁定逻辑来确保安全。