Algorithm 正常的union/find算法在没有任何额外工作的情况下是线程安全的吗?

Algorithm 正常的union/find算法在没有任何额外工作的情况下是线程安全的吗?,algorithm,multithreading,data-structures,lock-free,Algorithm,Multithreading,Data Structures,Lock Free,对于单线程情况,标准数据结构具有非常好的运行时间(有效地O(1))。但是,在多线程情况下,它的有效性/性能如何?我认为它是完全有效的,即使没有锁定或任何原子操作,除了原子指针大小的写入 有人认为以下逻辑有问题吗 首先,我假设指针大小的写操作是原子的。由此不难看出,您可以在多个线程中安全地运行find函数,因为将发生的唯一更新都设置为相同的值。如果允许find函数返回调用时为真的答案(与返回时相反),那么不难认为可以同时运行多个find和单个union;finds的参数没有改变,union只更新根

对于单线程情况,标准数据结构具有非常好的运行时间(有效地
O(1)
)。但是,在多线程情况下,它的有效性/性能如何?我认为它是完全有效的,即使没有锁定或任何原子操作,除了原子指针大小的写入

有人认为以下逻辑有问题吗

首先,我假设指针大小的写操作是原子的。由此不难看出,您可以在多个线程中安全地运行
find
函数,因为将发生的唯一更新都设置为相同的值。如果允许
find
函数返回调用时为真的答案(与返回时相反),那么不难认为可以同时运行多个
find
和单个
union
find
s的参数没有改变,
union
只更新根,
find
s从不更新根

至于剩下的案例(几个
union
s),我认为也可以,但我不太确定

顺便说一句:我并不要求解决方案和单线程版本一样高效。(为了避免锁/原子,我也愿意放弃全局相干态。)


编辑:再看一看,许多并集案例都不起作用,因为如果不是新根的那一方与其他对象(也不是根)并集,则可以将其从第二个并集的另一方断开

A = find(a)  // union 1
B = find(b)  // union 1
----
X = find(x)  //   union 2 (x == a) -> (X == A) -> (X.par aliases A.par)
Y = find(y)  //   union 2
X.par = Y    //   union 2
----
A.par = B    // union 1
这可以通过以下方法避免:


可用于此结构的同步类型与类似。性能将取决于执行的联合数,因为查找操作将停止


我不确定您是否可以同时运行多个查找和单个联合,因为联合操作的最后一个案例不是原子的。

有点晚,但供将来参考。使用原子操作,可以构建不相交的集合数据结构。不变的是,每个集合由其最小的成员表示,这允许避免由于竞争条件而产生的循环

// atomic_compare_and_exchange(unsigned int *data, unsigned int new_value, unsigned int comparator)


// "The result used to be the root of v once"
static unsigned int GetSet(volatile unsigned int *H_RESTRICT set, const unsigned int v)
{
  unsigned int next;
  unsigned int root = v;
  unsigned int prev = v;

  while (root != (next = set[root]))
  {
    /* Update set[prev] to point to next instead of root.
      * If it was updated by some other thread in the meantime, or if this
      * is the first step (where set[prev]==next, not root), ignore. */
    atomic_compare_and_exchange(set + prev, next, root);

    prev = root;
    root = next;
  }

  /* Update the path to the root */

  return root;
}

// FALSE == "They used not to be in the same set"
// TRUE == "They are in the same set, and will be forever"
static HBOOL IsInSameSet(volatile unsigned int *H_RESTRICT set, unsigned int v1, unsigned int v2)
{
  do
  {
    v1 = GetSet(set, v1);
    v2 = GetSet(set, v2);
  } while (set[v1] != v1 || set[v2] != v2);

  return v1 == v2;
}

