Làm cách nào để đợi khối được gửi không đồng bộ kết thúc?


180

Tôi đang thử nghiệm một số mã thực hiện xử lý không đồng bộ bằng cách sử dụng Grand Central Dispatch. Mã kiểm tra trông như thế này:

[object runSomeLongOperationAndDo:^{
    STAssert
}];

Các bài kiểm tra phải chờ cho hoạt động kết thúc. Giải pháp hiện tại của tôi trông như thế này:

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

Có vẻ hơi thô, bạn có biết một cách tốt hơn? Tôi có thể hiển thị hàng đợi và sau đó chặn bằng cách gọi dispatch_sync:

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

Sôi nhưng điều đó có thể phơi bày quá nhiều trên object.

Câu trả lời:


302

Đang cố gắng sử dụng a dispatch_semaphore. Nó sẽ trông giống như thế này:

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

Điều này sẽ hành xử chính xác ngay cả khi runSomeLongOperationAndDo:quyết định rằng hoạt động không thực sự đủ dài để tạo luồng và thay vào đó chạy đồng bộ.


61
Mã này đã không làm việc cho tôi. STAssert của tôi sẽ không bao giờ thực thi. Tôi đã phải thay thế dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);bằngwhile (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; }
nicktmro

41
Điều đó có thể là do khối hoàn thành của bạn được gửi đến hàng đợi chính? Hàng đợi bị chặn chờ semaphore và do đó không bao giờ thực thi khối. Xem câu hỏi này về việc gửi trên hàng đợi chính mà không chặn.
zoul

3
Tôi đã làm theo gợi ý của @Zoul & nicktmro. Nhưng có vẻ như nó sẽ rơi vào tình trạng bế tắc. Test Case '- [BlockTestTest testAsync]' đã bắt đầu. nhưng không bao giờ kết thúc
NSCry

3
Bạn có cần phát hành semaphore dưới ARC không?
Peter Warbo

14
đây chính xác là những gì tôi đang tìm kiếm. Cảm ơn! @PeterWarbo không bạn không. Việc sử dụng ARC sẽ loại bỏ sự cần thiết phải thực hiện một
điều_release

29

Ngoài kỹ thuật semaphore được đề cập đầy đủ trong các câu trả lời khác, giờ đây chúng ta có thể sử dụng XCTest trong Xcode 6 để thực hiện các kiểm tra không đồng bộ thông qua XCTestExpectation. Điều này giúp loại bỏ sự cần thiết của semaphores khi kiểm tra mã không đồng bộ. Ví dụ:

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

Vì lợi ích của các độc giả trong tương lai, trong khi kỹ thuật semaphore là một kỹ thuật tuyệt vời khi thực sự cần thiết, tôi phải thú nhận rằng tôi thấy quá nhiều nhà phát triển mới, không quen thuộc với các mẫu lập trình không đồng bộ tốt, hấp dẫn quá nhanh đối với semaphores như một cơ chế chung để tạo ra sự không đồng bộ thói quen cư xử đồng bộ. Tệ hơn nữa là tôi đã thấy nhiều người trong số họ sử dụng kỹ thuật semaphore này từ hàng đợi chính (và chúng ta không bao giờ nên chặn hàng đợi chính trong các ứng dụng sản xuất).

Tôi biết đây không phải là trường hợp ở đây (khi câu hỏi này được đăng, không có một công cụ hay như thế XCTestExpectation, ngoài ra, trong các bộ thử nghiệm này, chúng tôi phải đảm bảo thử nghiệm không kết thúc cho đến khi cuộc gọi không đồng bộ được thực hiện). Đây là một trong những tình huống hiếm hoi trong đó kỹ thuật semaphore để chặn luồng chính có thể là cần thiết.

Vì vậy, với lời xin lỗi của tôi cho tác giả của câu hỏi ban đầu này, người mà kỹ thuật semaphore là âm thanh, tôi viết lời cảnh báo này cho tất cả những nhà phát triển mới xem kỹ thuật semaphore này và xem xét áp dụng nó trong mã của họ như một cách tiếp cận chung để xử lý sự không đồng bộ phương pháp: Được cảnh báo rằng chín lần trong số mười, kỹ thuật semaphore khôngcách tiếp cận tốt nhất khi gặp các hoạt động không đồng bộ. Thay vào đó, hãy tự làm quen với các mẫu khối / đóng hoàn thành, cũng như các mẫu và thông báo giao thức ủy nhiệm. Đây thường là những cách tốt hơn để xử lý các tác vụ không đồng bộ, thay vì sử dụng các từ ngữ để làm cho chúng hoạt động đồng bộ. Thông thường có những lý do chính đáng để các tác vụ không đồng bộ được thiết kế để hoạt động không đồng bộ, vì vậy hãy sử dụng mẫu không đồng bộ đúng thay vì cố gắng làm cho chúng hoạt động đồng bộ.


