ฉันจะรอให้บล็อกที่ส่งแบบอะซิงโครนัสเสร็จสิ้นได้อย่างไร


180

ฉันกำลังทดสอบโค้ดบางตัวที่ประมวลผลแบบอะซิงโครนัสโดยใช้ Grand Central Dispatch รหัสการทดสอบมีลักษณะดังนี้:

[object runSomeLongOperationAndDo:^{
    STAssert
}];

การทดสอบต้องรอให้การดำเนินการเสร็จสิ้น โซลูชันปัจจุบันของฉันมีลักษณะดังนี้:

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

อันไหนที่ดูค่อนข้างหยาบคุณรู้วิธีที่ดีกว่านี้ไหม? ฉันสามารถเปิดเผยคิวและบล็อกโดยโทรdispatch_sync:

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

... objectแต่ที่อาจจะเปิดเผยมากเกินไปบน

คำตอบ:


302

dispatch_semaphoreพยายามที่จะใช้ ควรมีลักษณะดังนี้:

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:จะตัดสินใจว่าการดำเนินการนั้นไม่นานพอที่จะทำเกลียวและวิ่งพร้อมกันแทน


61
รหัสนี้ใช้ไม่ได้สำหรับฉัน STAssert ของฉันจะไม่มีวันดำเนินการ ฉันต้องแทนที่dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);ด้วยwhile (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; }
nicktmro

41
อาจเป็นเพราะบล็อกการเสร็จสิ้นของคุณถูกส่งไปยังคิวหลักหรือไม่ คิวถูกบล็อครอเซมาฟอร์จึงไม่ดำเนินการบล็อก ดูคำถามนี้เกี่ยวกับการจัดส่งในคิวหลักโดยไม่ปิดกั้น
zoul

3
ฉันทำตามคำแนะนำของ @Zoul & nicktmro แต่มันก็ดูเหมือนว่ามันจะหยุดชะงัก กรณีทดสอบ '- [BlockTestTest testAsync]' เริ่มต้นแล้ว แต่ไม่สิ้นสุด
NSCry

3
คุณต้องการที่จะปล่อยสัญญาณภายใน ARC หรือไม่?
Peter Warbo

14
นี่คือสิ่งที่ฉันกำลังมองหา ขอบคุณ! @ PeterWarbo ไม่คุณทำไม่ได้ การใช้ ARC ทำให้ไม่จำเป็นต้องใช้ dispatch_release ()
Hulvej

29

นอกจากนี้ยังมีเทคนิคสัญญาณครอบคลุมอย่างละเอียดถี่ถ้วนในคำตอบอื่น ๆ ตอนนี้เราสามารถใช้ XCTest ใน Xcode 6 XCTestExpectationทำการทดสอบผ่านไม่ตรงกัน สิ่งนี้ช่วยลดความต้องการเซมาฟอร์เมื่อทดสอบโค้ดอะซิงโครนัส ตัวอย่างเช่น:

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

เพื่อประโยชน์ของผู้อ่านในอนาคตในขณะที่เทคนิคสัญญาณส่งสัญญาณเป็นเทคนิคที่ยอดเยี่ยมเมื่อจำเป็นอย่างยิ่งฉันต้องยอมรับว่าฉันเห็นนักพัฒนาใหม่มากเกินไปไม่คุ้นเคยกับรูปแบบการเขียนโปรแกรมแบบอะซิงโครนัสที่ดี กิจวัตรประจำวันทำงานพร้อมกัน ยิ่งฉันเห็นหลายคนใช้เทคนิคสัญญาณนี้จากคิวหลัก (และเราไม่ควรบล็อกคิวหลักในแอพการผลิต)

ฉันรู้ว่านี่ไม่ใช่กรณีที่นี่ (เมื่อโพสต์คำถามนี้ไม่มีเครื่องมือที่ดีเช่นXCTestExpectationในชุดทดสอบเหล่านี้เราต้องแน่ใจว่าการทดสอบไม่เสร็จจนกว่าจะมีการโทรแบบอะซิงโครนัส) นี่เป็นหนึ่งในสถานการณ์ที่หายากเหล่านั้นซึ่งอาจมีเทคนิคเซมาฟอร์สำหรับบล็อกเธรดหลักอาจจำเป็น

