Idle-time processing in a runloop
Idle-time processing in a runloop
- Subject: Idle-time processing in a runloop
- From: Jakob Olesen <email@hidden>
- Date: Wed, 28 Jun 2006 19:55:11 +0200
I am writing a Cocoa app that needs to do some calculations in the
background. I understand from previous threads that something like
that should be done on a separate thread to keep the GUI responsive.
No problem.
Some of my background tasks do network communications, so I would
like to use a runloop. Then I can also communicate with the
background thread using distributed objects or a mach port. Very
cool. I would like the background thread to respond to requests from
the main thread as quickly as possible to avoid stalling the GUI.
In other words, I would like my runloop to process any pending
sources, and only when there are no sources should one of my
background tasks be run. One way of doing it is by polling the runloop:
Assume two functions task_queue_empty() and run_single_task() doing
the obvious thing.
void
threadMain()
{
for(;;) {
SInt32 rlExit = kCFRunLoopRunHandledSource;
// wait for a task to appear, handle all sources first
while(task_queue_empty() ||
rlExit==kCFRunLoopRunHandledSource) {
rlExit = CFRunLoopRunInMode(kCFRunLoopDefaultMode,
task_queue_empty() ? 1e10 : 0, YES);
}
run_single_task();
}
}
This works very nicely: I run a single task only when all sources are
exhausted, so they get highest priority. There is no waiting for idle
timers to fire.
Two problems:
1. I am "hijacking" the runloop, so this method cannot be used in the
main thread where NSApplicationMain() likes to call [NSRunLoop run].
Not a big problem, but it would be useful to run in the main thread
during development.
2. Recursive runloop invocations stop the background tasks. It would
be nice to be able to sneak in a task or two during a DO callout for
instance.
Another approach is to use a runloop observer:
void threadMain()
{
CFRunLoopObserverRef obs = CFRunLoopObserverCreate(...,
kCFRunLoopBeforeWaiting, ..., beforeWaitingObserver, ...);
CFRunLoopAddObserver(CFRunLoopGetCurrent(), obs,
kCFRunLoopCommonModes);
CFRunLoopRun();
}
void
beforeWaitingObserver(CFRunLoopObserverRef observer,
CFRunLoopActivity activity, void *info)
{
run_single_task();
// Make sure run loop wakes up immediately for the next task
if (!task_queue_empty)
CFRunLoopWakeUp(CFRunLoopGetCurrent());
}
This would work in the main thread as well, and I get to decide in
which runloop modes my tasks are running.
Problem: CFRunLoop calls the beforeWaitingObserver before EVERY
(version 1) source, not just before going to sleep. In other words,
my background tasks get the same priority as the runloop sources. Not
good.
Third approach: Prime a timer from the beforeWaitingObserver:
CFRunLoopTimerRef timer;
void threadMain()
{
timer = CFRunLoopTimerCreate(..., CFAbsoluteTimeGetCurrent()
+1e10, 1e10, 0, 0, timerCallBack, ...);
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer,
kCFRunLoopCommonModes);
CFRunLoopObserverRef obs = CFRunLoopObserverCreate(...,
kCFRunLoopBeforeWaiting, ..., beforeWaitingObserver, ...);
CFRunLoopAddObserver(CFRunLoopGetCurrent(), obs,
kCFRunLoopCommonModes);
CFRunLoopRun();
}
void
beforeWaitingObserver(CFRunLoopObserverRef observer,
CFRunLoopActivity activity, void *info)
{
if (!task_queue_empty)
CFRunLoopTimerSetNextFireDate(timer, CFAbsoluteTimeGetCurrent
()+0.010);
}
void
timerCallBack(CFRunLoopTimerRef timer, void *info)
{
run_single_task();
}
This gets the priorities straight: As long as each source can do its
business in 10ms, all sources will be processed before any background
tasks are run.
Problem: To keep the background thread responsive, each task is very
short, say 10ms. The timer causes a 10ms delay between tasks, so they
run at half speed for no reason.
To fix this, we can poll the runloop from the timer callback:
void
timerCallBack(CFRunLoopTimerRef timer, void *info)
{
CFStringRef mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
while (!task_queue_empty && CFRunLoopRunInMode(mode,0,YES)!
=kCFRunLoopRunHandledSource) {
run_single_task();
}
CFRelease(mode);
}
This actually seems to do the trick. The timer still causes a 10ms
delay every time a source has been handled, but that might actually
be a good thing. The main thread is likely to send multiple DO
requests at a time, and we will be able to handle them quickly this way.
There should be some code to prevent the timer callback from being
called recursively - I have omitted that for clarity.
It is not a very simple solution, though. There are also potential
problems with CFRunLoopStop() and recursive runloop invocations.
I guess it would be possible to use a NSNotificationQueue with
NSPostWhenIdle, but I suspect that causes delays too. I haven't tried
it.
What is the usual approach to idle-time processing in a Cocoa app?
Use a timer and live with the delays?
From http://developer.apple.com/documentation/Cocoa/Conceptual/
CocoaFundamentals/WhatIsCocoa/chapter_2_section_3.html
"Performance—To enhance the performance of your application, Cocoa
provides programmatic support for multithreading, idle-time
processing, lazy loading of resources, memory management, and run-
loop manipulation."
I can't seem to find that support...
NSLayoutManager claims to do layout "when the runloop is idle". How
is that done?
_______________________________________________
Do not post admin requests to the list. They will be ignored.
Cocoa-dev mailing list (email@hidden)
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden