Eşzamansız olarak gönderilen bir bloğun bitmesini nasıl beklerim?


180

Grand Central Dispatch kullanarak eşzamansız işlem yapan bazı kodları test ediyorum. Test kodu şuna benzer:

[object runSomeLongOperationAndDo:^{
    STAssert
}];

Testler, işlemin bitmesini beklemek zorundadır. Mevcut çözümüm şöyle:

__block BOOL finished = NO;
[object runSomeLongOperationAndDo:^{
    STAssert
    finished = YES;
}];
while (!finished);

Hangisi biraz kaba görünüyor, daha iyi bir yol biliyor musunuz? Ben kuyruk maruz ve sonra arayarak engelleyebilir dispatch_sync:

[object runSomeLongOperationAndDo:^{
    STAssert
}];
dispatch_sync(object.queue, ^{});

… Ama bu belki de çok fazla şey ifade ediyor object.

Yanıtlar:


302

Kullanmaya çalışmak a dispatch_semaphore. Bunun gibi bir şeye benzemeli:

dispatch_semaphore_t sema = dispatch_semaphore_create(0);

[object runSomeLongOperationAndDo:^{
    STAssert

    dispatch_semaphore_signal(sema);
}];

if (![NSThread isMainThread]) {
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
} else {
    while (dispatch_semaphore_wait(sema, DISPATCH_TIME_NOW)) { 
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]]; 
    }
}

runSomeLongOperationAndDo:İşlemin gerçekten diş açmaya yetecek kadar uzun olmadığına ve bunun yerine eşzamanlı olarak çalışmasına karar verse bile bu doğru davranmalıdır .


61
Bu kod benim için çalışmadı. STAssert'im asla çalışmaz. Ben değiştirmek zorunda dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);olanwhile (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; }
nicktmro

41
Muhtemelen tamamlama bloğunuz ana kuyruğa gönderilir mi? Kuyruk semaforu beklerken engellenir ve bu nedenle bloğu asla yürütmez. Ana kuyrukta engelleme olmadan gönderme hakkında bu soruya bakın .
zoul

3
@Zoul & nicktmro önerisini takip ettim. Ama çıkmaza girecek gibi görünüyor. Test Durumu '- [BlockTestTest testAsync]' başladı. ama hiç bitmedi
NSCry

3
Semaforu ARC altında bırakmanız mı gerekiyor?
Peter Warbo

14
tam da aradığım şey buydu. Teşekkürler! @PeterWarbo hayır bilmiyorsun. ARC kullanımı bir dispatch_release () yapma ihtiyacını ortadan kaldırır
Hulvej

29

Diğer cevaplarda ayrıntılı olarak ele alınan semafor tekniğine ek olarak, artık Xcode 6'da XCTest'i kullanarak eşzamansız testler yapabiliyoruz XCTestExpectation. Bu, eşzamansız kod test edilirken semafor ihtiyacını ortadan kaldırır. Örneğin:

- (void)testDataTask
{
    XCTestExpectation *expectation = [self expectationWithDescription:@"asynchronous request"];

    NSURL *url = [NSURL URLWithString:@"http://www.apple.com"];
    NSURLSessionTask *task = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        XCTAssertNil(error, @"dataTaskWithURL error %@", error);

        if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
            NSInteger statusCode = [(NSHTTPURLResponse *) response statusCode];
            XCTAssertEqual(statusCode, 200, @"status code was not 200; was %d", statusCode);
        }

        XCTAssert(data, @"data nil");

        // do additional tests on the contents of the `data` object here, if you want

        // when all done, Fulfill the expectation

        [expectation fulfill];
    }];
    [task resume];

    [self waitForExpectationsWithTimeout:10.0 handler:nil];
}

Gelecekteki okuyucular uğruna, sevk semaforu tekniği kesinlikle ihtiyaç duyulduğunda harika bir teknik olsa da, itiraf etmeliyim ki, asenkron programlama modellerine aşina olmayan çok fazla yeni geliştirici, asenkron yapmak için genel bir mekanizma olarak semaforlara çok çabuk yaklaşıyor rutinler eşzamanlı davranır. Daha da kötüsü, bu semafor tekniğini ana kuyruktan kullandığını gördüm (ve üretim uygulamalarındaki ana kuyruğu asla engellememeliyiz).

