SMGenerator 0.1

SMGenerator 0.1

测试已测试
语言 Obj-CObjective C
许可协议 MIT
发布最后发布2014年12月

Michael Shkutkov 维护。



  • Michael Shkutkov

SMGenerator: Objective C 中创建生成器的实验性快速且简单的方法

概述

如您所知,Objective-C 并不支持原生的 生成器,但它们可能非常有用,可以表达一些想法。例如 Python 有这样的支持,并且有时它们被 非常频繁地使用。Python 中最简单的具有意义的生成器看起来像这样

def countfrom(n):
    while True:
        yield n
        n += 1

yield 语句与常规函数中的 return 类似。区别在于生成器状态被保存,在下一次调用时将被恢复,并且执行将继续(而不是像常规函数那样从头开始)。

Mike Ash 在他很久以前的一个精彩的博客中 讨论了这个问题。他 提出了创建 Objective-C 生成器的一个解决方案。类似的技术也被用于 libextobjc 中的 EXTCoroutine,这是来自 libextobjc 的一种技术。

SMGenerator 建议在 Objective-C 中创建生成器的另一种方法。让我们更深入地探讨并讨论这种方法的优势和劣势。

想法

生成器背后的主要思想是它们在 "return" 时保存其状态。如果我们在另一个线程中启动我们的函数并在代码执行 yield 结果时停止呢?在停止之前,我们将该结果值传递给原始线程,该线程将通过生成器返回。在下一次执行时,我们只需重新启动线程并继续函数的评估。这个想法相当简单,让我们看看生成器将是什么样的。

基本实现

SMGenerator 的主要目标是创建和使用上的简单性。生成器通常用于循环中,所以如果我们可以编写类似这样的代码,那将非常酷

for (NSObject *object in generator) {
    //...
}

使用 SMGenerator,您可以这样做,因为它采用了 NSFastEnumeration 协议。使用 SMGenerator,Objective-C 中有意义的简单生成器将是

SM_GENERATOR(^(NSNumber *n) {
    while (TRUE) {
        SM_YIELD(n);
        n = @([n intValue] + 1);
    }
}, (@1));

SM_GENERATOR 和 SM_YIELD 是宏,允许我们编写更少的代码。SM_GENERATOR 至少有一个参数 - block,它使用 SM_YIELD 产生值。如果 block 有参数,您可以将它们传递给 SM_GENERATOR 作为第二个、第三个等参数。

您有两种使用生成器的方式

在 for/in 循环中

像这样

SMGenerator *generator = SM_GENERATOR(^(NSNumber *n) {
    while (TRUE) {
        SM_YIELD(n);
        n = @([n intValue] + 1);
    }
}, (@1));

for (NSNumber *num in generator) {
    NSLog(@"Number %@", num);
}

或者在某些情况下,也可以避免使用局部变量。

for (NSNumber *num in SM_GENERATOR(^(NSNumber *n) {
    while (TRUE) {
        SM_YIELD(n);
        n = @([n intValue] + 1);
    }
}, (@1))) {
    NSLog(@"Number %@", num);
}

通过向生成器发送“next”消息进行手动操作

SMGenerator有一个next方法

/*!
 * @method next
 *
 * @abstract
 * Produces next value
 *
 * @discussion
 * This method waits while next value will be processed by external block
 * If external block is ended, this method returns nil
 *
 * @result
 * Next generated value or nil
 */
- (id)next;

因此,只需向生成器发送“next”消息就可以简单地获取值

NSLog(@"Number 1 %@", [generator next]);
NSLog(@"Number 2 %@", [generator next]);
NSLog(@"Number 3 %@", [generator next]);

顺便说一下,在用户块中同时使用多个SM_YIELD语句不是问题。

SMGenerator *generator = SM_GENERATOR(^{
    while (TRUE) {
        SM_YIELD(@"one");
        SM_YIELD(@"two");
        SM_YIELD(@"three");
    }
});

更多技术细节

SMGenerator使用GCD在自身的队列上运行用户块。因此,该块在另一个线程中运行,并使用信号量实现与原始线程的同步(不仅限于主线程,也可能是创建您的生成器的任何线程)。实际上,只有当原始线程阻塞时,用户代码才会工作。

  1. 我们请求SMGenerator获取新值(例如,向实例发送next消息)
  2. SMGenerator恢复用户块并等待结果
  3. 用户块在另一个线程中开始工作,当结果准备好时,它会通知SMGenerator并停止执行
  4. SMGenerator接收到新值并将其返回给原始线程

通过这种方法,可以安全地从外部作用域修改对象和变量。例如,我们可以将“有意义的简单生成器”重写如下

