Bagaimana cara saya menunggu blok yang dikirim secara tidak sinkron untuk diselesaikan?

180

Saya menguji beberapa kode yang melakukan pemrosesan asinkron menggunakan Grand Central Dispatch. Kode pengujian terlihat seperti ini:

[object runSomeLongOperationAndDo:^{
    STAssert
}];

Tes harus menunggu operasi selesai. Solusi saya saat ini terlihat seperti ini:

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

Yang terlihat agak kasar, apakah Anda tahu cara yang lebih baik? Saya dapat membuka antrean lalu memblokir dengan menelepon dispatch_sync:

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

... tapi itu mungkin mengekspos terlalu banyak pada object.

zoul
sumber

Jawaban:

302

Mencoba menggunakan a dispatch_semaphore. Seharusnya terlihat seperti ini:

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]]; 
    }
}

Ini harus berperilaku benar bahkan jika runSomeLongOperationAndDo:memutuskan bahwa operasi sebenarnya tidak cukup lama untuk mendapatkan threading dan berjalan secara sinkron.

kperryua
sumber
61
Kode ini tidak berfungsi untuk saya. STAssert saya tidak akan pernah dieksekusi. Saya harus mengganti dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);denganwhile (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; }
nicktmro
41
Itu mungkin karena blok penyelesaian Anda dikirim ke antrian utama? Antrian diblokir menunggu semafor dan karenanya tidak pernah menjalankan blok. Lihat pertanyaan ini tentang pengiriman antrian utama tanpa pemblokiran.
zoul
3
Saya mengikuti saran @ Zoul & nicktmro. Tapi sepertinya akan menemui jalan buntu. Test Case '- [BlockTestTest testAsync]' dimulai. tetapi tidak pernah berakhir
NSCry
3
Apakah Anda perlu melepaskan semaphore di bawah ARC?
Peter Warbo
14
ini persis apa yang saya cari. Terima kasih! @PeterWarbo tidak, Anda tidak. Penggunaan ARC menghilangkan kebutuhan untuk melakukan dispatch_release ()
Hulvej
29

Selain teknik semaphore yang dibahas secara mendalam dalam jawaban lain, kita sekarang dapat menggunakan XCTest di Xcode 6 untuk melakukan tes asinkron via XCTestExpectation. Ini menghilangkan kebutuhan untuk semaphores saat menguji kode asinkron. Sebagai contoh:

- (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];
}

Demi pembaca masa depan, sementara teknik semaphore pengiriman adalah teknik yang luar biasa ketika benar-benar dibutuhkan, saya harus mengakui bahwa saya melihat terlalu banyak pengembang baru, tidak terbiasa dengan pola pemrograman asinkron yang baik, tertarik terlalu cepat ke semafor sebagai mekanisme umum untuk membuat asinkron. rutinitas berperilaku serempak. Lebih buruk lagi saya telah melihat banyak dari mereka menggunakan teknik semaphore ini dari antrian utama (dan kita tidak boleh memblokir antrian utama dalam aplikasi produksi).

Saya tahu ini tidak terjadi di sini (ketika pertanyaan ini diposting, tidak ada alat yang bagus seperti XCTestExpectation; juga, di suite pengujian ini, kami harus memastikan tes tidak selesai sampai panggilan asinkron dilakukan). Ini adalah salah satu situasi langka di mana teknik semaphore untuk memblokir utas mungkin diperlukan.

Jadi dengan permintaan maaf saya kepada penulis pertanyaan asli ini, untuk siapa teknik semafornya baik, saya menulis peringatan ini kepada semua pengembang baru yang melihat teknik semafor ini dan mempertimbangkan menerapkannya dalam kode mereka sebagai pendekatan umum untuk berurusan dengan asinkron. metode: Diperingatkan bahwa sembilan dari sepuluh, teknik semaphore tidakpendekatan terbaik saat menghadapi operasi asinkron. Alih-alih, biasakan diri Anda dengan pola blok penutupan / penutupan, serta pola dan notifikasi delegate-protokol. Ini sering merupakan cara yang jauh lebih baik untuk menangani tugas-tugas yang tidak sinkron, daripada menggunakan semafor untuk membuatnya berperilaku serempak. Biasanya ada alasan bagus bahwa tugas asinkronik dirancang untuk berperilaku asinkron, jadi gunakan pola asinkron yang tepat daripada mencoba membuatnya berperilaku sinkron.

