使用GCD从Cocoa应用程序运行Python脚本

16

我正在尝试从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中。

谢谢。


这个例子中的信息不足以让人真正了解你在做什么。 - jkh
1
你能告诉我你是如何将Python二进制文件与Xcode集成的吗? - bijan
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...");
}

自然,使用独立进程的方法有其局限性,其中最大的局限是您可以启动的进程数量的硬限制,但它似乎比嵌入整个解释器更少充满危险。我会说,除非您需要在脚本和托管环境之间进行交互,否则这将是一种更好的方法。


谢谢,@ipmcc。不幸的是,脚本将必须运行多次。很快就会达到那个严格的限制。这就是为什么我希望只设置一次Python环境,每次运行脚本时只留下最小的开销。实际上,脚本被发送到Python执行器作为NSString,使用“安全”的eval版本进行评估。 - Bastiaan M. van de Weerd
3
你可以使用 NSTask 来实现类似的解决方案;启动一个(或n个)长期运行的 Python 解释器任务,然后从标准输入读取脚本并进行 eval 运算,最后收集标准输出的结果。如果你的脚本只需要使用字符串进行交互,那么与其嵌入解释器,这种方法会更加合理。(至少从我的角度来看是这样的。) - ipmcc
太棒了。那肯定可以做到! - Bastiaan M. van de Weerd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接