优雅地退出无限循环线程

时间:2013-10-10 00:55:34

标签: c++ multithreading

我一直遇到尝试运行具有以下属性的线程的问题:

  1. 以无限循环运行,检查一些外部资源,例如:来自网络或设备的数据,
  2. 及时从其资源获取更新,
  3. 在被要求时立即退出,
  4. 有效地使用CPU。
  5. 第一种方法

    我见过的一个解决方案如下:

    void class::run()
    {
        while(!exit_flag)
        {
            if (resource_ready)
                use_resource();
        }
    }
    

    这满足第1,2和3点,但是忙碌的等待循环使用100%CPU。

    第二种方法

    可能的解决办法是将睡眠声明放在:

    void class::run()
    {
        while(!exit_flag)
        {
            if (resource_ready)
                use_resource();
            else
                sleep(a_short_while);
        }
    }
    

    我们现在没有锤击CPU,所以我们解决了1和4,但是当资源准备好或者我们被要求退出时,我们可以不必要地等待a_short_while

    第三种方法

    第三种选择是对资源进行阻塞读取:

    void class::run()
    {
        while(!exit_flag)
        {
            obtain_resource();
            use_resource();
        }
    }
    

    这将优雅地满足1,2和4,但现在如果资源不可用,我们不能要求线程退出。

    问题

    最好的方法似乎是第二个,睡眠时间短,只要能够实现CPU使用率和响应能力之间的权衡。 然而,这似乎仍然不是最理想的,对我来说也不优雅。这似乎是一个常见的问题需要解决。有更优雅的方式来解决它吗?有没有办法解决所有这四个要求?

7 个答案:

答案 0 :(得分:8)

这取决于线程正在访问的资源的具体情况,但基本上是以最小的延迟有效地执行它,资源需要提供一个API来执行可中断的阻塞等待。

在POSIX系统上,如果您使用的资源是文件或文件描述符(包括套接字),则可以使用select(2)poll(2)系统调用来执行此操作。为了允许等待被抢占,您还可以创建一个可以写入的虚拟管道。

例如,以下是等待文件描述符或套接字准备就绪或代码被中断的方法:

// Dummy pipe used for sending interrupt message
int interrupt_pipe[2];
int should_exit = 0;

void class::run()
{
    // Set up the interrupt pipe
    if (pipe(interrupt_pipe) != 0)
        ;  // Handle error

    int fd = ...;  // File descriptor or socket etc.
    while (!should_exit)
    {
        // Set up a file descriptor set with fd and the read end of the dummy
        // pipe in it
        fd_set fds;
        FD_CLR(&fds);
        FD_SET(fd, &fds);
        FD_SET(interrupt_pipe[1], &fds);
        int maxfd = max(fd, interrupt_pipe[1]);

        // Wait until one of the file descriptors is ready to be read
        int num_ready = select(maxfd + 1, &fds, NULL, NULL, NULL);
        if (num_ready == -1)
            ; // Handle error

        if (FD_ISSET(fd, &fds))
        {
            // fd can now be read/recv'ed from without blocking
            read(fd, ...);
        }
    }
}

void class::interrupt()
{
    should_exit = 1;

    // Send a dummy message to the pipe to wake up the select() call
    char msg = 0;
    write(interrupt_pipe[0], &msg, 1);
}

class::~class()
{
    // Clean up pipe etc.
    close(interrupt_pipe[0]);
    close(interrupt_pipe[1]);
}

如果你在Windows上,select()函数仍适用于套接字,但仅适用于套接字,因此你应该安装use WaitForMultipleObjects来等待资源句柄和一个事件句柄。例如:

// Event used for sending interrupt message
HANDLE interrupt_event;
int should_exit = 0;

void class::run()
{
    // Set up the interrupt event as an auto-reset event
    interrupt_event = CreateEvent(NULL, FALSE, FALSE, NULL);
    if (interrupt_event == NULL)
        ;  // Handle error

    HANDLE resource = ...;  // File or resource handle etc.
    while (!should_exit)
    {
        // Wait until one of the handles becomes signaled
        HANDLE handles[2] = {resource, interrupt_event};
        int which_ready = WaitForMultipleObjects(2, handles, FALSE, INFINITE);    
        if (which_ready == WAIT_FAILED)
            ; // Handle error
        else if (which_ready == WAIT_OBJECT_0))
        {
            // resource can now be read from without blocking
            ReadFile(resource, ...);
        }
    }
}

void class::interrupt()
{
    // Signal the event to wake up the waiting thread
    should_exit = 1;
    SetEvent(interrupt_event);
}

class::~class()
{
    // Clean up event etc.
    CloseHandle(interrupt_event);
}