rampok
sumber
1
Saya pikir ini harus menjadi jawaban yang diterima sekarang. Di sini juga ada dokumen: developer.apple.com/library/prerelease/ios/documentation/…
hris.to
Saya punya pertanyaan tentang ini. Saya punya beberapa kode asinkron yang melakukan sekitar selusin panggilan unduhan AFNetworking untuk mengunduh satu dokumen. Saya ingin menjadwalkan unduhan pada NSOperationQueue. Kecuali saya menggunakan sesuatu seperti semafor, unduhan dokumen NSOperationakan segera muncul untuk menyelesaikan dan tidak akan ada antrian unduhan nyata - mereka akan melanjutkan banyak proses secara bersamaan, yang tidak saya inginkan. Apakah semafor masuk akal di sini? Atau ada cara yang lebih baik untuk membuat NSOperations menunggu akhir yang tidak sinkron dari yang lain? Atau sesuatu yang lain?
Benjohn
Tidak, jangan gunakan semaphores dalam situasi ini. Jika Anda memiliki operasi antrian yang Anda tambahkan AFHTTPRequestOperationobjek, maka Anda harus hanya membuat operasi penyelesaian (yang Anda akan bergantung pada operasi lain). Atau gunakan grup pengiriman. BTW, Anda mengatakan Anda tidak ingin mereka berjalan secara bersamaan, yang baik jika itu yang Anda butuhkan, tetapi Anda membayar penalti kinerja serius melakukan ini secara berurutan daripada secara bersamaan. Saya biasanya menggunakan maxConcurrentOperationCount4 atau 5.
Rob
28

Saya baru saja datang ke masalah ini lagi dan menulis kategori berikut di 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

Dengan cara ini saya dapat dengan mudah mengubah panggilan tidak sinkron dengan panggilan balik menjadi panggilan sinkron:

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

Umumnya tidak menggunakan jawaban ini, mereka sering tidak akan skala (ada pengecualian di sana-sini, tentu saja)

Pendekatan-pendekatan ini tidak sesuai dengan bagaimana GCD dimaksudkan untuk bekerja dan pada akhirnya akan menyebabkan kebuntuan dan / atau membunuh baterai dengan polling nonstop.

Dengan kata lain, atur ulang kode Anda sehingga tidak ada yang sinkron menunggu hasil, tetapi alih-alih berurusan dengan hasil yang diberitahukan tentang perubahan status (mis. Protokol panggilan balik / delegasi, tersedia, pergi, kesalahan, dll.). (Ini dapat di refactored menjadi blok jika Anda tidak suka panggilan balik neraka.) Karena ini adalah bagaimana mengekspos perilaku nyata ke seluruh aplikasi daripada menyembunyikannya di balik façade palsu.

Sebagai gantinya, gunakan NSNotificationCenter , tentukan protokol delegasi khusus dengan callback untuk kelas Anda. Dan jika Anda tidak suka mucking dengan callback delegate seluruh, bungkus mereka menjadi kelas proxy konkret yang mengimplementasikan protokol khusus dan menyimpan berbagai blok di properti. Mungkin juga memberikan kemudahan konstruktor juga.

Pekerjaan awal sedikit lebih tetapi itu akan mengurangi jumlah kondisi ras yang mengerikan dan pemilihan baterai yang mematikan dalam jangka panjang.

(Jangan meminta contoh, karena itu sepele dan kami harus menginvestasikan waktu untuk mempelajari dasar-dasar obyektif juga.)


sumber
1
Ini peringatan penting karena pola desain obj-C dan testabilitas juga
BootMaker
8

Inilah trik bagus yang tidak menggunakan semaphore:

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