ดังนั้นด้วยคำขอโทษของฉันถึงผู้เขียนคำถามต้นฉบับนี้ซึ่งเทคนิคสัญญาณเป็นเสียงฉันเขียนคำเตือนนี้ให้กับนักพัฒนาใหม่ทุกคนที่เห็นเทคนิคสัญญาณนี้และพิจารณาใช้ในรหัสของพวกเขาเป็นแนวทางทั่วไปในการจัดการกับอะซิงโครนัส วิธีการ: ได้รับการเตือนล่วงหน้าว่าเก้าครั้งจากสิบเทคนิคสัญญาณไม่ได้วิธีที่ดีที่สุดเมื่อพบการดำเนินการแบบอะซิงโครนัส ให้ทำความคุ้นเคยกับรูปแบบบล็อก / การปิดที่สมบูรณ์รวมถึงรูปแบบของผู้รับมอบสิทธิ์และการแจ้งเตือน สิ่งเหล่านี้มักจะเป็นวิธีที่ดีกว่าในการจัดการกับภารกิจแบบอะซิงโครนัสมากกว่าการใช้เซมาฟอร์ โดยทั่วไปมีเหตุผลที่ดีที่งานแบบอะซิงโครนัสถูกออกแบบมาให้ทำงานแบบอะซิงโครนัสดังนั้นให้ใช้รูปแบบอะซิงโครนัสที่ถูกต้องแทนที่จะพยายามทำแบบอะซิงโครนัส


1
ฉันคิดว่านี่ควรเป็นคำตอบที่ได้รับการยอมรับในขณะนี้ นี่คือเอกสารเช่นกัน: developer.apple.com/library/prerelease/ios/documentation/
......

ฉันมีคำถามเกี่ยวกับเรื่องนี้ ฉันมีโค้ดแบบอะซิงโครนัสที่ทำงานเกี่ยวกับการดาวน์โหลดการเรียก AFNetwork โหลเพื่อดาวน์โหลดเอกสารเดียว NSOperationQueueฉันต้องการที่จะกำหนดเวลาในการดาวน์โหลด ถ้าฉันใช้บางสิ่งบางอย่างเช่นเซมาฟอร์การดาวน์โหลดเอกสารNSOperationจะปรากฏทันทีเพื่อให้เสร็จสมบูรณ์และจะไม่มีการจัดคิวการดาวน์โหลดที่แท้จริง - พวกเขาจะดำเนินการต่อไปพร้อม ๆ กันซึ่งฉันไม่ต้องการ คำอุปมานี้สมเหตุสมผลหรือไม่ หรือมีวิธีที่ดีกว่าในการทำให้ NSOperations รอการสิ้นสุดแบบอะซิงโครนัสของผู้อื่นหรือไม่ หรืออย่างอื่น?
Benjohn

ไม่อย่าใช้เซมาฟอร์ในสถานการณ์นี้ หากคุณมีคิวการทำงานที่คุณเพิ่มAFHTTPRequestOperationวัตถุคุณควรสร้างการดำเนินการให้เสร็จสมบูรณ์ (ซึ่งคุณต้องพึ่งพาการดำเนินการอื่น) หรือใช้กลุ่มการจัดส่ง BTW คุณบอกว่าคุณไม่ต้องการให้พวกเขาทำงานพร้อมกันซึ่งก็ดีถ้านั่นคือสิ่งที่คุณต้องการ แต่คุณจ่ายค่าปรับประสิทธิภาพอย่างจริงจังโดยทำสิ่งนี้ตามลำดับแทนที่จะทำพร้อมกัน ฉันมักจะใช้maxConcurrentOperationCount4 หรือ 5
Rob

28

