【问题标题】:Thread locking a (possible) destoyed mutex线程锁定(可能)破坏的互斥锁
【发布时间】:2021-11-07 17:50:32
【问题描述】:

我有 2 个线程安全列表(相同类型),其中每个节点都有一个互斥锁(我知道这不是最佳解决方案,但这就是我被要求做的)。我有以下功能,我想匹配 2 个节点,每个节点来自不同的列表。逻辑是我从第一个列表中传递一个节点,然后检查第二个列表中的所有节点。

这里是节点的类型和列表:

typedef struct node_type{
    //some data;
    struct node_type *next;
    pthread_mutex_t lock;
}node_t;
typedef struct list_type{
    node_t *head;
    pthread_mutex_t lock;
}list_t;

这是函数的代码:

node_t *find_match(list_t *l,node_t *n)
{

    node_t *cur,*prev;

    pthread_mutex_lock(&l->lock);
    if(l->head == NULL)
    {
        pthread_mutex_unlock(&l->lock);
        return NULL;
    }
    else
    {
        cur = l->head;
        while(cur != NULL)
        {
            pthread_mutex_lock(&cur->lock);
            if(n != NULL)
            {
                pthread_mutex_lock(&n->lock);
                //comparing the data and checks for match.
            }
            else
            {
                //some message or something
                return NULL;
            }
        if(cur == l->head)
        {
            pthread_mutex_unlock(&l->lock);
        }
        else
        {
            pthread_mutex_unlock(&prev->lock);
            prev = cur;
            cur = cur->next;
        }
        pthread_mutex_unlock(&prev);
        return NULL;
    }
}

我想提醒您注意 if 语句“n != NULL”。在那里,如果 n 不为 NULL,那么线程将尝试锁定节点的互斥锁。如果另一个线程拥有它并试图删除该节点,会发生什么?我能以某种方式避免它吗?有没有更好的写法?

【问题讨论】:

  • 我首先建议对代码示例进行一些清理:(1):末尾缺少一个右大括号(2):请确保解锁您使用@987654324锁定的列表锁@在方法的每个出口。例如,在您返回“一些消息或某事”之前。 (3)由于n != NULL在函数内部永远不会改变,所以也可以在锁定列表之前放在函数的开头。

标签: c multithreading mutex


【解决方案1】:

问题如果另一个线程拥有它并试图删除该节点,会发生什么?

非常简短的回答发生的事情是不确定的,可能是灾难性的。”

简短回答使用在另一个线程中删除的节点的互斥锁会导致未定义的行为。当其他线程在删除期间使用互斥体并且独立于它是在锁定状态还是解锁状态下删除时,也是如此。

首先:这是一个很好的问题!

我们先解释一下“删除”是什么意思,这样你就可以判断它是否适用于你的情况。其次,解释了为什么使用已删除节点的互斥锁会导致未定义的行为,从而导致不正确的行为(并可能导致灾难),即使在 find_match(l,n 的示例实现中同时使用该互斥锁进行删除和匹配也是如此)。第三,探索了一些解决方案,尽管您可能不喜欢这些答案。

删除

假设删除一个对象也会释放它的内存。 在 C 中,这应该通过 malloc()free() 或类似的。

在使用自定义(de)分配器或所有节点在所有节点处理之前分配、在所有节点处理之后释放并且从不重用_。其中“重用”是指注释被“删除”后分配给node->nextlist->head。在这种“静态”的情况下,删除后一定要解锁节点的互斥锁,否则pthread_mutex_lock(deleted_node)中的所有线程都会耐心等待很长时间......

为什么会出现未定义的行为?

释放内存,例如调用free(node),必须被视为销毁该内存:在销毁之前写入其中的值必须被视为丢失,而在销毁之后从其中读取的值应该被认为是未定义的或“随机的”。 C 语言和运行时根本不提供任何保证可以在销毁内存时传递信息,例如,因为内存被重用或分配实现向其写入值以进行簿记。

因此,任何依赖于跨内存释放传递信息的机制都会出现未定义的行为,而且肯定是不正确的。

因此,依赖于可能被删除/释放的对象内部的互斥体的机制也会遭受未定义和不正确的行为。由于行为未定义,释放节点的一方是否使用互斥锁、锁定或解锁都没有关系。该行为只是不同地未定义,请查看“锁定删除和魔法值”以获得更多详细信息。

避免这种情况

我不完全知道你被“要求做什么”。什么是固定的,你可以改变什么?取决于此,解决方案的范围可以从“无”到“丑陋”到“出色”的解决方案。

你当前的实现试图通过使用 n->mutex 来防止节点 n 被删除; n 的一部分。如上所述,这会导致不确定的行为和可能的灾难。

为了防止删除节点n,你必须使用超出它的节点之外的东西,让我们将它定义为ownerowner( n)。在您的示例中,如果 l_n->headn,则 owner(n) 是列表 l_n,或者l_n 中的节点 p,其中 p->nextpl_np 都有一个互斥体,因此在访问 n 之前锁定该互斥体可以起作用。当然,删除 n 的函数应该锁定同一个互斥体。

但是,如果您不应该更改 find_match(l,n) 的函数签名并且也不应该更改节点和列表的数据结构,则没有办法你可以知道owner(n)。毕竟,n 没有链接到它的所有者,所有者也不是函数参数。 在这种情况下,解决问题的唯一正确或安全的方法是拥有一种全局锁 - 不是节点或列表的一部分 - 保护 的删除和 每个 匹配n 与另一个节点。嗯。

如果允许您将签名更改为 find_match(l,n,o),其中 o 是所有者,这似乎是一个解决方案。但是,除非 o 是拥有 n 本身的列表,否则会出现相同的所有权问题递归:您不能信任节点的互斥锁p 具有 p->next == n,除非您锁定了 owner(p) 的互斥锁。你没有。这意味着您还应该将 owner(p) 作为参数。换句话说:它不会解决你的问题。

另一种方法是使用 find_match(l,n,list_that_contains_n) 并在遍历列表、匹配列表中的项目或删除/添加元素时始终锁定完整列表(锁定当然,在 llist_that_contains_n 上)。这样,您根本不需要来自节点的互斥锁。这加速了函数本身的遍历、删除和添加,但减少了对它们的并发访问。这取决于领域和用例是更好还是更差。 从 list_that_contains_n->head 开始锁定每个元素是可行的,但如果要正确执行此操作,您必须锁定 list_that_contains_n 中在 之前的所有元素>n。原因与前面提到的递归 owner 参数的原因类似。可能有一个解决方案不需要锁定所有这些元素,但我想它可能不是很可读。 如果有人可以在不锁定 n 之前的所有元素的情况下给出一个可读、正确、经过验证和有效的示例:那就太好了!

如果您可以更改节点和列表的数据结构,则可以添加对所有者的反向引用,这很有趣,因为所有者既可以是列表也可以是节点。更好地将问题重组为使用完整的双向链表并为此找到正确的实现。

结论

如您所见,您提出了一个有趣的问题。如果你能提供更多关于你被要求做什么的信息,我想有人——也许是我——可以给你一个更准确或更合适的答案!

锁定删除和魔法值

但是如果我在删除之前锁定节点呢?

假设删除和释放节点的线程首先锁定节点的互斥体。

如果您在解除分配后 解锁节点,pthread_mutex_unlock(&node->lock) 会访问没收内存,可能会损坏程序的其他部分。祈祷内存不被重复使用。

如果您解锁节点,可能会发生几件事情。如果执行 find_match(l,node) 的线程恰好在 pthread_mutex_lock(&node->lock) 中,如果内存没有被重用,它将永远等待。如果内存重用,那么如果线程唤醒会发生什么是不确定的,因为互斥锁很可能被其他东西覆盖。祈祷 pthread_mutex_lock(&node->lock) 只返回一个错误代码...

但是如果我在节点锁中放置一个魔法值呢?

在 C 语言中不能保证只为 node->lock 分配一个值会被另一个线程看到。为了争论,我们假设您的平台和架构有这样的保证。

内存仍可能被重用,因此仍不确定会发生什么。

另一个问题是:你会在节点锁中放入什么魔法值?互斥锁的实现是特定于平台的。并且 POSIX 线程只定义了一个 INITIALIZED 互斥锁的值,而不是用于应该使用的互斥锁。您可以尝试 pthread_mutex_destroy(node->lock),但同样,这对重用内存没有任何帮助。

【讨论】:

  • 你的比例很有趣。我不会详细介绍。我给你一张大图。我必须为我的本科生做一个股市模拟。在那里,我必须使用两个列表,一个包含买入股票命令,另一个包含卖出股票命令。有 N 个线程(应该适用于任何数量)读取列表并尝试查找买卖命令之间的匹配项。同时,虽然没有直接询问,但我们的教授告诉我们,删除节点也必须是可能的,例如,如果输入命令的用户改变了主意。
  • 我的想法是“如果我有用户尝试删除节点,而它正在执行匹配搜索,那么我应该在尝试锁定它之前检查它是否被删除。但是如果我尝试锁定它然后它被删除了怎么办,因为执行删除的线程首先得到它?另外,我注意到检查它是否为 NULL 是不正确的,因为它不会是 NULL,但它会指向一个未分配的内存地址。因此,它会再次尝试读取该地址的内容,但会发生意想不到的事情。
  • 我想过使用一个布尔变量来指示它何时处于匹配状态,但可以在匹配过程中完成删除。例如,假设用户添加了一条命令并想要删除它,因为他给出了错误的股票数量(他想买/卖多少)。当他执行删除命令时,线程可能会开始搜索该节点的匹配项。它可能会找到匹配项,也可能不会。那我为什么要让这个变量停止删除,最后没有找到匹配项呢?
  • 关于双重列表,我想过,但是我发现它很难实现,因为我对制作数据结构和大项目不是很熟悉,尤其是像这样的语言C.
  • 如果删除读取的比率非常小,您也可以应用完全锁定两个列表的模式,但是,对仅遍历列表的函数使用读取锁定,例如 find_match (l,n) 并为删除节点的函数使用写锁。由于列表的状态是一致的,您还可以大大简化 _find_match(l,n)。 Pthread 提供了使用读写锁的功能,尽管我没有研究过它们,因为 C++ 提供了这些内存模型和并发工具作为语言规范的一部分;-) 祝你好运!
【解决方案2】:

根据doc of pthread_mutex_lock,如果“互斥锁指定的值不引用已初始化的互斥锁对象”,它将返回EINVAL。所以大概你可以在释放节点之前取消lock 字段。像这样的:

n->lock = NULL;
free(n); // Or whatever you mean by destroying the node

然后当您使用 mutex_lock 时,您可以检查它的返回值(无论如何这是一个好习惯)。

【讨论】:

  • 问题是其他线程是否会看到n->lock的新值是不确定的,也许他们只能看到一部分;两者都会导致混乱。确保这个安全的唯一方法是确保find_match 和节点被销毁的地方在同一个互斥体中(除了其他问题)。
  • 没有什么可以阻止 malloc() 重用相同的内存位置(作为已删除的节点)和一些其他线程向它写入不同的值。这可能会导致灾难性的后果。
  • 互斥锁的实现是特定于平台的,因此假设未初始化的值是危险的。您可以分配 PTHREAD_MUTEX_INITIALIZER,而不是像 PTHREAD_MUTEX_UNINITIALIZED 这样的东西。分配前者将使 pthread_mutex_lock() 改变可能已经被其他东西使用的内存位置,再次导致灾难性的结果。
  • 由于我之前的cmets中的三个原因,你的回答是一个危险的建议,即使是真的你也应该经常检查函数结果。这就是我会否决它的原因。
  • 要评论我关于“其他线程是否甚至会看到 n->lock 的新值”的第一条评论,确实 pthread_mutex_lock() 可能确保互斥锁是 read 但是,正确地分配一个互斥体的值,n->lock = ....; 并不能确保该值或其部分何时可以被其他线程读取。
猜你喜欢
  • 2012-12-25
  • 1970-01-01
  • 2013-01-31
  • 1970-01-01
  • 2021-02-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多