Burada böyle olmadığını biliyorum (bu soru gönderildiğinde, böyle güzel bir araç yoktu XCTestExpectation; Ayrıca, bu test paketlerinde, eşzamansız çağrı yapılana kadar testin bitmediğinden emin olmalıyız). Bu, ana ipliği bloke etmek için semafor tekniğinin gerekli olabileceği nadir durumlardan biridir.

Bu yüzden, semafor tekniğinin sağlam olduğu bu orijinal sorunun yazarından özür dilerim, bu uyarıyı bu semafor tekniğini gören tüm yeni geliştiricilere yazıyorum ve kodunda asenkron ile uğraşmak için genel bir yaklaşım olarak görmeyi düşünüyorum yöntem: on üzerinden dokuz kez, semafor teknik olduğunu forewarned değileşzamansız işlemlerle karşılaşırken en iyi yaklaşım. Bunun yerine, tamamlama bloğu / kapatma kalıplarının yanı sıra delege protokolü kalıplarını ve bildirimlerini öğrenin. Bunlar, eşzamanlı davranmalarını sağlamak için semaforlar kullanmak yerine, asenkron görevlerle uğraşmak için genellikle çok daha iyi yollardır. Genellikle eşzamansız görevlerin eşzamansız davranacak şekilde tasarlanmasının iyi nedenleri vardır, bu nedenle eşzamanlı davranmaya çalışmak yerine doğru eşzamansız deseni kullanın.


1
Sanırım bu şimdi kabul edilen cevap olmalı. Dokümanlar da
şöyledir

Bununla ilgili bir sorum var. Tek bir belgeyi indirmek için yaklaşık bir düzine AFNetworking indirme çağrısı yapan asenkron kodum var. İndirme işlemlerini bir NSOperationQueue. Bir semafor gibi bir şey kullanmazsam, belge indirme işlemleri NSOperationhemen tamamlanmış gibi görünür ve indirme işlemlerinde gerçek bir kuyruk olmayacaktır - eşzamanlı olarak ilerleyeceklerdir, ki istemiyorum. Semaforlar burada makul mü? Yoksa NSOperations'ın diğerlerinin asenkron sonunu beklemesini sağlamanın daha iyi bir yolu var mı? Veya başka bir şey?
Benjohn

Hayır, bu durumda semafor kullanmayın. AFHTTPRequestOperationNesneleri eklediğiniz işlem kuyruğunuz varsa , o zaman bir tamamlama işlemi oluşturmanız gerekir (bu işlem diğer işlemlere bağımlı olacaktır). Veya dağıtım gruplarını kullanın. BTW, onların eşzamanlı olarak çalışmasını istemediğinizi söylüyorsunuz, bu da ihtiyacınız olan şeyse iyi, ancak eşzamanlı olarak değil, bunu sırayla yaparak ciddi performans cezası ödersiniz. Genelde maxConcurrentOperationCount4 veya 5 kullanıyorum .
Rob

28

Kısa süre önce bu konuya tekrar geldim ve şu kategoriyi yazdım NSObject:

@implementation NSObject (Testing)