Apa yang Anda lakukan adalah menunggu menggunakan dispatch_syncdengan blok kosong untuk Sinkron menunggu pada antrian pengiriman serial sampai blok A-Synchronous telah selesai.

Leslie Godwin
sumber
Masalah dengan jawaban ini adalah bahwa itu tidak mengatasi masalah asli OP, yaitu bahwa API yang perlu digunakan mengambil Handler penyelesaian sebagai argumen dan segera kembali. Menyebut bahwa API di dalam blok async jawaban ini akan segera kembali meskipun Handler penyelesaian belum berjalan. Kemudian blok sinkronisasi akan dijalankan sebelum Handler selesai.
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);
}

Contoh penggunaan:

[self performAndWait:^(dispatch_semaphore_t semaphore) {
  [self someLongOperationWithSuccess:^{
    dispatch_semaphore_signal(semaphore);
  }];
}];
Oliver Atkinson
sumber
2

Ada juga SenTestingKitAsync yang memungkinkan Anda menulis kode seperti ini:

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

(Lihat artikel objc.io untuk detailnya.) Dan karena Xcode 6 ada AsynchronousTestingkategori XCTestyang memungkinkan Anda menulis kode seperti ini:

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

Ini adalah alternatif dari salah satu tes saya:

__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);
Peter DeWeese
sumber
1
Ada kesalahan dalam kode di atas. Dari NSCondition dokumentasi untuk -waitUntilDate:"Anda harus mengunci penerima sebelum memanggil metode ini." Jadi -unlockseharusnya setelah -waitUntilDate:.
Patrick
Ini tidak menskalakan apa pun yang menggunakan banyak utas atau menjalankan antrian.
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]];
}

Ini berhasil untuk saya.


sumber
3
baik, itu menyebabkan penggunaan CPU yang tinggi
kevin
4
@kevin Yup, ini adalah polling ghetto yang akan membunuh baterai.
@ Larry, bagaimana cara mengkonsumsi lebih banyak baterai. tolong dibimbing.
pkc456
@ pkc456 Lihat di buku ilmu komputer tentang perbedaan antara cara polling dan notifikasi sinkron. Semoga berhasil.
2
Empat setengah tahun kemudian dan dengan pengetahuan dan pengalaman yang saya peroleh, saya tidak akan merekomendasikan jawaban saya.
0

Terkadang, Timeout loops juga membantu. Bolehkah Anda menunggu sampai Anda mendapatkan sinyal (mungkin BOOL) dari metode panggilan balik async, tetapi bagaimana jika tidak ada respons, dan Anda ingin keluar dari loop itu? Berikut ini solusinya, kebanyakan dijawab di atas, tetapi dengan tambahan Timeout.

#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];
}
Khulja Sim Sim
sumber
1
Masalah yang sama: usia baterai gagal.
1
@ Larry Tidak yakin bahkan jika Anda melihat kode. Ada TIMEOUT_SECONDS periode di mana jika panggilan async tidak merespons, itu akan memutus loop. Itu hack untuk memecahkan kebuntuan. Kode ini berfungsi sempurna tanpa mematikan baterai.
Khulja Sim Sim
0

Solusi yang sangat primitif untuk masalah ini:

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

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

Swift 4:

Gunakan synchronousRemoteObjectProxyWithErrorHandleralih-alih remoteObjectProxysaat membuat objek jarak jauh. Tidak perlu lagi semafor.

Contoh di bawah ini akan mengembalikan versi yang diterima dari proxy. Tanpa synchronousRemoteObjectProxyWithErrorHandleritu akan crash (mencoba mengakses memori yang tidak dapat diakses):

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
}
Freek Sanders
sumber
-1

Saya harus menunggu sampai UIWebView dimuat sebelum menjalankan metode saya, saya bisa mendapatkan ini bekerja dengan melakukan pemeriksaan siap UIWebView pada utas utama menggunakan GCD dalam kombinasi dengan metode semafor yang disebutkan dalam utas ini. Kode akhir terlihat seperti ini:

-(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

}
Albert Renshaw
sumber