在C中挂起线程池的线程

Suspending threads of thread pool in C

我正在从事一个基于并行编程的项目,我需要尽可能高效地执行给定任务(在时间和能源消耗方面)。 为此,我需要根据某些条件暂停工作池中的一些线程。这些线程是使用 pthread_create().

创建的

我有两种工作池,一种存储活动线程,另一种存储挂起线程。确定要挂起的线程后,我将其 threadID 推入我的挂起线程池,然后使用 pthread_kill.

挂起线程
push_task_suspended(threadID);
int status = pthread_kill(threadID,SIGSTOP);

但是,我在使用它时遇到了分段错误。我在这段代码上有 运行 gdb,它显示由于 pthread_kill.

而导致的分段错误

你能告诉我为什么会收到这个吗?

我不知道为什么 pthread_kill(threadID,SIGSTOP) 崩溃了——我猜 threadID 不是线程的 pthread_t? -- 但这绝对不是解决问题的好方法!

条件变量有点棘手,但值得理解。我在这里有点兴奋......但我希望它有用。


使用您自己的 'task_suspended' 队列 -- 使用 sema_t

让我们假设您有一个关于挂起任务出队和空闲工作人员入队的互斥体。那么空闲的工人必须:

  loop:
    lock(mutex)
      .... look for task, but if none pending ....
      enqueue(self)  -- on task_suspended queue
    unlock(mutex)    -- (a)
    suspend(self)    -- (b)
    goto loop

而在添加任务时,逻辑是:

    lock(mutex)
      enqueue(task)  -- on task pending queue
      if (worker-idle-queue-not-empty)
        dequeue(worker)
        desuspend(worker)
    unlock(mutex)

事实上,desuspend()不需要在mutex里面,但这是小事。

重要的是 desuspend() 必须有效,即使它发生在 (a) 的 unlock() 和 (b) 的 suspend() 之间。您可以为每个线程提供自己的 sem_t 信号量——那么 suspend() 就是 sem_wait()desuspend() 就是 sem_post()。 [但是,不,你不能为此使用互斥!!]


使用 'Condition Variable'

使用您自己的 'task_suspended' 队列,您正在重新发明轮子。

正如上面评论中提到的,为这项工作提供的工具是(所谓的)'condition variable' -- pthread_cond_t.

使用 'condition variables' 的关键是了解它们 绝对不是 变量——它们 not 有一个值,他们确实 not 在任何意义上计算 pthread_cond_wait() and/or pthread_cond_signal() 的数量......它们不是信号量的一种形式。尽管名称如此,但最好将 pthread_cond_t 视为线程的 队列 。然后:

    pthread_cond_wait(cond, mutex)  is, effectively:  enqueue(self)   -- on 'cond'
                                                      unlock(mutex)
                                                      suspend(self)
                                                      ....wait for signal...
                                                      lock(mutex)

神奇的是 enqueue()+unlock()+suspend() 是单个操作(就所有线程而言),然后:

    pthread_cond_signal(cond)  is, effectively:      if ('cond' queue-not-empty)
                                                       dequeue(thread)  -- from 'cond'
                                                       desuspend(thread)

另外,通过某种魔法,所有操作都是单一操作。 [注意:pthread_cond_signal() 允许出列和取消挂起多个线程,见下文。]

所以现在,对于工作线程,我们有:

    lock(mutex)
  loop:
      .... look for task, but if none pending ....
      pthread_cond_wait(cond, mutex)     
      goto loop
    ... if have task, pick it up ...
    unlock(mutex)

以及任务创建:

    lock(mutex)
      enqueue(task)    
      pthread_cond_signal(cond)
    unlock(mutex)

其中 cond 取代了待处理线程的显式队列。

现在,pthread_cond_signal(cond) 可以在 mutex 的内部或外部。如果在内部,那么从概念上讲,一旦线程从 cond 队列中出队,它就会 运行 并立即阻塞在互斥锁上——这似乎是一种浪费。但是实现可以做一些聪明的事情,只需将重新启动的线程从一个队列转移到另一个队列。

注意,任务创建者不知道有多少个挂起的线程,也不关心。 POSIX 表示 pthread_cond_signal() 函数应:

  • ...unblock at least one of the threads that are blocked on the specified condition variable cond (if any threads are blocked on cond).

  • ...have no effect if there are no threads currently blocked on cond.

特别注意“解锁至少一个线程”。同样,将 cond 视为变量是错误的。将 cond 视为(比方说)“任务就绪”标志、计数或您可能认为是变量的任何其他内容是错误的。事实并非如此。当一个线程在 pthread_cond_wait() 之后重新启动时,它所等待的可能已经发生,也可能没有发生,如果已经发生,另一个线程可能已经先到达那里。这就是为什么你读到的所有内容(所谓的)'condition variables' 都会谈论在循环中使用它们,并且 return 进入循环的顶部(就在 lock(mutex) 之后)return 来自 pthread_cond_wait().