__block NSNumber *n = @(1);
SMGenerator *generator = SM_GENERATOR(^{
    while (TRUE) {
        SM_YIELD(n);
        n = @([n intValue] + 1);
    }
});

一大步

可能在阅读了上一节后,你说:“停一下,如果我们异步计算下一个值,那么当请求生成器关于下一个值的询问时,它只是返回已经产生的值”。这是一个合理的评论。实际上,你可以用SMGenerator做这件事!只需使用SM_ASYNC_GENERATOR而不是SM_GENERATOR。这对于性能要求较高的生成器来说可能是一个真正的重大进步。我们的先前列出示例以异步方式重写如下

SM_ASYNC_GENERATOR(^(NSNumber *n) {
    while (TRUE) {
        SM_YIELD(n);
        n = @([n intValue] + 1);
    }
}, (@1));

但是,你应该小心使用SM_ASYNC_GENERATOR,因为它具有异步逻辑。在SM_ASYNC_GENERATOR中,使用__block变量或修改外部对象是潜在的!

警告和限制

任何想法的实现都有自己的警告。所以让我们强调一些SMGenerator的警告。

  • 用户块不能将原始类型作为参数,所以使用Objective-C对象(自定义对象、NSString、NSNumber、NSValue)
  • 可以在用户块中使用return语句,但这会停止生成器(向生成器发送"next"消息时将返回nil)
  • 如果用户块接受一些参数,则必须将它们传递给SM_GENERATOR/SM_ASYNC_GENERATOR,否则您将收到运行时错误。所以不要忘记它们。
  • 用户块必须只产生Objective-C对象。如果需要,请使用Objective-C字面量
  • 由于SMGenerator基于GCD,因此它有一些与此相关的限制。在iOS 6/7中,您不能有超过512个激活的生成器。这是一个相当大的数字,但值得注意。
  • 当使用SM_ASYNC_GENERATOR时,修改外部对象或使用__block变量时要小心。
  • SMGenerator必须与ARC构建,并针对iOS 6.0及更高版本,或Mac OS X Mountain Lion 10.8及更高版本。

不同生成器的语法比较

让我们比较SMGenerator、MAGenerator和EXTCoroutine提供的语法。

有意义的简单生成器

我们已经看到了使用SMGenerator的样子,但让我们再次重复这段代码

SMGenerator *generator = SM_GENERATOR(^(NSNumber *n) {
    while (TRUE) {
        SM_YIELD(n);
        n = @([n intValue] + 1);
    }
}, (@42));

for (NSNumber *num in generator) {
    NSLog(@"Number %@", num);
}

使用MAGenerator得到相同的结果

GENERATOR(int, CountFrom(int start), (void)) {
    __block int n;
    GENERATOR_BEGIN(void) {
        n = start;
        while(TRUE) {
            GENERATOR_YIELD(n);
            n++;
        }
    }
    GENERATOR_END
}

int (^counter)(void) = CountFrom(42);
for(int i = 0; i < 10; i++) {
    NSLog(@"Number %d", counter());
}

使用EXTCoroutine

__block int n;
int (^generator)(int) = coroutine(int from)({
    n = from;
    while(TRUE) {
        yield n;
        n++;
    }
});

for(int i = 0; i < 10; i++) {
    NSLog(@"%d", generator(42));
}

MAGenerator稍微有些冗长,同时EXTCoroutine则更加简单,但您在使用它时必须非常小心,因为如果写错了东西,它可能不会按预期工作。

int (^generator)(int) = coroutine(int n)({
    while(TRUE) {
        yield n;
        n++;
    }
});

它将产生42、43、43、43、43、43、43、43、43、43、43。这很明显吗?猜不是...

特定路径和扩展名的文件搜索

SMGenerator

SMGenerator *fileFinder = SM_GENERATOR(^(NSString *path, NSString *ext) {
    NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath: path];
    for (NSString *subpath in enumerator) {
        if([[subpath pathExtension] isEqualToString: ext]) {
            SM_YIELD((id)[path stringByAppendingPathComponent: subpath]);
        }
    }
}, @"/Applications", @"app");

for(NSString *path in fileFinder) {
    NSLog(@"%@", path);
}

MAGenerator

GENERATOR(id, FileFinder(NSString *path, NSString *extension), (void))
{
    __block NSString *subpath;
    __block NSDirectoryEnumerator *enumerator;
    GENERATOR_BEGIN(void) {
        enumerator = [[NSFileManager defaultManager] enumeratorAtPath: path];
        for (subpath in enumerator) {
            if([[subpath pathExtension] isEqualToString: extension]) {
                GENERATOR_YIELD((id)[path stringByAppendingPathComponent: subpath]);
            }
        }
    }
    GENERATOR_END
}

 for(NSString *path in MAGeneratorEnumerator(FileFinder(@"/Applications", @"app"))) {
    NSLog(@"%@", path);
}