- (void) performSelector: (SEL) selector
    withBlockingCallback: (dispatch_block_t) block
{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self performSelector:selector withObject:^{
        if (block) block();
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    dispatch_release(semaphore);
}

@end

Bu şekilde kolayca bir geri arama ile asenkron çağrı senkron test birine dönüştürebilirsiniz:

[testedObject performSelector:@selector(longAsyncOpWithCallback:)
    withBlockingCallback:^{
    STAssert
}];

24

Genellikle bu cevapların hiçbirini kullanmayın, genellikle ölçeklenmezler (burada ve orada istisnalar vardır, elbette)

Bu yaklaşımlar, GCD'nin nasıl çalışacağı ile uyumsuzdur ve sonuçta kilitlenmeye neden olan ve / veya durmadan yoklama yoluyla pili öldüren sonuç olacaktır.

Başka bir deyişle, kodunuzu bir sonuç için eşzamanlı bir bekleme olmayacak şekilde yeniden düzenleyin, bunun yerine durum değişikliği konusunda bilgilendirilen bir sonuçla ilgilenin (örn. Geri aramalar / delege protokolleri, kullanılabilir olma, uzaklaşma, hatalar, vb.). (Geri arama cehenneminden hoşlanmıyorsanız bunlar bloklara yeniden yerleştirilebilir.) Çünkü gerçek bir davranışı, yanlış bir cephenin arkasına gizlemekten ziyade uygulamanın geri kalanına nasıl gösterebilirsiniz.

Bunun yerine, NSNotificationCenter kullanın , sınıfınız için geri çağrıları olan özel bir temsilci protokolü tanımlayın. Temsilci geri çağrıları ile mucking yapmayı sevmiyorsanız, bunları özel protokolü uygulayan ve çeşitli bloğu özelliklerde kaydeden somut bir proxy sınıfına sarın. Muhtemelen aynı zamanda kolaylık kurucuları da sağlamaktadır.

İlk çalışma biraz daha fazladır, ancak uzun vadede korkunç yarış koşulları ve pil cinayeti yoklama sayısını azaltacaktır.

(Bir örnek istemeyin, çünkü önemsizdir ve objektif-c temellerini öğrenmek için zaman harcamak zorunda kaldık.)


1
Ob

8

İşte semafor kullanmayan şık bir numara:

dispatch_queue_t serialQ = dispatch_queue_create("serialQ", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQ, ^
{
    [object doSomething];
});
dispatch_sync(serialQ, ^{ });

Yapmanız gereken, dispatch_syncA-Synchronous bloğu tamamlanana kadar seri bir dağıtım kuyruğunda Senkronize olarak beklemek için boş bir blokla beklemektir.


Bu yanıtla ilgili sorun, OP'nin orijinal sorununa değinmemesi, yani kullanılması gereken API'nin bir completeionHandler'ı argüman olarak alması ve hemen geri dönmesidir. CompletionHandler henüz çalışmadığı halde bu API'yi bu yanıtın zaman uyumsuz bloğunun içinde çağırmak hemen dönecektir. Daha sonra senkronizasyon bloğu completionHandler'dan önce yürütülecektir.
BTRUE

6
- (void)performAndWait:(void (^)(dispatch_semaphore_t semaphore))perform;
{
  NSParameterAssert(perform);
  dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
  perform(semaphore);
  dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
  dispatch_release(semaphore);
}

Örnek kullanım:

[self performAndWait:^(dispatch_semaphore_t semaphore) {
  [self someLongOperationWithSuccess:^{
    dispatch_semaphore_signal(semaphore);
  }];
}];

2

Bunun gibi kod yazmanıza izin veren SenTestingKitAsync de var :

- (void)testAdditionAsync {
    [Calculator add:2 to:2 block^(int result) {
        STAssertEquals(result, 4, nil);
        STSuccess();
    }];
    STFailAfter(2.0, @"Timeout");
}

(Ayrıntılar için objc.io makalesine bakın.) Ve Xcode 6'dan beri AsynchronousTesting, XCTestkodu şöyle yazmanıza izin veren bir kategori var :

XCTestExpectation *somethingHappened = [self expectationWithDescription:@"something happened"];
[testedObject doSomethigAsyncWithCompletion:^(BOOL succeeded, NSError *error) {
    [somethingHappened fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:NULL];

1

İşte testlerimden bir alternatif:

__block BOOL success;
NSCondition *completed = NSCondition.new;
[completed lock];

STAssertNoThrow([self.client asyncSomethingWithCompletionHandler:^(id value) {
    success = value != nil;
    [completed lock];
    [completed signal];
    [completed unlock];
}], nil);    
[completed waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];
[completed unlock];
STAssertTrue(success, nil);

1
Yukarıdaki kodda bir hata var. Gönderen NSCondition belgeler için -waitUntilDate:"bu yöntemi çağırmadan alıcıyı önce kilitlemelisiniz." Yani sonra -unlockolmalı -waitUntilDate:.
Patrick

Bu, birden çok iş parçacığı veya çalışma kuyruğu kullanan hiçbir şeyle ölçeklendirilmez.

0
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[object blockToExecute:^{
    // ... your code to execute
    dispatch_semaphore_signal(sema);
}];

while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
    [[NSRunLoop currentRunLoop]
        runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0]];
}

Bu benim için yaptı.


3
iyi, ama yüksek cpu kullanımına neden olur
kevin

4
@kevin Yup, bu pili öldürecek getto anketi.

@Marry, nasıl daha fazla pil tüketir. lütfen yönlendirin.
pkc456

@ pkc456 Bir bilgisayar bilimi kitabında, yoklama ve eşzamansız bildirim arasındaki farklar hakkında bir göz atın. İyi şanslar.

2
Dört buçuk yıl sonra kazandığım bilgi ve deneyimle cevabımı tavsiye etmem.

0

Bazen, Zaman Aşımı döngüleri de yardımcı olabilir. Zaman uyumsuz geri çağırma yönteminden (BOOL olabilir) sinyal alana kadar bekleyebilir misiniz, ama hiç yanıt yoksa ve bu döngüden kurtulmak ister misiniz? Aşağıda, çoğunlukla yukarıda cevaplanan, ancak Zaman Aşımı eklenmiş bir çözüm var.

#define CONNECTION_TIMEOUT_SECONDS      10.0
#define CONNECTION_CHECK_INTERVAL       1

NSTimer * timer;
BOOL timeout;

CCSensorRead * sensorRead ;

- (void)testSensorReadConnection
{
    [self startTimeoutTimer];

    dispatch_semaphore_t sema = dispatch_semaphore_create(0);

    while (dispatch_semaphore_wait(sema, DISPATCH_TIME_NOW)) {

        /* Either you get some signal from async callback or timeout, whichever occurs first will break the loop */
        if (sensorRead.isConnected || timeout)
            dispatch_semaphore_signal(sema);

        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:[NSDate dateWithTimeIntervalSinceNow:CONNECTION_CHECK_INTERVAL]];

    };

    [self stopTimeoutTimer];

    if (timeout)
        NSLog(@"No Sensor device found in %f seconds", CONNECTION_TIMEOUT_SECONDS);

}

