我一直遇到尝试运行具有以下属性的线程的问题:
我见过的一个解决方案如下:
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使用率和响应能力之间的权衡。 然而,这似乎仍然不是最理想的,对我来说也不优雅。这似乎是一个常见的问题需要解决。有更优雅的方式来解决它吗?有没有办法解决所有这四个要求?
答案 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)
如果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