ฉันเพิ่งมาที่ปัญหานี้อีกครั้งและเขียนหมวดหมู่ต่อไปนี้ใน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

วิธีนี้ฉันสามารถเปลี่ยนการโทรแบบอะซิงโครนัสได้อย่างง่ายดายด้วยการโทรกลับเป็นซิงโครนัสในการทดสอบ:

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

24

โดยทั่วไปไม่ใช้คำตอบใด ๆ เหล่านี้ แต่มักจะไม่ขยายขนาด (มีข้อยกเว้นตรงนี้และแน่นอน)

วิธีการเหล่านี้เข้ากันไม่ได้กับวิธีที่ GCD ตั้งใจทำงานและจะทำให้เกิดการหยุดชะงักและ / หรือฆ่าแบตเตอรี่โดยการหยุดการเลือกตั้ง

กล่าวอีกนัยหนึ่งให้จัดเรียงโค้ดของคุณใหม่เพื่อไม่ให้เกิดการซิงโครนัส แต่ให้จัดการกับผลลัพธ์ที่ได้รับแจ้งการเปลี่ยนแปลงสถานะ (เช่นโพรโทคอลโทรกลับ / ผู้แทนที่พร้อมใช้งานกำลังจะออกไป (สิ่งเหล่านี้สามารถนำกลับมาใช้ใหม่เป็นบล็อกได้หากคุณไม่ชอบนรกการติดต่อกลับ) เพราะนี่คือวิธีเปิดเผยพฤติกรรมที่แท้จริงให้กับส่วนที่เหลือของแอปกว่าซ่อนไว้ด้านหลังซุ้มด้านหน้าที่ผิด

ให้ใช้NSNotificationCenterแทนกำหนดโปรโตคอลผู้ร่วมประชุมที่กำหนดเองด้วยการเรียกกลับสำหรับคลาสของคุณ และถ้าคุณไม่ชอบการแม็คกับการเรียกกลับแบบมอบหมายทั้งหมดให้ตัดมันลงในคลาสพร็อกซีที่เป็นรูปธรรมซึ่งใช้โปรโตคอลที่กำหนดเองและบันทึกบล็อกต่างๆ อาจเป็นตัวสร้างความสะดวกสบายเช่นกัน

งานเริ่มแรกนั้นมีมากขึ้นเล็กน้อย แต่มันจะช่วยลดจำนวนสภาพการแข่งขันที่น่ากลัวและการลงคะแนนเลือกตั้งสังหารแบตเตอรี่ในระยะยาว

(อย่าขอตัวอย่างเพราะมันไม่สำคัญและเราต้องใช้เวลาในการเรียนรู้พื้นฐานของวัตถุประสงค์ด้วยเช่นกัน)


1
มันเป็นคำเตือนที่สำคัญเพราะรูปแบบการออกแบบ obj-C และความสามารถในการทดสอบเช่นกัน
BootMaker

8

นี่คือเคล็ดลับที่ดีที่ไม่ได้ใช้สัญญาณ:

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

สิ่งที่คุณทำคือรอใช้dispatch_syncกับบล็อกว่างเปล่าเพื่อรอคิวการจัดส่งแบบอนุกรมจนกว่าบล็อก A-Synchronous จะเสร็จสมบูรณ์


ปัญหาเกี่ยวกับคำตอบนี้คือมันไม่ได้แก้ไขปัญหาดั้งเดิมของ OP ซึ่งก็คือ API ที่จำเป็นต้องใช้จะทำให้แฮนเดิลเป็นอาร์กิวเมนต์และส่งคืนทันที การเรียก API นั้นภายในบล็อค async ของคำตอบนี้จะกลับมาทันทีแม้ว่าความสมบูรณ์ Handler ยังไม่ได้ทำงาน จากนั้นบล็อกการซิงค์จะถูกเรียกใช้งานก่อนที่จะเสร็จสมบูรณ์ Handler
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);
}

ตัวอย่างการใช้งาน:

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

2

