【问题标题】:Win32 threads dying for no apparent reasonWin32 线程无缘无故死亡
【发布时间】:2011-02-14 14:26:34
【问题描述】:

我有一个程序会生成 3 个工作线程,它们会进行一些数字运算,并等待它们完成,如下所示:

#define THREAD_COUNT 3
volatile LONG waitCount;
HANDLE pSemaphore;

int main(int argc, char **argv)
{
    // ...

    HANDLE threads[THREAD_COUNT];
    pSemaphore = CreateSemaphore(NULL, THREAD_COUNT, THREAD_COUNT, NULL);
    waitCount = 0;

    for (int j=0; j<THREAD_COUNT; ++j)
    {
        threads[j] = CreateThread(NULL, 0, Iteration, p+j, 0, NULL);
    }
    WaitForMultipleObjects(THREAD_COUNT, threads, TRUE, INFINITE);

    // ...
}

工作线程在代码中的某些点使用自定义的 Barrier 函数等待所有其他线程到达 Barrier:

void Barrier(volatile LONG* counter, HANDLE semaphore, int thread_count = THREAD_COUNT)
{
    LONG wait_count = InterlockedIncrement(counter);
    if ( wait_count == thread_count )
    {
        *counter = 0;
        ReleaseSemaphore(semaphore, thread_count - 1, NULL);
    }
    else
    {
        WaitForSingleObject(semaphore, INFINITE);
    }
}

(基于this answer实现)

程序偶尔会死锁。如果那时我使用 VS2008 来中断执行并在内部进行挖掘,那么在 Barrier()Wait... 行上只有 1 个工作线程在等待。 waitCount 的值始终为 2。

为了让事情更尴尬,线程工作得越快,它们就越有可能死锁。如果我在 Release 模式下运行,死锁会出现 10 次中的 8 次。如果我在调试模式下运行并在线程函数中放置一些打印以查看它们挂起的位置,它们几乎不会挂起。

所以看来我的一些工作线程被提前杀死了,剩下的就卡在了 Barrier 上。但是,线程实际上除了读写内存(并调用Barrier())之外什么都不做,而且我非常肯定不会发生段错误。我也有可能得出错误的结论,因为(如上面链接的问题中所述)我是 Win32 线程的新手。

这里可能发生了什么,我如何使用 VS 调试这种奇怪的行为?

【问题讨论】:

  • 如果一个线程正在死亡,您至少应该看到附带的调试器出现第一次机会异常的证据。另外,当您在挂起期间进行调试中断时,其他两个工作线程在做什么?
  • 我刚去Debug -> Exceptions...,检查了每一个异常类型,但是死锁没有中断就发生了。这是否意味着我的线程“正常”停止并且我的设计有缺陷?此外,当我在挂起期间中断时,其他两个工作线程根本不会显示在“线程”窗口中:只有主线程和一个工作线程。
  • 好的,听起来你已经涵盖了你的异常,用于调试。其他后台线程是否有可能只是正常终止?输出窗口将显示类似“线程## 已退出,代码为 0”。
  • 你说的很对,输出窗口(我一直忘记它存在......)说两个线程以代码0退出。我想我应该重新评估Barrier()的正确性?
  • 停止编写自己的同步原语,开始使用系统提供的优秀同步原语。

标签: c++ multithreading visual-c++


【解决方案1】:

如何调试奇怪的线程行为?

与您所说的不太一样,但答案几乎总是:非常了解代码,了解所有可能的结果并找出正在发生的结果。调试器在这里变得不那么有用了,因为您可以跟随一个线程并错过导致其他线程失败的原因,或者跟随父线程,在这种情况下执行不再是顺序的,您最终会到处乱跑。

现在,解决问题。

pSemaphore = CreateSemaphore(NULL, THREAD_COUNT, THREAD_COUNT, NULL);

来自MSDN documentation

lInitialCount [in]:信号量对象的初始计数。此值必须大于或等于零且小于或等于 lMaximumCount。信号量的状态在其计数大于零时发出信号,在它为零时发出无信号。每当等待函数释放正在等待信号量的线程时,计数就会减一。通过调用 ReleaseSemaphore 函数将计数增加指定的数量。

还有here

在线程尝试执行任务之前,它使用 WaitForSingleObject 函数来确定信号量的当前计数是否允许它这样做。等待函数的超时参数设置为零,因此如果信号量处于非信号状态,函数会立即返回。 WaitForSingleObject 将信号量的计数减一。

所以我们在这里要说的是,信号量的计数参数告诉您一次允许多少线程执行给定任务。当您最初将计数设置为 THREAD_COUNT 时,您将允许所有线程访问“资源”,在这种情况下将继续向前。

您链接的答案使用此信号量创建方法:

CreateSemaphore(0, 0, 1024, 0)

这基本上是说不允许任何线程使用该资源。在您的实现中,信号量已发出信号(> 0),因此一切都会愉快地进行,直到其中一个线程设法将计数减少到零,此时其他一些线程等待信号量再次发出信号,这可能不是t 与您的计数器同步发生。请记住,当WaitForSingleObject 返回时,它会减少信号量上的计数器。

