diff --git a/ChangeLog b/ChangeLog index 798519fd2..0ab8f617c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,11 @@ +2026-03-19 Frederik Seiffert + + * Source/NSOperation.m: + Ensure NSOperationQueue mainQueue executes queued operations on the + main thread, using libdispatch main-queue fast path when available. + * Tests/base/NSOperation/threads.m: + Add test for enqueuing on mainQueue from a background thread. + 2026-03-12 Richard Frith-Macdonald * Source/GSHTTPURLHandle.m: diff --git a/Source/NSOperation.m b/Source/NSOperation.m index ff03189b7..26ff80000 100644 --- a/Source/NSOperation.m +++ b/Source/NSOperation.m @@ -66,6 +66,7 @@ #import "Foundation/NSValue.h" #import "GNUstepBase/NSArray+GNUstepBase.h" #import "GSPrivate.h" +#import "GSDispatch.h" #define GSInternal NSOperationInternal #include "GSInternal.h" @@ -609,6 +610,7 @@ - (void) main @interface NSOperationQueue (Private) - (void) _execute; +- (void) _main: (NSOperation *)op; - (void) _thread: (NSNumber *) threadNumber; - (void) observeValueForKeyPath: (NSString *)keyPath ofObject: (id)object @@ -632,6 +634,17 @@ - (void) observeValueForKeyPath: (NSString *)keyPath static NSString *threadKey = @"NSOperationQueue"; static NSOperationQueue *mainQueue = nil; +#if GS_USE_LIBDISPATCH == 1 +static void +mainQueueExecuteOperation(void *context) +{ + NSOperation *op = (NSOperation *)context; + + [mainQueue _main: op]; + RELEASE(op); +} +#endif + @implementation NSOperationQueue + (id) currentQueue @@ -648,6 +661,7 @@ + (void) initialize if (nil == mainQueue) { mainQueue = [self new]; + [mainQueue setMaxConcurrentOperationCount: 1]; } } @@ -886,6 +900,10 @@ - (NSArray *) operations - (void) setMaxConcurrentOperationCount: (NSInteger)cnt { + if (self == mainQueue) + { + cnt = 1; + } if (cnt < 0 && cnt != NSOperationQueueDefaultMaxConcurrentOperationCount) { @@ -1102,11 +1120,35 @@ - (void) _thread: (NSNumber *) threadNumber [NSThread exit]; } +- (void) _main: (NSOperation *)op +{ + BOOL concurrent; + + concurrent = [op isConcurrent]; + NS_DURING + { + ENTER_POOL + [op start]; + LEAVE_POOL + } + NS_HANDLER + { + NSLog(@"Problem running operation %@ ... %@", + op, localException); + } + NS_ENDHANDLER + if (NO == concurrent) + { + [op _finish]; + } +} + /* Check for operations which can be executed and start them. */ - (void) _execute { NSInteger max; + NSMutableArray *mainQueueOperations = nil; [internal->lock lock]; @@ -1136,9 +1178,17 @@ - (void) _execute options: NSKeyValueObservingOptionNew context: isFinishedCtxt]; internal->executing++; - if (YES == [op isConcurrent]) + if (self == mainQueue) + { + if (nil == mainQueueOperations) + { + mainQueueOperations = [NSMutableArray new]; + } + [mainQueueOperations addObject: op]; + } + else if (YES == [op isConcurrent]) { - [op start]; + [op start]; } else { @@ -1178,6 +1228,23 @@ - (void) _execute } NS_ENDHANDLER [internal->lock unlock]; + + if (nil != mainQueueOperations) + { + GS_FOR_IN(NSOperation *, op, mainQueueOperations) + { +#if GS_USE_LIBDISPATCH == 1 + dispatch_async_f(dispatch_get_main_queue(), RETAIN(op), + mainQueueExecuteOperation); +#else + [self performSelectorOnMainThread: @selector(_main:) + withObject: op + waitUntilDone: NO]; +#endif + } + GS_END_FOR(mainQueueOperations) + RELEASE(mainQueueOperations); + } } @end diff --git a/Tests/base/NSOperation/threads.m b/Tests/base/NSOperation/threads.m index c5359327f..9b6c76964 100644 --- a/Tests/base/NSOperation/threads.m +++ b/Tests/base/NSOperation/threads.m @@ -4,6 +4,8 @@ #import #import #import +#import +#import #import "ObjectTesting.h" @@ -135,6 +137,28 @@ - (void) main } @end +static BOOL +waitForOperation(NSOperation *op, NSTimeInterval timeout) +{ + NSDate *end; + + end = [NSDate dateWithTimeIntervalSinceNow: timeout]; + while (NO == [op isFinished] && [end timeIntervalSinceNow] > 0.0) + { + NSDate *next; + BOOL handled; + + next = [NSDate dateWithTimeIntervalSinceNow: 0.01]; + handled = [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode + beforeDate: next]; + if (NO == handled) + { + [NSThread sleepForTimeInterval: 0.01]; + } + } + return [op isFinished]; +} + int main() { ThreadCounter *cnt; @@ -225,6 +249,19 @@ int main() PASS(([NSOperationQueue currentQueue] == [NSOperationQueue mainQueue]), "current queue outside -main is main queue"); PASS(([NSOperationQueue mainQueue] != nil), "main queue is not nil"); + obj = [OpFlag new]; + [NSThread detachNewThreadSelector: @selector(addOperation:) + toTarget: [NSOperationQueue mainQueue] + withObject: obj]; + PASS((YES == waitForOperation(obj, 2.0)), + "main queue runs enqueued operations"); + PASS(([obj ran] == YES), "main queue operation ran"); + PASS(([obj thread] == [NSThread mainThread]), + "main queue operation ran in main thread"); + PASS(([obj queue] == [NSOperationQueue mainQueue]), + "main queue operation reported main queue"); + RELEASE(obj); + obj = [OpFlag new]; [q addOperation: obj]; [q waitUntilAllOperationsAreFinished];