【问题标题】:Solve race condition during semaphore initialization在信号量初始化期间解决竞争条件
【发布时间】:2012-09-17 11:52:07
【问题描述】:

我有一个必须在线程之间共享的数组,受信号量保护。我把初始化代码放在了一个可以多次调用的函数中,一个“构造函数”,如下:

#include <stdbool.h> //for bool
#include <semaphore.h>

sem_t global_mutex;
char global_array[N]; // Protected with global_mutex

struct my_struct *new_my_struct(){
    static bool is_init = false; // This will be initialized only once, right?
    if (!is_init){                         // 1
        sem_init(&global_mutex, 0, 1);     // 2
        sem_wait(&global_mutex);           // 3
        if (!is_init){                     // 4
           is_init = true;                 // 5
           ... initialize global_array ... // 6
        }
        sem_post(&global_mutex);           // 7
    }

    ... proceed on the create and return a my_struct pointer ...
}

在理想情况下,线程将从 1 运行到 7,初始化数组并退出临界区。即使另一个线程在 2 中停止,在 4 中的测试也会是错误的,并且不会覆盖数组。我没有想过如果线程卡在 1 并重新初始化信号量会发生什么,但我相信只要is_init 由第一个运行的线程设置为 true 就不会引起太大的关注!

现在,如果一个线程在 4 处停止,另一个线程从开始运行到完成,初始化并填充 global_array,则存在竞争条件。当线程停止在 4 运行时,它会重新初始化数组并删除第一个线程存储的状态。

我想知道是否有任何方法可以避免这种竞争条件(也许巧妙地使用static?)或者我是否应该将初始化代码与构造函数分开并在主线程中使用它,当没有并发。

此代码正在使用中,我还没有遇到竞争状况。但是,据我所知,这是可能的,我希望更正它。

【问题讨论】:

  • 由于数组是全局的,为什么不初始化它和信号量before你开始你的线程?那么你就不用担心初始化代码中的竞争条件了。
  • 真的有必要检查 (!is_init) 两次吗? ;o 是的,静态布尔值将被视为函数范围内的全局变量。
  • @Magtheridon96 是的,一个线程可以在 3 处停止,另一个线程继续到 5。然后第一个线程不会进入 if,第二个线程将继续初始化数组。糟糕,您刚刚发现了一个错误!第一个线程可以继续使用未初始化数组的函数!
  • @JoachimPileborg 出于美学和易用性考虑,我不希望这样做。您不会看到许多标准库要求您在使用其他功能之前在主线程中调用初始化函数,并且不要求用户记住这样做更安全。
  • 我很难想到任何没有具有某种打开、创建或初始化功能的库。对于一些你甚至必须调用一个 init 函数之前你可以调用 open 函数(例如 MySQL)。

标签: c concurrency semaphore


【解决方案1】:

如果信号量的真正用途是作为互斥体,请使用pthread_mutex_t。这些可以静态初始化,这样你的问题就会消失。

语法是

pthread_mutex_t global_mutex = PTHREAD_MUTEX_INITIALIZER;

如果您确实需要全局对象的动态初始化,请查看pthread_once。这是 POSIX 为此类任务预见的类型 (pthread_once_t) 和功能。

【讨论】:

  • 我需要销毁以这种方式初始化的互斥锁吗?
  • @BrunoKim,不,你不应该调用 destroy 。每个 init 调用应该只有一个销毁。
【解决方案2】:

有几种方法可以进行线程安全的延迟初始化,但这不是其中之一。

pthread_once 是一种方式,使用实际上是互斥体(静态初始化)的全局互斥体来同步初始化是另一种方式。实现可以保证 static 局部变量的线程安全初始化,但不是必须的(至少,它们在 C11 之前没有,我还没有检查过)。

但是,尽管同步实际初始化,双重检查锁定不能保证在 C 或 Posix 中工作。检查一个线程中的标志是一场数据竞赛,该标志是在另一个线程中设置的,而 both 线程中没有某种同步。在初始化已经完成的常见情况下,pthread_once 的实现应该尽可能快。 如果您的实现保证函数范围静态变量的线程安全初始化,那么这也将尽其所能。除非您真的知道自己在做什么(例如,您正在为某个新系统自己实现 pthread_once),否则请使用其中一种,而不是自己尝试避免在常见情况下代价高昂的锁定。

【讨论】:

  • 一般的双重检查可以吗?我首先在 Java 中学习它,并在我的代码中使用它来避免在读取期间锁定整个数组,如下所示:for element in array{ if (element is desired){ lock(); if (element is desired){ do_stuff(element);} unlock(); } }
  • @BrunoKim:只要element_is_desired 被限制在代码正在运行的线程中就可以了。但是你通常不需要第二个if,因为通常会有调用lock() 不可能改变element_is_desired。如果element_is_desired 是从另一个线程更改的,那么不,这是不正常的。在其他线程认为它已将其设置为“true”之后,您的线程可能会看到值“false”。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-03
  • 1970-01-01
  • 2019-08-20
  • 1970-01-01
  • 2021-09-27
  • 1970-01-01
相关资源
最近更新 更多