【问题标题】:Thread Safe Counter used by Recursive Call to Function递归调用函数使用的线程安全计数器
【发布时间】:2018-02-24 17:42:41
【问题描述】:

在使用一个名为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

【问题讨论】:

  • 我看到了Interlocked.Increment,最终我会需要它,但问题是该计数器有多个实例,并确保每次通过时它都会增加,尤其是在离开递归问题时
  • @mjwills 这将防止竞争条件,但如果您希望线程维护可能相互偏移的独立计数器,则无济于事。
  • Interlocked.Increment 适用于递增GlobalID,但大概您不希望在这一行出现重复:ID = GlobalID;,因此您将锁定整个块。
  • @JasonR。假设这项工作是批处理的而不是连续传入的,您可以为每个线程维护一个单独的计数器,并在您加入所有线程时聚合计数。
  • @JasonR。如果您不知道自己真正想要实现什么,则很难建议正确的实施(因此它使帖子不太适合 SO,也可以以回答标题的方式回答,可能对您的情况没有帮助- 这有点发生了)......瑞安和我的回答,它应该向您展示您将面临的问题,但我认为我们的任何一个答案都不能直接适用于您的样本(也都回答了标题中的问题) .

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


【解决方案1】:

对于线程安全计数,使用互锁方法: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 语句围绕需要以同步方式获取值的逻辑。 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
    }
}

【讨论】:

  • Answer 有一些优点,但是“您可以通过属性公开该字段”是一个糟糕的建议 - 您也可以使用 public int GlobalID { get { return random.Next(1000); } } 代替。当多个线程递增变量时,无法获取有意义的变量值。
  • (thisClass.IsPropertyValueAboveZero) 只是基于条件语句的任意表达方式。它对其余代码没有有意义的引用。我将更新问题以反映这一点
  • @AlexeiLevenkov 您对字段/属性的返回值有一个很好的看法。然而,它是那个时刻的价值。您永远不应该在确定性代码中使用该值(改用其中一种互锁方法,例如Interlocked.CompareExchange)。我发现它有时在调试中很有用,以了解相对值和正在发生的事情。作为旁注,Int32get 是线程安全的,但对于Int64,您需要使用Interlocked.Read。我已根据您的评论更新了我的答案。
  • @Ryan 喜欢编辑。 CompareExchange 获得 useful 价值也无济于事 - 正如您指出的那样,它是“当时的价值”,与随机价值一样好。 (确实用于调试的只读属性很有用,但是在代码中开始使用它太诱人了......也许一些更可怕的属性名称会有所帮助)
  • @JasonR。 if 条件 IsPropertyValueAboveZero 中的逻辑很重要,因为您试图在块中使用 GlobalID 的当前值,而这正是 @AlexeiLevenkov 所警告的。以这种方式使用 GlobalID 是不安全的,您需要额外的锁定逻辑。
【解决方案2】:

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

将变量存储在静态中:

public static int Bob;

然后在静态函数中递增:

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

任何时候你想增加,请致电IncrementBob

【讨论】:

【解决方案3】:

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

因为您只需要在增加值时读取值,而不是 Interlocked.Increment 就足够了。确保从不直接检查或使用 value 或 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”分开会很有用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多