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

时间:2017-09-15 13:47:01

标签: c# multithreading recursion task-parallel-library taskfactory

使用我用作计数器的名为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

3 个答案:

答案 0 :(得分:4)

对于线程安全计数,请使用Interlocked方法: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使用字段。可以通过属性公开字段,但在代码中可能会有问题。最好在需要以同步方式获取值的逻辑周围使用Interlocked.CompareExchange或显式lock语句之类的Interlock方法。 IncrementDecrement等的返回值通常应该在逻辑内部使用。

  

我的问题涉及柜台。我不知道如何在多个线程中管理该计数器。如果我将它作为变量传递给函数,我相信我会有一个不准确的低/使用数字离开递归循环。全局变量确保下一个数字高于前一个数字。

     

如果我的多个线程的计数器具有不同的起始位置,我该如何才能使这个线程安全?我目前看到的唯一方法是手动编写同一函数和计数器的多个版本并使用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 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);  //Recursive Call
    }
}

答案 1 :(得分:2)

如果您使用变量作为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.

}

否则代码会变得更加棘手 - 例如,如果您有时只想增加全局值但是将其用于每次调用(这可能不是您的情况,因为示例显示增量始终使用频率较低),而不是必须将当前值传递给您功能分开。同样在这种情况下,传递给函数和实际计数器的值将彼此无关。如果用例不是"只有在递增时才使用值"你可能应该回顾一下你想要实现的目标 - 很可能你需要一些其他的数据结构。请注意,使用lock来保护读取和写入以防止解决线程安全,因为您无法预测您将要读取的值(可能会被其他线程多次递增)。

注意:帖子中的示例代码显示了一些旧ID值和最新计数器值的不清楚用法。如果代码实际上反映了你想要做什么分离"计算呼叫次数"来自"对象ID"会有用的。

答案 2 :(得分:-1)

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

将变量存储在静态中:

public static int Bob;

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

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

无论何时想要增加,请致电IncrementBob