【问题标题】:How can barriers be destroyable as soon as pthread_barrier_wait returns?一旦 pthread_barrier_wait 返回,障碍如何被销毁?
【发布时间】:2011-08-18 16:48:59
【问题描述】:

此问题基于:

When is it safe to destroy a pthread barrier?

以及最近的 glibc 错误报告:

http://sourceware.org/bugzilla/show_bug.cgi?id=12674

我不确定 glibc 中报告的信号量问题,但根据上面链接的问题,一旦pthread_barrier_wait 返回,大概应该是有效的破坏屏障。 (通常情况下,获得PTHREAD_BARRIER_SERIAL_THREAD 的线程,或者已经认为自己对屏障对象“负责”的“特殊”线程会破坏它。)我能想到的主要用例是当屏障对象时用于同步新线程对创建线程堆栈上数据的使用,防止创建线程返回,直到新线程使用数据;其他障碍的生命周期可能与整个程序的生命周期相同,或者由其他同步对象控制。

无论如何,如何确保一旦pthread_barrier_wait 在任何线程中返回,屏障的破坏(甚至可能解除其驻留的内存的映射)是安全的?似乎其他尚未返回的线程需要检查屏障对象的至少一部分以完成它们的工作并返回,就像在上面引用的 glibc 错误报告中,sem_post 必须检查服务员计数调整信号量值后。