1
Tôi nghĩ rằng đây nên là câu trả lời được chấp nhận ngay bây giờ. Dưới đây cũng là các tài liệu: developer.apple.com/l
Library / prelelease / ios / document / /

Tôi có một câu hỏi về điều này. Tôi đã nhận được một số mã không đồng bộ thực hiện khoảng một chục cuộc gọi tải xuống AFNetworking để tải xuống một tài liệu. Tôi muốn lên lịch tải xuống trên một NSOperationQueue. Trừ khi tôi sử dụng một cái gì đó giống như một semaphore, NSOperationtất cả các phần tải xuống tài liệu sẽ ngay lập tức hoàn tất và sẽ không có bất kỳ hàng đợi tải xuống thực sự nào - chúng sẽ tiến hành đồng thời, điều mà tôi không muốn. Là semaphores hợp lý ở đây? Hoặc có cách nào tốt hơn để khiến NSOperations chờ kết thúc không đồng bộ của người khác không? Hay cái gì khác?
Stewohn

Không, đừng sử dụng semaphores trong tình huống này. Nếu bạn có hàng đợi hoạt động mà bạn đang thêm các AFHTTPRequestOperationđối tượng, thì bạn chỉ nên tạo một hoạt động hoàn thành (mà bạn sẽ phụ thuộc vào các hoạt động khác). Hoặc sử dụng các nhóm công văn. BTW, bạn nói rằng bạn không muốn chúng chạy đồng thời, sẽ tốt nếu đó là những gì bạn cần, nhưng bạn phải trả tiền phạt hiệu suất nghiêm trọng khi thực hiện việc này một cách tuần tự thay vì đồng thời. Tôi thường sử dụng maxConcurrentOperationCount4 hoặc 5.
Rob

28

Gần đây tôi đã đến vấn đề này một lần nữa và đã viết thể loại sau đây về 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

Bằng cách này, tôi có thể dễ dàng biến cuộc gọi không đồng bộ với cuộc gọi lại thành cuộc gọi đồng bộ trong các thử nghiệm:

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

24

Nói chung, không sử dụng bất kỳ câu trả lời nào trong số này, chúng thường không có tỷ lệ (chắc chắn có ngoại lệ ở đây và ở đó)

Các phương pháp này không tương thích với cách GCD dự định hoạt động và cuối cùng sẽ gây ra bế tắc và / hoặc giết chết pin bằng cách bỏ phiếu không ngừng.

Nói cách khác, sắp xếp lại mã của bạn để không có kết quả chờ đồng bộ, mà thay vào đó là xử lý kết quả được thông báo về việc thay đổi trạng thái (ví dụ: các cuộc gọi lại / giao thức ủy nhiệm, có sẵn, biến mất, lỗi, v.v.). (Chúng có thể được tái cấu trúc thành các khối nếu bạn không thích gọi lại địa ngục.) Bởi vì đây là cách phơi bày hành vi thực sự với phần còn lại của ứng dụng hơn là ẩn nó đằng sau mặt tiền giả.

Thay vào đó, hãy sử dụng NSNotificationCenter , xác định giao thức ủy nhiệm tùy chỉnh với các cuộc gọi lại cho lớp của bạn. Và nếu bạn không thích mucking với các cuộc gọi lại đại biểu khắp nơi, hãy bọc chúng vào một lớp proxy cụ thể thực hiện giao thức tùy chỉnh và lưu các khối khác nhau trong các thuộc tính. Có lẽ cũng cung cấp các nhà xây dựng thuận tiện là tốt.

Công việc ban đầu hơi nhiều nhưng nó sẽ làm giảm số lượng các điều kiện chủng tộc khủng khiếp và bỏ phiếu giết người pin trong thời gian dài.