在您发布的示例中,设置:

::ReleaseSemaphore(sync.Semaphore, sync.ThreadsCount - 1, 0);

之所以有效,是因为每个 WaitForSingleObject 调用都会将信号量的值减 1,并且有 threadcount - 1 要做,这发生在 threadcount - 1 WaitForSingleObjects 全部返回时,因此信号量返回 0,因此再次未发出信号,因此在下一次通过时,每个人都在等待,因为不允许任何人同时访问资源。

简而言之,将初始值设置为零,看看是否可以解决问题。


编辑 一点解释:换个角度看,信号量就像一个 n 原子门。你做的通常是这样的:

// Set the number of tickets:
HANDLE Semaphore = CreateSemaphore(0, 20, 200, 0);

// Later on in a thread somewhere...
// Get a ticket in the queue
WaitForSingleObject(Semaphore, INFINITE); 

// Only 20 threads can access this area 
// at once. When one thread has entered 
// this area the available tickets decrease 
// by one. When there are 20 threads here
// all other threads must wait.

// do stuff

ReleaseSemaphore(Semaphore, 1, 0);
// gives back one ticket.

因此,我们在这里放置信号量的用途并不完全是它们的设计用途。

【讨论】:

  • 将初始值设置为 0 似乎可以解决问题,但我必须再阅读您的答案几次才能完全理解原因。 :)
  • 如果信号量的计数为零,则任何Wait函数都必须等待,否则可以继续。当您将信号量初始计数设置为 > 0 时,等待函数不必等待,因此线程不会在障碍处停止。我花了几次阅读您的代码、链接代码和 msdn 来确定...
  • 哦,当您释放时,您将信号量 once 增加 threads-1,此时这些线程可以从 WaitFromSingleObject 返回,此时它们每个都将信号量计数器减 1。有线程 1 个这样的线程(一个不必等待),所以信号量计数器最终归零,所以下次......
【解决方案2】:

很难准确猜测您可能会遇到什么。并行编程是(IMO)遵循“保持简单,显然是正确的”理念的地方之一,不幸的是,我不能说您的Barrier 代码似乎符合条件。就个人而言,我想我会有这样的事情:

// define and initialize the array of events use for the barrier:
HANDLE barrier_[thread_count];

for (int i=0; i<thread_count; i++)
    barrier_[i] = CreateEvent(NULL, true, false, NULL);

// ...

Barrier(size_t thread_num) { 
    // Signal that this thread has reached the barrier:
    SetEvent(barrier_[thread_num]); 

    // Then wait for all the threads to reach the barrier:
    WaitForMultipleObjects(thread_count, barrier_, true, INFINITE);
}

编辑:

好的,既然意图已经明确(需要处理多次迭代),我会修改答案,但只是稍微修改一下。不是一个事件数组,而是两个:一个用于奇数迭代,一个用于偶数迭代:

// define and initialize the array of events use for the barrier:
HANDLE barrier_[2][thread_count];

for (int i=0; i<thread_count; i++) {
    barrier_[0][i] = CreateEvent(NULL, true, false, NULL);
    barrier_[1][i] = CreateEvent(NULL, true, false, NULL);
}

// ...

Barrier(size_t thread_num, int iteration) { 
    // Signal that this thread has reached the barrier:
    SetEvent(barrier_[iteration & 1][thread_num]); 

    // Then wait for all the threads to reach the barrier:
    WaitForMultipleObjects(thread_count, &barrier[iteration & 1], true, INFINITE);
    ResetEvent(barrier_[iteration & 1][thread_num]);
}

【讨论】:

  • 这是我最初尝试的,如我链接的另一个问题中所述。但是,这不起作用,因为一旦Wait... 在一个线程上返回,它就会重置所有事件并阻止其他线程从它们的Wait... 返回(如this answer 的cmets 中所述)。跨度>
  • 不要使用自动重置事件。它们根本无法正常工作。 Raymond Chen 对此做了一系列深入的文章。我经常使用这种方法加入一系列线程。
  • @suszterpatt:糟糕——非常正确。您需要使它们手动重置事件(将第二个参数的true 传递给CreateEvent)。
  • @Jerry: 那时,一个线程可以从Wait返回,执行到下一次调用Barrier并立即从它返回,因为其他线程没有有机会重置他们的事件(如对另一个问题的编辑中所述......对不起)。
  • 确定要先重置所有事件?
【解决方案3】:

在你的障碍中,是什么阻止了这条线:

*计数器 = 0;

在另一个线程执行另一个线程时执行?

LONG wait_count = 互锁增量(计数器);

【讨论】:

  • if() 条件。如果一个线程使用*counter = 0 进入分支,那么只有在所有其他线程在它之前调用InterlockedIncrement() 时它才能这样做。这就是计数器到达THREAD_COUNT的方式。
猜你喜欢
  • 1970-01-01
  • 2023-03-03
  • 2017-01-24
  • 2016-08-22
  • 2018-02-22
  • 2014-12-22
  • 1970-01-01
  • 1970-01-01
  • 2010-09-23
相关资源
最近更新 更多