【问题讨论】:

    标签: c pthreads posix race-condition barrier


    【解决方案1】:

    我将通过pthread_barrier_wait() 的示例实现来解决这个问题,它使用了pthreads 实现可能提供的互斥锁和条件变量功能。请注意,这个例子并没有尝试处理性能方面的考虑(具体来说,当等待线程被解除阻塞时,它们在退出等待时都被重新序列化)。我认为使用 Linux Futex 对象之类的东西可以帮助解决性能问题,但 Futex 仍然远远超出我的经验。

    此外,我怀疑此示例是否正确处理信号或错误(如果在信号的情况下处理的话)。但我认为可以添加对这些内容的适当支持作为读者的练习。

    我主要担心该示例可能存在竞争条件或死锁(互斥锁处理比我喜欢的要复杂)。另请注意,这是一个甚至尚未编译的示例。将其视为伪代码。另外请记住,我的经验主要是在 Windows 中 - 我更多地将其视为一种教育机会,而不是其他任何事情。所以伪代码的质量可能很低。

    但是,抛开免责声明不谈,我认为它可以让您了解如何处理问题中提出的问题(即,pthread_barrier_wait() 函数如何允许它使用的 pthread_barrier_t 对象被任何被释放的线程中的一个或多个线程在退出时没有使用屏障对象的危险)。

    这里是:

    /* 
     *  Since this is a part of the implementation of the pthread API, it uses
     *  reserved names that start with "__" for internal structures and functions
     *
     *  Functions such as __mutex_lock() and __cond_wait() perform the same function
     *  as the corresponding pthread API.
     */
    
    // struct __barrier_wait data is intended to hold all the data
    //  that `pthread_barrier_wait()` will need after releasing
    //  waiting threads.  This will allow the function to avoid
    //  touching the passed in pthread_barrier_t object after 
    //  the wait is satisfied (since any of the released threads
    //   can destroy it)
    
    struct __barrier_waitdata {
        struct __mutex cond_mutex;
        struct __cond cond;
        
        unsigned waiter_count;
        int wait_complete;
    };
    
    struct __barrier {
        unsigned count;
        
        struct __mutex waitdata_mutex;
        struct __barrier_waitdata* pwaitdata;
    };
    
    typedef struct __barrier pthread_barrier_t;
    
    
    
    int __barrier_waitdata_init( struct __barrier_waitdata* pwaitdata)
    {
        waitdata.waiter_count = 0;
        waitdata.wait_complete = 0;
        
        rc = __mutex_init( &waitdata.cond_mutex, NULL);
        if (!rc) {
            return rc;
        }
    
        rc = __cond_init( &waitdata.cond, NULL);
        if (!rc) {
            __mutex_destroy( &pwaitdata->waitdata_mutex);
            return rc;
        }
    
        return 0;
    }
    
    
    
    
    int pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned int count)
    {
        int rc;
        
        rc = __mutex_init( &barrier->waitdata_mutex, NULL);
        if (!rc) return rc;
    
        barrier->pwaitdata = NULL;
        barrier->count = count;
        
        //TODO: deal with attr
    }
    
    
    
    int pthread_barrier_wait(pthread_barrier_t *barrier)
    {
        int rc;
        struct __barrier_waitdata* pwaitdata;
        unsigned target_count;
    
        // potential waitdata block (only one thread's will actually be used)
        struct __barrier_waitdata waitdata; 
        
        // nothing to do if we only need to wait for one thread...
        if (barrier->count == 1) return PTHREAD_BARRIER_SERIAL_THREAD;
        
        rc = __mutex_lock( &barrier->waitdata_mutex);
        if (!rc) return rc;
        
        if (!barrier->pwaitdata) {
            // no other thread has claimed the waitdata block yet - 
            //  we'll use this thread's
            
            rc = __barrier_waitdata_init( &waitdata);
            if (!rc) {
                __mutex_unlock( &barrier->waitdata_mutex);
                return rc;
            }
    
            barrier->pwaitdata = &waitdata;
        }
        
        pwaitdata = barrier->pwaitdata;
        target_count = barrier->count;
        
        //  all data necessary for handling the return from a wait is pointed to
        //  by `pwaitdata`, and `pwaitdata` points to a block of data on the stack of
        //  one of the waiting threads.  We have to make sure that the thread that owns
        //  that block waits until all others have finished with the information
        //  pointed to by `pwaitdata` before it returns.  However, after the 'big' wait
        //  is completed, the `pthread_barrier_t` object that's passed into this 
        //  function isn't used. The last operation done to `*barrier` is to set 
        //  `barrier->pwaitdata = NULL` to satisfy the requirement that this function
        //  leaves `*barrier` in a state as if `pthread_barrier_init()` had been called - and
        //  that operation is done by the thread that signals the wait condition 
        //  completion before the completion is signaled.
    
        // note: we're still holding  `barrier->waitdata_mutex`;
        
        rc = __mutex_lock( &pwaitdata->cond_mutex);
        pwaitdata->waiter_count += 1;
        
        if (pwaitdata->waiter_count < target_count) {
            // need to wait for other threads
            
            __mutex_unlock( &barrier->waitdata_mutex);
            do {
                // TODO:  handle the return code from `__cond_wait()` to break out of this
                //          if a signal makes that necessary
                __cond_wait( &pwaitdata->cond,  &pwaitdata->cond_mutex);
            } while (!pwaitdata->wait_complete);
        }
        else {
            // this thread satisfies the wait - unblock all the other waiters
            pwaitdata->wait_complete = 1;
    
            // 'release' our use of the passed in pthread_barrier_t object
            barrier->pwaitdata = NULL;
            
            // unlock the barrier's waitdata_mutex - the barrier is  
            //  ready for use by another set of threads
            __mutex_unlock( barrier->waitdata_mutex);
    
            // finally, unblock the waiting threads
            __cond_broadcast( &pwaitdata->cond);
        }
    
        // at this point, barrier->waitdata_mutex is unlocked, the 
        //  barrier->pwaitdata pointer has been cleared, and no further 
        //  use of `*barrier` is permitted...
        
        // however, each thread still has a valid `pwaitdata` pointer - the 
        // thread that owns that block needs to wait until all others have 
        // dropped the pwaitdata->waiter_count
        
        // also, at this point the `pwaitdata->cond_mutex` is locked, so
        //  we're in a critical section
        
        rc = 0;
        pwaitdata->waiter_count--;
        
        if (pwaitdata == &waitdata) {
            // this thread owns the waitdata block - it needs to hang around until 
            //  all other threads are done
    
            // as a convenience, this thread will be the one that returns 
            //  PTHREAD_BARRIER_SERIAL_THREAD
            rc = PTHREAD_BARRIER_SERIAL_THREAD;
            
            while (pwaitdata->waiter_count!= 0) {
                __cond_wait( &pwaitdata->cond, &pwaitdata->cond_mutex);
            };
    
            __mutex_unlock( &pwaitdata->cond_mutex);
            __cond_destroy( &pwaitdata->cond);
            __mutex_destroy( &pwaitdata_cond_mutex);
        }
        else if (pwaitdata->waiter_count == 0) {
            __cond_signal( &pwaitdata->cond);
            __mutex_unlock( &pwaitdata->cond_mutex);
        }
    
        return rc;
    }
    

    20111 年 7 月 17 日:更新以回应有关流程共享障碍的评论/问题

    我完全忘记了进程之间共享障碍的情况。正如你所提到的,在这种情况下,我概述的想法将非常失败。我真的没有使用 POSIX 共享内存的经验,所以我提出的任何建议都应该持怀疑态度

    总结一下(为了我的利益,如果没有其他人的话):

    当任何线程在pthread_barrier_wait() 返回后获得控制权时,屏障对象需要处于“初始化”状态(但是,该对象上最近的pthread_barrier_init() 设置它)。 API 还暗示,一旦任何线程返回,可能会发生以下一种或多种情况:

    • 再次调用pthread_barrier_wait() 开始新一轮线程同步
    • pthread_barrier_destroy() 在屏障对象上
    • 如果为屏障对象分配的内存位于共享内存区域中,则它可以被释放或取消共享。

    这些事情意味着在pthread_barrier_wait() 调用允许任何 线程返回之前,它几乎需要确保所有等待线程不再在该调用的上下文中使用屏障对象。我的第一个答案是通过在会阻塞所有线程的屏障对象之外创建一组“本地”同步对象(互斥锁和关联的条件变量)来解决这个问题。这些本地同步对象被分配在恰好首先调用pthread_barrier_wait()的线程的堆栈上。

    我认为对于流程共享的障碍也需要做类似的事情。但是,在这种情况下,仅在线程堆栈上分配这些同步对象是不够的(因为其他进程将无权访问)。对于进程共享屏障,必须在进程共享内存中分配这些对象。我认为我上面列出的技术可以类似地应用:

    • 控制本地同步变量(waitdata 块)的“分配”的waitdata_mutex 已经在进程共享内存中,因为它位于屏障结构中。当然,当屏障设置为THEAD_PROCESS_SHARED 时,该属性也需要应用于waitdata_mutex
    • 当调用__barrier_waitdata_init() 来初始化本地互斥量和条件变量时,它必须在共享内存中分配这些对象,而不是简单地使用基于堆栈的waitdata 变量。
    • 当“清理”线程销毁 waitdata 块中的互斥体和条件变量时,它还需要清理该块的进程共享内存分配。
    • 在使用共享内存的情况下,需要有某种机制来保证共享内存对象在每个进程中至少打开一次,并且在每个进程中关闭正确的次数(但之前不能完全关闭)进程中的每个线程都已完成使用它)。我还没想好具体怎么做……

    我认为这些更改将允许该计划在流程共享障碍的情况下运行。上面的最后一个要点是要弄清楚的关键项目。另一个是如何为共享内存对象构造一个名称,该对象将保存“本地”进程共享waitdata。您需要该名称的某些属性:

    • 您希望名称的存储位于struct pthread_barrier_t 结构中,以便所有进程都可以访问它;这意味着对名称长度的已知限制
    • 您希望名称对于对pthread_barrier_wait() 的一组调用中的每个“实例”都是唯一的,因为在所有线程完全退出之前可能会进行第二轮等待第一轮等待(因此为waitdata 设置的进程共享内存块可能尚未释放)。因此,名称可能必须基于进程 ID、线程 ID、屏障对象的地址和原子计数器等内容。
    • 我不知道将名称设为“可猜测”是否存在安全隐患。如果是这样,则需要添加一些随机化-不知道多少。也许您还需要对上面提到的数据以及随机位进行哈希处理。就像我说的,我真的不知道这是否重要。

    【讨论】:

    • 我需要花时间详细阅读这篇文章,但我真的很喜欢将状态保存在服务员堆栈之一而不是屏障对象本身的想法。甚至可以以这样一种方式使用这种方法,即在所有第一组线程完成返回之前,一组新线程就可以开始在屏障上等待。
    • @R.:我相信该示例支持您提到的内容(新线程可以在第一组完全等待之前开始等待),因为 barrier-&gt;pwaitdata 在保持时设置为 NULL barrier-&gt;waitdata_mutex。本质上,将释放等待者的线程在释放该互斥体之前准备屏障对象以供重用。
    • 确实,根据你的想法,我制定了一个基于 futex 的实现,它可以避免在任何线程被解锁后访问原始屏障对象,甚至可以完全避免系统调用,如果它可以通过几次旋转(这个通常可以涵盖同步新线程启动参数的重要 count=2 情况)。
    • 注意:确实需要稍微非常规地使用 futex 来解除对具有屏障实例结构的原始线程的阻塞;我最终使用了相同的int 作为等待标志和“所有其他线程都已完成访问实例”标志。
    • 我遇到了一个问题——这种方法没有考虑进程共享的屏障,事实上如果你天真地将它应用于进程共享的屏障,它会导致内存损坏。你有什么聪明的想法来解决这个问题吗?
    【解决方案2】:

    据我所知,pthread_barrier_destroy 不需要立即操作。您可以让它等到所有仍处于唤醒阶段的线程都被唤醒。

    例如,您可以有一个原子计数器awakening,它最初设置为被唤醒的线程数。然后它将作为pthread_barrier_wait 返回之前的最后一个动作递减。 pthread_barrier_destroy 然后可能会一直旋转,直到计数器下降到 0

    【讨论】:

    • 是否需要在屏障超出范围或释放对象之前调用pthread_barrier_destroy?对此我有点不清楚。如果是这样,我相信这个答案可能是对的。
    • 不幸的是,旋转在这里是一种相当糟糕的行为(例如,当其中一个唤醒线程的优先级低于旋转线程时,它甚至可能导致死锁)。不过,我认为使用服务员计数和 futex(或等效项)也可以。
    • R..,对,我没有想到不同的优先级。因此,有时可能需要yield。当然,在一个体面的系统上,你有这样的东西:) 使用原子计数器变量作为 futex 肯定是一个非常有效的实现。
    • 使用 futex 和等待计数是 glibc 所做的。一旦遇到障碍,该实现似乎在唤醒线程时同步,并确保返回的最后一个线程是返回 PTHREAD_BARRIER_SERIAL_THREAD 的线程,因此到那时(它受锁保护)所有其他线程都将前进,并且不会检查屏障。
    • @nos:我很困惑这是如何工作的,除非你将它与迈克尔的回答结合起来,即在打算最后返回的线程堆栈上使用二级结构。如果任何线程已经从pthread_barrier_wait 返回,我认为没有安全的方法来检查服务员的数量。您可以发出无条件的 futex 唤醒呼叫,但这在 count=2 的屏障中似乎非常浪费,并且它们覆盖的时间间隔很短,否则它们可能通过几次旋转就可以通过。
    猜你喜欢
    • 1970-01-01
    • 2022-07-22
    • 1970-01-01
    • 2018-10-22
    • 2018-08-28
    • 2012-08-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多