Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Synchronize Asynchronous tasks in Objective-C

I'm trying to send some images file (almost 100MB) to my iDevice clients using GCDAsyncSocket.

I want to Synchronously send packets to the clients. I mean after sending 100MB of data to first client iterating to the next client.but because of Asynchronous nature of GCDAsyncSocket I don't know how can I serialize these packet sending.

I can't use semaphore because before sending images I negotiate with each client to know what images I should send then try to send those images. and I can't find a neat way to wait and signal the semaphore.

- (void)sendImagesToTheClients:clients
{
    ...
    //negotiating with client to know which images should sent
    ...

    for(Client* client in clients)
    {
       packet = [packet addImages: images];
       [self sendPacket:packet toClient:client];
    }
}

- (void)sendPacket:packet toClient:client
{
 // Initialize Buffer
    NSMutableData *buffer = [[NSMutableData alloc] init];
    NSData *bufferData = [NSKeyedArchiver archivedDataWithRootObject:packet];

    uint64_t headerLength = [bufferData length];
    [buffer appendBytes:&headerLength length:sizeof(uint64_t)];
    [buffer appendBytes:[bufferData bytes] length:[bufferData length]];

    // Write Buffer
    [client.socket writeData:buffer withTimeout:-1.0 tag:0];
}

this is how AsyncSocket writing data works:

- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag
{
    if ([data length] == 0) return;

    GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag];

    dispatch_async(socketQueue, ^{ @autoreleasepool {

        LogTrace();

        if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
        {
            [writeQueue addObject:packet];
            [self maybeDequeueWrite];
        }
    }});

    // Do not rely on the block being run in order to release the packet,
    // as the queue might get released without the block completing.
}

so how can I synchronize this task?

UPDATE

for socket connection I use GCDAsyncSocket which heavily uses delegation for event notification.(GCDAsyncSocket.h and GCDAsyncSocket.m) (no method with completionHandler).

I have written a class named TCPClient which handles socket connection and packet sending and set it as the delegate of initialized socket. after writing a packet, the delegate method - (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag gets called. which only informs me some data has been written. here I can't decide based of written data to call dispatch_group_leave. so relying delegate method is useless.

I have modified [client.socket writeData:buffer withTimeout:-1.0 tag:0] in GCDAsyncSocket.h and .m files to accept a completionBlock: [client.socket writeData:buffer withTimeout:-1.0 tag:0 completionBlock:completionBlock] using this approach helps me to solve synchronizing async tasks.

// perform your async task
dispatch_async(self.socketQueue, ^{
    [self sendPacket:packet toClient:client withCompletion:^(BOOL finished, NSError *error)
     {
         if (finished) {
             NSLog(@"images file sending finished");
             //leave the group when you're done
             dispatch_group_leave(group);
         }
         else if (!finished && error)
         {
             NSLog(@"images file sending FAILED");
         }
     }];

but the problem is after updating GCDAsyncsocket, my code may break. here I'm looking for neat way to add completion handler to GCDAsyncsocket without modifying it directly. like creating a wrapper around it or using features of objective-c runtime. do you have any idea?

like image 278
Hashem Aboonajmi Avatar asked Mar 18 '23 01:03

Hashem Aboonajmi


1 Answers

You can accomplish this with dispatch groups. For a async task with a completion block:

//create & enter the group
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);

// perform your async task
dispatch_async(socketQueue, ^{

    //leave the group when you're done
    dispatch_group_leave(group);
});

// wait for the group to complete 
// (you can use DISPATCH_TIME_FOREVER to wait forever)
long status = dispatch_group_wait(group,
                    dispatch_time(DISPATCH_TIME_NOW,NSEC_PER_SEC * COMMAND_TIMEOUT));

// check to see if it timed out, or completed
if (status != 0) {
    // timed out
}

Alternatively for a task with a delegate:

@property (nonatomic) dispatch_group_t group;

-(BOOL)doWorkSynchronously {

    self.group = dispatch_group_create();
    dispatch_group_enter(self.group);

    [object doAsyncWorkWithDelegate:self];

    long status = dispatch_group_wait(self.group,
                        dispatch_time(DISPATCH_TIME_NOW,NSEC_PER_SEC * COMMAND_TIMEOUT));

    // A
    if (status != 0) {
        // timed out
        return NO
    }

    return YES;
}

-(void)asyncWorkCompleted {}

    // after this call, control should jump back to point A in the doWorkSynchronously method
    dispatch_group_leave(self.group);
}
like image 109
thelaws Avatar answered Apr 26 '23 16:04

thelaws