static void Union(volatile unsigned int *H_RESTRICT set, unsigned int v1, unsigned int v2)
{
  unsigned int result;

  do
  {
    v1 = GetSet(set, v1);
    v2 = GetSet(set, v2);
    if (v1 == v2)
    {
      /* Already same set. This cannot be changed by a parallel operation. */
      break;
    }
    if (v1 < v2)
    {
      /* Make sure we connect the larger to the smaller set. This also avoids
       * cyclic reconnections in case of race conditions. */
      unsigned int tmp = v1;
      v1 = v2;
      v2 = tmp;
    }

    /* Make v1 point to v2 */
    result = atomic_compare_and_exchange(set + v1, v2, v1);
  } while (result != v1);
}
//原子比较和交换(无符号整数*数据、无符号整数新值、无符号整数比较器)
//“结果曾经是v的根一次”
静态无符号整数GetSet(volatile unsigned int*H_RESTRICT set,const unsigned int v)
{
无符号整数next;
无符号整数根=v;
无符号int-prev=v;
while(root!=(next=set[root]))
{
/*将集合[prev]更新为指向下一个而不是根。
*如果它同时被其他线程更新,或者
*是第一步(其中set[prev]==next,而不是root),忽略*/
原子比较和交换(set+prev,next,root);
prev=根;
根=下一个;
}
/*更新根目录的路径*/
返回根;
}
//FALSE==“它们以前不在同一组中”
//TRUE==“它们在同一组中,并且将永远存在”
静态HBOOL IsInSameSet(易失性无符号整数*H_限制集,无符号整数v1,无符号整数v2)
{
做
{
v1=GetSet(set,v1);
v2=GetSet(set,v2);
}while(set[v1]!=v1 | | set[v2]!=v2);
返回v1==v2;
}
静态void并集(volatile unsigned int*H_RESTRICT set,unsigned int v1,unsigned int v2)
{
无符号整数结果;
做
{
v1=GetSet(set,v1);
v2=GetSet(set,v2);
如果(v1==v2)
{
/*已设置相同。这不能通过并行操作更改*/
打破
}
如果(v1
如果您所指的非原子联合是秩部分,是的,我就在这里。但如果你放弃了,我认为你不会失去太多。请参阅编辑。o此问题与1-union/many find案例无关,因为find将永远不会找到当前组中不是父项的内容,并且只要指定的值是当前组中的父项,任何赋值都是有效的。假设在同一集中有2个元素,并且对它们进行了查找。如果在第二次查找时,父项会正确更改在到达父级之前,结果可能会混乱。因此,在进行联合时,您需要锁定集合。这个示例可以用另一种方法解决,但我没有找到更好的方法。我不同意:在查找开始时,树中的每个节点都指向更靠近根的某个对象。在更新步骤中,指针将从有效的内容更新为其他同样有效的内容。(顺便说一句,我假设使用NULL表示根)查找/并集的工作原理几乎相同。我认为即使使用CAS,并集A和B的两次并行尝试也可能会在A和B之间创建一个循环。
// atomic_compare_and_exchange(unsigned int *data, unsigned int new_value, unsigned int comparator)


// "The result used to be the root of v once"
static unsigned int GetSet(volatile unsigned int *H_RESTRICT set, const unsigned int v)
{
  unsigned int next;
  unsigned int root = v;
  unsigned int prev = v;

  while (root != (next = set[root]))
  {
    /* Update set[prev] to point to next instead of root.
      * If it was updated by some other thread in the meantime, or if this
      * is the first step (where set[prev]==next, not root), ignore. */
    atomic_compare_and_exchange(set + prev, next, root);

    prev = root;
    root = next;
  }

  /* Update the path to the root */

  return root;
}

// FALSE == "They used not to be in the same set"
// TRUE == "They are in the same set, and will be forever"
static HBOOL IsInSameSet(volatile unsigned int *H_RESTRICT set, unsigned int v1, unsigned int v2)
{
  do
  {
    v1 = GetSet(set, v1);
    v2 = GetSet(set, v2);
  } while (set[v1] != v1 || set[v2] != v2);

  return v1 == v2;
}

static void Union(volatile unsigned int *H_RESTRICT set, unsigned int v1, unsigned int v2)
{
  unsigned int result;

  do
  {
    v1 = GetSet(set, v1);
    v2 = GetSet(set, v2);
    if (v1 == v2)
    {
      /* Already same set. This cannot be changed by a parallel operation. */
      break;
    }
    if (v1 < v2)
    {
      /* Make sure we connect the larger to the smaller set. This also avoids
       * cyclic reconnections in case of race conditions. */
      unsigned int tmp = v1;
      v1 = v2;
      v2 = tmp;
    }

    /* Make v1 point to v2 */
    result = atomic_compare_and_exchange(set + v1, v2, v1);
  } while (result != v1);
}