在Cocoa应用中使用GCD运行Python脚本

16 投票
1 回答
8603 浏览
提问于 2025-04-17 10:14

我正在尝试从一个Cocoa应用程序中运行一个Python脚本。在主线程上运行得很好,但我希望它能在后台运行,也就是在一个并发的GCD队列中。

我使用以下方法来设置一个管理类,以便运行这个Python脚本:

- (BOOL)setupPythonEnvironment {
    if (Py_IsInitialized()) return YES;

    Py_SetProgramName("/usr/bin/python");
    Py_Initialize();

    NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"MyScript"     ofType:@"py"];

    FILE *mainFile = fopen([scriptPath UTF8String], "r");
    return (PyRun_SimpleFile(mainFile, (char *)[[scriptPath lastPathComponent] UTF8String]) == 0);
}

然后,这个脚本会通过以下实例方法被(反复)调用,使用的是管理类的一个共享单例实例:

- (id)runScriptWithArguments:(NSArray *)arguments {
    return [NSClassFromString(@"MyScriptExecutor") runWithArguments:arguments];
}

上面的Objective-C代码连接到以下的Python代码:

from Foundation import *

def run_with_arguments(arguments):
#    ...a long-running script

class MyScriptExecutor(NSObject):
    @classmethod
    def runWithArguments_(self, arguments):
        return run_with_arguments(arguments)

当我总是从主队列运行上面的Objective-C方法时,这一切都能正常工作,但当从其他队列运行时,脚本返回的是null。有人能告诉我我想做的事情是否根本不被支持,以及有没有好的解决办法吗?

这个Python脚本调用得很频繁,而且运行时间也很长,所以在主线程上运行会太慢,就像在一个串行队列中运行一样。此外,我希望尽可能把并发代码都放在Objective-C里。

谢谢,

1 个回答

14

根据这个页面,在嵌入Python时,有一些比较复杂的线程问题。那有没有可能直接在一个单独的进程中运行这些脚本呢?比如,下面这个-runBunchOfScripts方法会在一个并行的后台队列中运行这个脚本十次(通过调用-runPythonScript),把结果收集到一个字符串数组里,然后在所有脚本完成后再在主线程中调用你的对象:

- (NSString*)runPythonScript
{
    NSTask* task = [[[NSTask alloc] init] autorelease];
    task.launchPath = @"/usr/bin/python";  
    NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"MyScript" ofType:@"py"];
    task.arguments = [NSArray arrayWithObjects: scriptPath, nil];

    // NSLog breaks if we don't do this...
    [task setStandardInput: [NSPipe pipe]];

    NSPipe *stdOutPipe = nil;
    stdOutPipe = [NSPipe pipe];
    [task setStandardOutput:stdOutPipe];

    NSPipe* stdErrPipe = nil;
    stdErrPipe = [NSPipe pipe];
    [task setStandardError: stdErrPipe];

    [task launch];        

    NSData* data = [[stdOutPipe fileHandleForReading] readDataToEndOfFile];

    [task waitUntilExit];

    NSInteger exitCode = task.terminationStatus;

    if (exitCode != 0)
    {
        NSLog(@"Error!");
        return nil;
    }

    return [[[NSString alloc] initWithBytes: data.bytes length:data.length encoding: NSUTF8StringEncoding] autorelease];
}

- (void)runBunchOfScripts
{
    dispatch_group_t group = dispatch_group_create();
    NSMutableArray* results = [[NSMutableArray alloc] init];
    for (NSUInteger i = 0; i < 10; i++)
    {
        dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSString* result = [self runPythonScript];
            @synchronized(results)
            {
                [results addObject: result];
            }
        });
    }

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        [self scriptsDidFinishWithResults: results];
        dispatch_release(group);
        [results release];
    });
}

- (void)scriptsDidFinishWithResults: (NSArray*)results
{
    NSLog(@"Do something with the results...");
}

当然,使用单独进程的方法也有它的局限性,最明显的就是你能启动的进程数量是有限制的。不过,这种方法似乎比嵌入整个解释器要安全得多。我认为,除非你需要脚本和主环境之间频繁互动,否则这种方法会更好。

撰写回答