(Đừng hỏi một ví dụ, vì nó tầm thường và chúng tôi cũng phải đầu tư thời gian để tìm hiểu những điều cơ bản khách quan.)


1
Đây cũng là một cảnh báo quan trọng vì các mẫu thiết kế obj-C và khả năng kiểm tra
BootMaker

8

Đây là một mẹo tiện lợi không sử dụng semaphore:

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

Những gì bạn làm là chờ sử dụng dispatch_syncvới một khối trống để chờ Đồng bộ trên hàng đợi gửi nối tiếp cho đến khi khối A-Đồng bộ hoàn thành.


Vấn đề với câu trả lời này là nó không giải quyết được vấn đề ban đầu của OP, đó là API cần được sử dụng sẽ lấy xongHandler làm đối số và trả về ngay lập tức. Gọi API đó trong khối không đồng bộ của câu trả lời này sẽ trả về ngay lập tức mặc dù hoàn thànhHandler chưa chạy. Sau đó, khối đồng bộ hóa sẽ được thực thi trước khi hoàn thành.
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);
}

Ví dụ sử dụng:

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

2

Ngoài ra còn có SenTestingKitAsync cho phép bạn viết mã như thế này:

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

(Xem bài viết objc.io để biết chi tiết.) Và vì Xcode 6 có một AsynchronousTestingdanh mục XCTestcho phép bạn viết mã như thế này:

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

1

Đây là một thay thế từ một trong những thử nghiệm của tôi:

__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
Có một lỗi trong mã trên. Từ NSCondition tài liệu cho -waitUntilDate:"Bạn phải khóa người nhận trước khi gọi phương thức này." Vì vậy, -unlocknên được sau -waitUntilDate:.
Patrick

Điều này không mở rộng cho bất cứ điều gì sử dụng nhiều luồng hoặc chạy hàng đợi.

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

Điều này đã làm điều đó cho tôi.


3
tốt, nó gây ra việc sử dụng cpu cao mặc dù
kevin

4
@kevin Yup, đây là cuộc bỏ phiếu ghetto sẽ giết chết pin.

@Barry, làm thế nào để nó tiêu thụ nhiều pin hơn. xin hướng dẫn.
pkc456

@ pkc456 Hãy xem một cuốn sách khoa học máy tính về sự khác biệt giữa cách thức bỏ phiếu và thông báo không đồng bộ hoạt động. Chúc may mắn.

2
Bốn năm rưỡi sau, với kiến ​​thức và kinh nghiệm tôi đã đạt được, tôi sẽ không đề xuất câu trả lời của mình.

0

Đôi khi, các vòng lặp Timeout cũng hữu ích. Bạn có thể đợi cho đến khi bạn nhận được một số tín hiệu (có thể là BOOL) từ phương thức gọi lại không đồng bộ, nhưng nếu không có phản hồi bao giờ và bạn muốn thoát ra khỏi vòng lặp đó thì sao? Dưới đây là giải pháp, chủ yếu được trả lời ở trên, nhưng có thêm Thời gian chờ.

#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
Vấn đề tương tự: tuổi thọ pin không thành công.

1
@Barry Không chắc chắn ngay cả khi bạn nhìn vào mã. Có khoảng thời gian TIMEOUT_SECONDS trong đó nếu cuộc gọi không đồng bộ không phản hồi, nó sẽ phá vỡ vòng lặp. Đó là hack để phá vỡ bế tắc. Mã này hoạt động hoàn hảo mà không làm chết pin.
Khulja Sim Sim

0

Giải pháp rất nguyên thủy cho vấn đề:

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

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

0

Swift 4:

Sử dụng synchronousRemoteObjectProxyWithErrorHandlerthay vì remoteObjectProxykhi tạo đối tượng từ xa. Không cần thêm cho một semaphore.

Ví dụ dưới đây sẽ trả về phiên bản nhận được từ proxy. Nếu không có synchronousRemoteObjectProxyWithErrorHandlernó sẽ bị sập (cố gắng truy cập bộ nhớ không thể truy cập):

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

Tôi phải đợi cho đến khi UIWebView được tải trước khi chạy phương thức của mình, tôi đã có thể làm việc này bằng cách thực hiện kiểm tra sẵn sàng UIWebView trên luồng chính bằng GCD kết hợp với các phương thức semaphore được đề cập trong luồng này. Mã cuối cùng trông như thế này:

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

}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.