答案 1 :(得分:3)

如果obtain_ressource()函数支持超时值,您将获得有效的解决方案:

while(!exit_flag)
{
    obtain_resource_with_timeout(a_short_while);
    if (resource_ready)
        use_resource();
}

这有效地将sleep()obtain_ressurce()电话相结合。

答案 2 :(得分:2)

查看manpage for nanosleep

  

如果nanosleep()函数因信号被中断而返回,则该函数返回值-1并设置errno以指示中断。

换句话说,你可以通过发送信号来中断睡眠线程(sleep manpage说类似的东西)。这意味着您可以使用第二种方法,并使用中断在线程休眠时立即唤醒线程。

答案 3 :(得分:1)

使用Gang of Four Observer Pattern:

http://home.comcast.net/~codewrangler/tech_info/patterns_code.html#Observer

回调,不要阻止。

答案 4 :(得分:0)

这里可以使用Self-Pipe技巧。 http://cr.yp.to/docs/selfpipe.html 假设您正在从文件描述符中读取数据。

创建管道并选择()以获取管道输入以及您感兴趣的资源的可读性。 然后,当数据进入资源时,线程会唤醒并执行处理。否则就会睡觉。 要终止线程,发送一个信号并在信号处理程序中,在管道上写一些东西(我会说一些永远不会来自您感兴趣的资源的东西,类似于NULL来说明这一点)。 select调用返回并且读取输入的线程知道它有毒丸,是时候退出并调用pthread_exit()。

编辑:更好的方法是看到数据来自管道,因此只是退出而不是检查管道上的值。

答案 5 :(得分:0)

Win32 API使用或多或少的方法:

someThreadLoop( ... )
{
  MSG msg;
  int retVal;

  while( (retVal = ::GetMessage( &msg, TaskContext::winHandle_, 0, 0 )) > 0 )
  {
    ::TranslateMessage( &msg );
    ::DispatchMessage( &msg );
  }
}

GetMessage本身会阻塞,直到收到任何类型的消息,因此不使用任何处理(refer)。如果收到WM_QUIT,则返回false,正常退出线程函数。这是其他地方提到的生产者/消费者的变种。

您可以使用生产者/消费者的任何变体,并且模式通常类似。有人可能会争辩说,人们会想要分担关于退出和获取资源的责任,但是OTOH退出也可能取决于获得资源(或者可以被视为资源之一 - 但是特殊资源)。我至少会抽象出生产者消费者模式,并有各种各样的实现。

因此:

AbstractConsumer:

void AbstractConsumer::threadHandler()
{
  do
  {
    try
    {        
      process( dequeNextCommand() ); 
    }
    catch( const base_except& ex )
    {
      log( ex );
      if( ex.isCritical() ){ throw; }
      //else we don't want loop to exit...
    }
    catch( const std::exception& ex )
    {
      log( ex );
      throw; 
    }
  }
  while( !terminated() );
}

virtual void /*AbstractConsumer::*/process( std::unique_ptr<Command>&& command ) = 0;
//Note: 
// Either may or may not block until resource arrives, but typically blocks on
// a queue that is signalled as soon as a resource is available.
virtual std::unique_ptr<Command> /*AbstractConsumer::*/dequeNextCommand() = 0;
virtual bool /*AbstractConsumer::*/terminated() const = 0;

我通常封装命令以在消费者的上下文中执行函数,但消费者中的模式始终是相同的。

答案 6 :(得分:0)

上面提到的任何(最好的,最好的)方法将执行以下操作:创建线程,然后阻止wwiting for resource,然后将其删除。

如果您担心效率,那么在等待IO时这不是最好的方法。至少在Windows上,你将在用户模式下分配大约1mb的内存,在内核中只分配一个额外的线程。如果你有很多这样的资源怎么办?拥有许多等待线程也会增加上下文切换并降低程序速度。如果资源需要更长时间并且提出了许多请求,该怎么办?你最终可能会有大量的等待线程。

现在,它的解决方案(再次,在Windows上,但我确信在其他操作系统上应该有类似的东西)是使用线程池(Windows提供的)。在Windows上,这不仅会创建有限数量的线程,它还能够检测线程何时等待IO,并将从那里查找线程并在等待时将其重新用于其他操作。

请参阅http://msdn.microsoft.com/en-us/library/windows/desktop/ms686766(v=vs.85).aspx

此外,对于更细粒度的控制位仍然具有在等待IO时放弃线程的能力,请参阅IO完成端口(我认为它们无论如何都会在内部使用线程池):http://msdn.microsoft.com/en-us/library/windows/desktop/aa365198(v=vs.85).aspx