使用EXTCoroutine实现的有限生成器变为无限(它们只是一直重复)。因此,我们需要编写更多代码来处理这个细微差别。

__block NSString *subpath;
__block NSDirectoryEnumerator *enumerator;
NSString * (^generator)(NSString *, NSString *) = coroutine(NSString *path, NSString *ext)({
    enumerator = [[NSFileManager defaultManager] enumeratorAtPath: path];
    for (subpath in enumerator) {
        if([[subpath pathExtension] isEqualToString: ext]) {
            yield [path stringByAppendingPathComponent: subpath];
        }
    }
    yield (NSString *)nil;
});

NSString *path;
do {
    path = generator(@"/Applications", @"app");
    if (path != nil) {
        NSLog(@"%@", path);
    }
} while (path != nil);

正如您从这两个示例中看到的那样,由于采用新的方法,SMGenerator提出了更稳健且更简单的解决方案。

性能

使用生成器,特别是SMGenerator的成本是什么?

让我们进行一些测试,比较同步和异步的SMGenerator、MAGenerator、EXTCoroutine,以及没有生成器实现相同任务的性能。

对于每个测试,我们测量每个实现执行10次,然后排除2个最大值和2个最小值,然后计算中位数。所有与测试相关的代码(iOS,OSX)您都可以在此GitHub项目(https://github.com/shkutkov/ObjectiveCGeneratorsPerformance)中找到。您可以抓取项目并自行运行。我已在iOS模拟器和iPhone 5上运行iOS测试,在MacBookPro 13"(2,26Gh Intel Core 2 Duo)上运行OSX测试。

测试 #1:从1生成到100000的数字

实现 iOS模拟器 iPhone 5 MacBookPro
同步SMGenerator 1.350800 3.800238 0.883701
异步SMGenerator 1.049956 3.843883 0.889207
MAGenerator 0.059778 0.214627 0.012960
EXTCoroutine 0.039936 0.116216 0.003566
没有生成器 0.002214 0.010248 0.001619

如您所见,在这次合成测试中,SMGenerator同步和异步版本都是输家。

测试 #2:将1到1000的数字打印到控制台

让我们用相同的实现只打印1000个值到控制台。

生成器名称 iOS模拟器 iPhone 5 MacBookPro
同步SMGenerator 0.705184 0.658221 0.293223
异步SMGenerator 0.695445 0.637643 0.275483
MAGenerator 0.652017 0.580639 0.246619
EXTCoroutine 0.646062 0.581865 0.244638
没有生成器 0.639307 0.562586 0.214950

如您在此示例中看到的那样,在此示例中没有SLGenerator与其他实现之间几乎没有什么区别。

让我们看一下最后一个具有重计算的示例。

计算素数的代码将不会很高效,但对我们来说这不是大问题 - 我们只需要生成器中的“重”任务。(如果您对代码感兴趣,请参阅PrimeNumbersGeneratorManager

测试 #3:生成大于100000的前1000个素数并打印到控制台

生成器名称 iOS模拟器 iPhone 5 MacBookPro
同步SMGenerator 1.319347 4.643134 3.077379
异步SMGenerator 1.192727 4.269168 2.797900
MAGenerator 1.972622 4.876356 3.539097
EXTCoroutine 1.973660 4.878249 3.540615
没有生成器 1.097907 3.298502 2.108130

在这个例子中,我们模拟了值生成需要一些时间,并且对值进行处理的场景。没有生成器的实现从代码角度来看并不优雅,但真的很快。您还可以看到,SMGenerator的异步版本在所有其他生成器实现中是最快的。这不是出乎意料的,因为在处理结果的同时,我们的生成器正在处理新的值。这可以提高您的代码在多核处理器上的性能。

将值传递到生成器块:参数与闭包

SMGenerator提供了两种方式将值传递到用户块:通过参数或者直接使用外部作用域的变量。第一种方式更加健壮(尤其是在异步版本中)且更具解释性。但除此之外,它还略微提高了效率(根据未在此展示的性能测试结果)。

结论

  • 使用生成器可能会对你的应用程序性能产生轻微影响,所以请谨慎选择。
  • SMGenerator为Objective-C中的生成器创建提供了另一种方式。它具有简单而整洁的语法。
  • 在现实世界的代码中,所有生成器的性能都相似。
  • 使用SMGenerator的异步版本,你可以真正简单地获得非常快速和高效的生成器。

如果您有任何问题、建议或补丁,请联系我:[点击此处[email protected]

许可证

SMGenerator是以MIT许可证发布的。有关完整和合法的许可证,请参阅LICENSE文件。鼓励在任何和所有类型的项目中使用,只要遵守许可证条款(它们很简单)!