函数递归调用使用的线程安全计数器

Thread Safe Counter used by Recursive Call to Function

在使用名为 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)  //Recursive Call
     }
 }

我基本上使用 GlobalID 作为每次调用函数时的递增计数器。计数器在第一次执行时被分配一个起始位置。我正在使用全局变量,因为我想确保无论执行进入还是离开递归调用,每次传递都准确增加 ID。从分配全局变量起始位置的 ForEach 循环调用此函数(第一次......)

我的目标是在初始调用中使用 Parallel.ForEach 而不是常规的 For Each 循环。我的问题与柜台有关。我不知道如何在多个线程中管理该计数器。如果我将它作为变量传递给函数,我相信我将有一个不准确的较低/已用数字离开递归循环。全局变量确保下一个数字高于前一个数字。 thisClass.IsPropertyValueAboveZero 只是描述基于条件语句的动作的任意方式。它对其余代码没有有意义的引用。

如果我有多个线程,它们的计数器的起始位置不同,我该如何使这个线程安全?我目前看到的唯一方法是手动编写同一函数和计数器的多个版本并使用 TaskFactory

对于线程安全计数,使用互锁方法:System.Threading.Interlocked.Increment(ref globalID)

private static int globalID;

// Be very careful about using a property to expose the counter
// do not *use* this value directly, but can be useful for debugging
// instead use the return values from Increment, Decrement, etc.
public int UnsafeGlobalID { get { return globalID; } }

注意:您需要为 ref 使用一个字段。可以通过 属性 公开该字段,但在代码中可能会出现问题。最好使用 Interlock 方法,例如 Interlocked.CompareExchange 或明确的 lock 语句围绕需要以同步方式获取值的逻辑。 IncrementDecrement 等的 return 值通常应该在逻辑中使用。

My problem deals with the counter. I don't know how to manage that counter within multiple threads. If I pass it as a variable to the function, I believe I will have a inaccurate lower / used number leaving the recursive loop. The global variable ensures the next number is higher than the previous number.

If I have multiple threads that have different starting positions for their counter, how do I accomplish making this thread safe? The only way I see at the moment is manually writing multiple versions of the same function and counter and using TaskFactory

MyFunction(newClass, ID) 的递归调用将具有 ID 的值,但我不确定 If (thisClass.IsPropertyValueAboveZero) 应该做什么。是为了确保你有一个非零的起点吗?如果是这样,最好在这个函数之外的初始调用之前确保它是非零的。

此外,foreach 循环中的逻辑对我来说没有意义。在 MyClass newClass = new MyClass(GlobalID, ID) 中,ID 将是参数值,或者如果 IsPropertyValueAboveZero 为真,它将是 GlobalID 的当前值。因此 ID 通常会小于 GlobalID,因为 GlobalIDforeach 循环之前递增。我认为您在不需要时通过了 GlobalID

// if IsPropertyValueAboveZero is intended to start above zero
// then you can just initialize the counter to 1
private static int globalID = 1;

public void MyFunction(myClass thisClass, int id)
{
    // ListOfMyClass and ListOfClasses are probably not thread-safe
    // and you may need to add locks around the Add and get a copy
    // of ListOfClasses before the foreach enumeration
    // You may want to look at 
    ListOfMyClass.Add(thisClass); 

    foreach (class someClass in ListOfClasses)
    {
        int newId = System.Threading.Interlocked.Increment(ref globalID);
        MyClass newClass = new MyClass(newId);
        MyFunction(newClass, newId);  //Recursive Call
    }
}

对于单个计数器的线程安全整数递增,请考虑使用 Interlocked.Increment

静态存储变量:

public static int Bob;

然后在静态函数中递增它:

public static int IncrementBob()
{
    return Interlocked.Increment(ref Bob);
}

任何时候你想递增,调用IncrementBob

如果您使用变量作为 Interlocked.Increment 的反标准用法(请参阅 C# Thread safe fast(est) counter)。由于您的代码实际上想要使用计数器的值,因此您必须非常小心才能获得正确的值,因为同时必须保护从该值读取的值。

因为你只需要在递增时读取值,所以 Interlocked.Increment 就足够了。确保从不检查或使用值或直接GlobalID,而多个线程可以修改它:

var ID = someValue; 
var newValue = Interlocked.Increment(ref GlobalID);
if (thisClass.IsPropertyValueAboveZero)
{
      // you can't use GlobalID directly here because it could be already incremented more
      ID = newValue - 1; // note -1 to match your original code.

}

否则代码会变得更加棘手 - 例如,如果您只是有时想增加全局值但每次调用都使用它(这可能不是您的情况,因为示例显示递增总是使用较少)而不是您必须传递当前值分别到你的功能。同样在这种情况下,传递给函数的值和实际计数器将彼此无关。如果用例不是 "use value only when incremented",您可能应该查看您想要实现的目标——很可能您需要一些其他数据结构。请注意,使用 lock 来保护读写计数器不会解决线程安全问题,因为您无法预测您将要读取的值(其他线程可能会多次递增)。

注意:post 中的示例代码显示了一些旧 ID 值和最新计数器值的用法不明确。如果代码确实反映了您想要执行的操作,那么将 "counting number of calls" 与 "object ID" 分开会很有用。