-(void) startTimeoutTimer {

    timeout = NO;

    [timer invalidate];
    timer = [NSTimer timerWithTimeInterval:CONNECTION_TIMEOUT_SECONDS target:self selector:@selector(connectionTimeout) userInfo:nil repeats:NO];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}

-(void) stopTimeoutTimer {
    [timer invalidate];
    timer = nil;
}

-(void) connectionTimeout {
    timeout = YES;

    [self stopTimeoutTimer];
}

1
Aynı sorun: pil ömrü başarısız.

1
@Barry Eğer koda baksanız bile emin değilim. Zaman uyumsuz çağrı yanıt vermezse döngüyü kıracak TIMEOUT_SECONDS dönemi vardır. Bu kilitlenmeyi kırmak için bir hack. Bu kod, pili öldürmeden mükemmel çalışır.
Khulja Sim Sim

0

Soruna çok ilkel bir çözüm:

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

[object runSomeLongOperationAndDo:^{
    STAssert
    nextOperationAfterLongOperationBlock();
}];

0

Hızlı 4:

Uzak nesneyi oluşturmak synchronousRemoteObjectProxyWithErrorHandleryerine kullanın remoteObjectProxy. Artık bir semafora gerek yok.

Aşağıdaki örnek proxy'den alınan sürümü döndürecektir. Bu olmadan synchronousRemoteObjectProxyWithErrorHandlerçökecek (erişilebilir olmayan belleğe erişmeye çalışarak):

func getVersion(xpc: NSXPCConnection) -> String
{
    var version = ""
    if let helper = xpc.synchronousRemoteObjectProxyWithErrorHandler({ error in NSLog(error.localizedDescription) }) as? HelperProtocol
    {
        helper.getVersion(reply: {
            installedVersion in
            print("Helper: Installed Version => \(installedVersion)")
            version = installedVersion
        })
    }
    return version
}

-1

Benim yöntem çalıştırmadan önce bir UIWebView yüklenene kadar beklemek zorunda, bu iş parçacığında bahsedilen semafor yöntemleri ile birlikte GCD kullanarak ana iş parçacığında UIWebView hazır kontroller gerçekleştirerek bu çalışma başardı. Son kod şöyle görünür:

-(void)myMethod {

    if (![self isWebViewLoaded]) {

            dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

            __block BOOL isWebViewLoaded = NO;

            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

                while (!isWebViewLoaded) {

                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((0.0) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                        isWebViewLoaded = [self isWebViewLoaded];
                    });

                    [NSThread sleepForTimeInterval:0.1];//check again if it's loaded every 0.1s

                }

                dispatch_sync(dispatch_get_main_queue(), ^{
                    dispatch_semaphore_signal(semaphore);
                });

            });

            while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]];
            }

        }

    }

    //Run rest of method here after web view is loaded

}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.