7

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

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

第一种方法

我为此看到的一种解决方案如下:

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

这满足第 1、2 和 3 点,但作为一个繁忙的等待循环,使用 100% CPU。

第二种方法

一个可能的解决方法是在以下位置添加一个 sleep 语句:

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 使用率和响应能力之间取得平衡,就可以进行短暂的睡眠。然而,这似乎仍然不是最理想的,对我来说也不优雅。这似乎是一个需要解决的常见问题。有没有更优雅的方法来解决它?有没有一种方法可以满足所有这四个要求?

4

7 回答 7

8

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

在 POSIX 系统上,如果您使用的资源是文件或文件描述符(包括套接字) ,您可以使用select(2)or系统调用来执行此操作。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()功能仍然适用于套接字,但适用于套接字,因此您应该安装 useWaitForMultipleObjects以等待资源句柄和事件句柄。例如:

// 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);
}
于 2013-10-10T01:02:28.570 回答
3

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

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

这有效地结合了sleep()调用obtain_ressurce()

于 2013-10-10T01:01:39.767 回答
2

查看手册页nanosleep

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

换句话说,您可以通过发送信号来中断睡眠线程(sleep联机帮助页中有类似的内容)。这意味着您可以使用第二种方法,并在线程处于睡眠状态时使用中断立即唤醒线程。

于 2013-10-10T01:13:06.523 回答
1

使用四人组观察者模式:

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

回调,不要阻塞。

于 2013-10-10T01:00:33.513 回答
0

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

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

编辑:更好的方法是查看数据是否出现在管道上,因此只是退出而不是检查该管道上的值。

于 2013-10-10T03:58:37.787 回答
0

Win32 API 或多或少地使用了这种方法:

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

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

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

您可以使用生产者/消费者的任何变体,并且模式通常是相似的。有人可能会争辩说,人们希望将退出和获取资源的责任分开,但是 OTOH 退出也可能取决于获取资源(或者可以被视为一种资源 - 但一种特殊的资源)。我至少会抽象生产者消费者模式并对其进行各种实现。

所以:

摘要消费者:

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;

我通常封装命令在消费者的上下文中执行一个功能,但消费者中的模式总是一样的。

于 2013-10-10T05:07:14.080 回答
0

上面提到的任何(至少是大多数)方法都将执行以下操作:创建线程,然后阻止它获取资源,然后将其删除。

如果您担心效率,这不是等待 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/库/windows/desktop/aa365198(v=vs.85).aspx

于 2013-10-10T06:50:38.387 回答