注意:当一个线程在 pthread_cond_wait() 之后重新启动时,它可能是由单个 pthread_cond_signal() 重新启动的多个线程之一,是的,POSIX 允许这样做似乎很奇怪——大概要么适合一些历史实现,要么允许一些更简单的实现(可能与线程优先级有关)。但是,即使 pthread_cond_wait() 确实保证只重新启动一个线程,重新启动的线程也可以在 一些其他工作线程之后重新获得互斥量 ,因此:

     Worker 1               |  Worker 2               |  Task Queue
       busy                 |    busy                 |    empty
       lock(mutex)          |    .                    |    .
       + task queue empty   |    .                    |    lock(mutex)
       unlock(mutex) +      |    .                    |    -
                 wait(cond) |    .                    |    -
       ~                    |    lock(mutex)          |    + enqueue task
       ~                    |    -                    |    + signal(cond)
       re-lock(mutex)       |    -                    |    unlock(mutex)                      
       -                    |    + dequeue task       |    .
       -                    |    unlock(mutex)        |    empty
       + task queue empty ! |    busy                 |    .     

其中 + 是线程拥有互斥量的地方,- 是等待互斥量的地方,~ 是等待 'cond' 发出信号。

您可能担心每次将新任务加入队列时都执行 pthread_cond_signal(cond)...所以您只能在任务队列为空时执行此操作。您应该能够说服自己这样做是可行的——尤其是在互斥体内部完成的情况下。


使用 sem_tsem_t 与计数器

或者,您可以使用 sem_t 来计算 'tasks - waiters' 的数量。每次将新任务添加到队列时,信号量都会递增 (sem_post)。每次工作人员完成一项任务时,它都会保留下一个任务或等待(sem_wait)。您仍然需要一种安全的方法来使任务入队和出队——例如:lock(mutex)、enqueue(task)、unlock(mutex)、post(sem);和:等待(sem),锁定(互斥),出列(任务),解锁(互斥)。

这里唯一的困难是信号量的最大值可以小到 32767 -- 参见 sysconf(_SC_SEM_VALUE_MAX)

或者您可以使用一个 sem_t 和服务员人数。因此,对于工作线程,我们有:

  loop:
    lock(mutex)
      .... look for task, but if none pending ....
      increment waiter-count
    unlock(mutex)
    sem_wait(sem)
    goto loop

以及任务创建:

    lock(mutex)
      enqueue(task)    
      kick = (waiter-count != 0)
      if (kick)
        decrement(waiter-count)
    unlock(mutex)
    if (kick)
      sem_post(sem)

sem_post() 可以放在互斥体内部 -- 但放在外面更好。

除非你有超过 32767 个工作线程 (!)。

但是,当您取消选择它时,您会发现这是(在很大程度上)重新发明 pthread_cond_wait/_signal(),并且不太可能更有效率。

我不同意以前的发帖人所说的信号通常不能与线程一起使用。你只需要谨慎行事。

使用条件变量、互斥量或信号量的解决方案的缺点是线程不会立即暂停,并且需要主动检查要停止的线程部分。您可以使用信号实现立即停止。你不能使用 SIGSTOP.

pthread_kill(threaID, SIGSTOP) 会停止整个过程,因此 caller 也会停止,这可能不是故意的,您对此无能为力。 Linux pthread_kill(3) 手册页说“如果安装了信号处理程序,将在线程中调用处理程序 thread,但如果信号的处理是“停止”、“继续”或“终止”,则此操作将影响整个过程”,而信号(7) 手册页指定 SIGSTOP 的配置无法更改。

但是您可以改用不同的信号,例如 SIGUSR1 表示“停止线程”,SIGUSR2 表示“继续线程”。您必须捕获 这些信号(以避免使用默认配置),并确保要停止的线程不会阻塞它们。当然,信号必须用pthread_kill().

发送

“停止线程”信号的信号处理程序调用 pause() 或其他一些无限期等待的函数。稍后的“继续线程”信号(实际上,任何未被阻塞的捕获信号)将中断 pause() 调用,“停止线程”信号处理程序将 return,线程将恢复执行.

使用线程和信号的一般规则是“阻止一切”。线程应该只解除阻塞它们严格需要做出反应的信号。标准信号,如 SIGTERMSIGINTSIGCHLD,可能从外部发送到应用程序,应该在除一个专用线程之外的所有线程中被阻塞。当收到终止信号时,该线程应负责终止其他线程,使用 pthread_cancel() 或发送应用程序定义的信号。