นอกจากนี้ยังมีSenTestingKitAsyncที่ให้คุณเขียนโค้ดดังนี้:

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

(ดูบทความ objc.ioสำหรับรายละเอียด) และเนื่องจาก Xcode 6 มีAsynchronousTestingหมวดหมู่XCTestที่ให้คุณเขียนโค้ดดังนี้:

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

1

นี่คือทางเลือกจากหนึ่งในการทดสอบของฉัน:

__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
มีข้อผิดพลาดในรหัสด้านบน จากNSCondition เอกสารสำหรับ-waitUntilDate:"คุณต้องล็อคตัวรับสัญญาณก่อนที่จะเรียกวิธีนี้" ดังนั้นควรจะเป็นหลังจากที่-unlock -waitUntilDate:
Patrick

สิ่งนี้ไม่ได้ปรับขนาดเป็นสิ่งใดก็ตามที่ใช้หลายเธรดหรือรันคิว

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

มันทำเพื่อฉัน


3
ดีมันทำให้การใช้งานซีพียูสูง แต่
วิน

4
@ kevin Yup นี่คือการสำรวจสลัมที่จะสังหารแบตเตอรี่

@ Barry วิธีการใช้งานแบตเตอรี่มากขึ้น กรุณาแนะนำ
pkc456

@ pkc456 ลองดูในหนังสือวิทยาศาสตร์คอมพิวเตอร์เกี่ยวกับความแตกต่างระหว่างวิธีการสำรวจและการแจ้งเตือนแบบอะซิงโครนัส โชคดี.

2
สี่ปีครึ่งต่อมาและด้วยความรู้และประสบการณ์ที่ฉันได้รับฉันจะไม่แนะนำคำตอบของฉัน

0

บางครั้งการหมดเวลาของลูปก็มีประโยชน์เช่นกัน คุณอาจรอจนกว่าคุณจะได้รับสัญญาณ (อาจเป็นบูล) จากวิธีการโทรกลับ async แต่จะเกิดอะไรขึ้นถ้าไม่มีการตอบสนองและคุณต้องการแยกวงออกจากกัน นี่คือวิธีการแก้ปัญหาส่วนใหญ่ตอบข้างต้น แต่ด้วยการเพิ่มการหมดเวลา

#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
ปัญหาเดียวกัน: อายุการใช้งานแบตเตอรี่ล้มเหลว

1
@ Barry ไม่แน่ใจแม้ว่าคุณจะดูรหัส มีระยะเวลา TIMEOUT_SECONDS ซึ่งหากการโทรแบบ async ไม่ตอบสนองจะเป็นการแบ่งลูป นั่นคือแฮ็คที่จะทำลายการหยุดชะงัก รหัสนี้ทำงานได้อย่างสมบูรณ์แบบโดยไม่ทำลายแบตเตอรี่
Khulja Sim Sim

0

วิธีการแก้ปัญหาดั้งเดิมมาก:

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

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

0

สวิฟท์ 4:

ใช้synchronousRemoteObjectProxyWithErrorHandlerแทนremoteObjectProxyเมื่อสร้างวัตถุระยะไกล ไม่ต้องการเซมาฟอร์อีกต่อไป

ตัวอย่างด้านล่างจะส่งคืนเวอร์ชันที่ได้รับจากพร็อกซี หากไม่มีsynchronousRemoteObjectProxyWithErrorHandlerจะทำให้เกิดปัญหา (พยายามเข้าถึงหน่วยความจำที่ไม่สามารถเข้าถึงได้):

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

ฉันต้องรอจนกว่าจะโหลด UIWebView ก่อนที่จะใช้วิธีการของฉันฉันสามารถใช้งานได้โดยทำการตรวจสอบ UIWebView ที่พร้อมใช้ในการตรวจสอบเธรดหลักโดยใช้ GCD ร่วมกับวิธีสัญญาณที่กล่าวถึงในหัวข้อนี้ รหัสสุดท้ายดูเหมือนว่านี้:

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

}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.