iOS knowledge combing asynchronous programming - coobjc learning

Asynchronous programming is a hot topic in recent years.

Today, we will talk about asynchronous programming and coobjc in iOS platform.

First of all, to answer the question, why do we need asynchronous programming?

In the early years, people were used to running a thread to perform time-consuming tasks, even if the time-consuming task was not CPU intensive, such as a synchronous IO or network call. But today, there is no doubt that asynchronous rather than subthreading should be used in this scenario. The cost of opening thread itself is relatively large, and multi-threaded programming is always locked, which is easy to cause crash or more serious performance problems. In iOS platform, many system APIs are unscientific synchronous and time-consuming calls, and GCD API is a very good thread encapsulation, which leads to various problems caused by misuse of multithreading in iOS platform.

All in all, in principle, many CPU free time-consuming operations such as network and IO should be solved by using asynchronous first.

Then look at the asynchronous programming scheme. The commonly used methods in iOS platform are delegate and block callback. Delegate leads to logical fragmentation, and the usage scenario focuses on the UI layer, which is not good for most asynchronous scenarios.

The block callback syntax also has some flaws. The biggest problem is going back to hell:

[NSURLConnection sendAsynchronousRequest:rq queue:nil completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
        if (connectionError) {
            if (callback) {
                callback(nil, nil,connectionError);
            }
        }
        else{
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
                NSString *imageUrl = dict[@"image"];
                [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:imageUrl]] queue:nil completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
                    dispatch_async(dispatch_get_global_queue(0, 0), ^{
                        if (connectionError) {
                            callback(nil, dict,connectionError);
                        }
                        else{
                            UIImage *image = [[UIImage alloc] initWithData:data];
                            if (callback) {
                                (image, dict, nil);
                            }
                        }
                    });
                }];
            });
            
        }
    }]

But I don't seem to feel very painful in iOS development, at least not as painful as the front-end. Maybe it's because the deep callback in our actual development will be replaced by the delegate or notificaiton mechanism. But this kind of hybrid use is a challenge to code quality. To ensure code quality, the team needs to make a lot of constraints and have high execution, which is actually very difficult.

Another solution is reactive programming. ReactiveCocoa and RxSwift are both practitioners of this idea. The responsive concept is very advanced, but there are problems in debugging, and the learning cost is relatively high. It is not easy for the whole team to use it well.

In recent years, the most concerned asynchronous model is the async/await scheme. The birth of go has made the concept of synergism popular (although goroutine is not strictly a synergism), and js's support for async/await has also attracted much attention.

swift has a proposal to add async/await syntax, but it's expected to wait another year or two.

However, this year, Alibaba has opened a coobjc library, which can provide the async/await capabilities for iOS, and has done a lot of work to make a perfect adaptation to the problems that may be encountered in iOS programming, which is worth learning.

Association plan

Let's leave coobjc out of the way and see what we can do to implement the collaboration.

protothreads

protothreads is the lightest Association Library. Its implementation relies on the skill of switch/case syntax, and then uses a bunch of macros to encapsulate it as a primitive supporting Association. It has realized the general cooperation ability.

For specific implementation, please refer to this article A "fly level" C language library.

However, this scheme can't keep the call stack. From the current perspective, it's not a complete process.

Implementation based on setjmp/longjmp

It's a bit like goto, but it can save context, but the context here is just the content of register, not the complete stack.

Reference resources Talk about setjmp and longjmp

ucontext

Reference resources ucontext - a simple co program library that everyone can implement

ucontext provides a relatively complete ability to save context including stack. ucontext can encapsulate a complete process library. Reference resources coroutine

But ucontext is not supported on iOS:

int  getcontext(ucontext_t *) __OSX_AVAILABLE_BUT_DEPRECATED(__MAC_10_5, __MAC_10_6, __IPHONE_2_0, __IPHONE_2_0) __WATCHOS_PROHIBITED __TVOS_PROHIBITED;
void makecontext(ucontext_t *, void (*)(), int, ...) __OSX_AVAILABLE_BUT_DEPRECATED(__MAC_10_5, __MAC_10_6, __IPHONE_2_0, __IPHONE_2_0) __WATCHOS_PROHIBITED __TVOS_PROHIBITED;
int  setcontext(const ucontext_t *) __OSX_AVAILABLE_BUT_DEPRECATED(__MAC_10_5, __MAC_10_6, __IPHONE_2_0, __IPHONE_2_0) __WATCHOS_PROHIBITED __TVOS_PROHIBITED;
int  swapcontext(ucontext_t * __restrict, const ucontext_t * __restrict) __OSX_AVAILABLE_BUT_DEPRECATED(__MAC_10_5, __MAC_10_6, __IPHONE_2_0, __IPHONE_2_0) __WATCHOS_PROHIBITED __TVOS_PROHIBITED;

Compiler implementation

The compiler can do anything of course... Here is mainly a way to implement stack free co programming through the compiler.

Whether there is a separate stack can be divided into stack and no stack. Of course, the ability of stack cooperation is more perfect, but the non stack cooperation is lighter and should be slightly improved in performance and memory consumption.

However, when the language does not support co programming at the beginning, it is easy to create a stack co programming to step on various pits, such as the auto release mechanism.

Stack free routines are handled by the compiler, which is relatively simple, as long as a tag is generated at a specific location during compilation to jump.

Here is an example of LLVM's no stack co program code compilation

This kind of insertion and jump is actually similar to the aforementioned switch case implementation, but the implementation of the compiler is very flexible because of its defects.

Assembly implementation

Using assembly can save the state of each register and complete the ability of ucontext.

As for the call stack, in fact, the stack space can be opened manually when creating the cooperation program. Just point the stack register to the past.

What's more troublesome is that the mechanism of different platforms is different, and different assembly code needs to be written.

coobjc

Back to today's main character, coobjc, it uses the assembly scheme to implement the stack cooperation.

The principle part, Design Chapter stack switch of iOS co process coobjc It's very good. Reading is highly recommended.

Let's focus on its use.

async/await/channel

coobjc creates a cooperation process through the co_launch method, and uses await to wait for the asynchronous task to return. See an example:

- (void)viewDidLoad{
    ...
        co_launch(^{
            NSData *data = await(downloadDataFromUrl(url));
            UIImage *image = await(imageFromData(data));
            self.imageView.image = image;
        });
}

The above-mentioned code turns the code that originally required dispatch [async twice into sequential execution, which makes the code more concise

The parameter that await accepts is a Promise or Channel object. Let's first look at Promise:

// make a async operation
- (COPromise<id> *)co_fetchSomethingAsynchronous {

    return [COPromise promise:^(COPromiseResolve  _Nonnull resolve, COPromiseReject  _Nonnull reject) {
        dispatch_async(_someQueue, ^{
            id ret = nil;
            NSError *error = nil;
            // fetch result operations
            ...

            if (error) {
                reject(error);
            } else {
                resolve(ret);
            }
        });
    }];
}

// calling in a coroutine.
co_launch(^{

    id ret = await([self co_fetchSomethingAsynchronous]);

    NSError *error = co_getError();
    if (error) {
        // error
    } else {
        // ret
    }
});

Promise is the encapsulation of an asynchronous task. await will wait for the callback of promise's reject/resolve.

It should be noted that the await of coobjc is a little different from that of javascript/dart. For javascript, when calling asynchronous tasks, each layer should explicitly use await, otherwise, it will not block the external layer. Here's an example:

function timeout(ms) {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), ms);
    });
}
async function test() {
    const v = await timeout(100);
    console.log(v);
}
console.log('test start');
var result = test();
console.log(result);
console.log('test end');

Test function. When it is called outside, if there is no await, when it is encountered inside the test function, it will be directly executed outside. The test function returns a Promise object. The output order here is:

test start
Promise { <pending> }
test end
long_time_value

So is dart's async/await.

But coobjc is not. Its await is relatively simple. It will block the entire call stack. Take a look at the demo of coobjc:

- (void)coTest
{
    co_launch(^{
        NSLog(@"co start");
        id ret = [self test];

        NSError *error = co_getError();
        if (error) {
            // error
        } else {
            // ret
        }
        NSLog(@"co end");
    });
    NSLog(@"co out");
}
- (id)test {
    NSLog(@"test before");
    id ret = await([self co_fetchSomethingAsynchronous]);
    NSLog(@"test after");
    return ret;
}
- (COPromise<id> *)co_fetchSomethingAsynchronous {

    return [COPromise promise:^(COPromiseFulfill  _Nonnull resolve, COPromiseReject  _Nonnull reject) {
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"co run");
            id ret = @"test";
            NSError *error = nil;
            // fetch result operations

            if (error) {
                reject(error);
            } else {
                resolve(ret);
            }
        });
    }];
}

In the coTest method, the [self test] is called directly, which is executed in sequence. The log output sequence is as follows

2019-11-05 11:19:39.456798+0800 JFDemos[57239:5352156] co out
2019-11-05 11:19:39.660899+0800 JFDemos[57239:5352156] co start
2019-11-05 11:19:39.660994+0800 JFDemos[57239:5352156] test before
2019-11-05 11:19:39.662987+0800 JFDemos[57239:5352156] co run
2019-11-05 11:19:39.663110+0800 JFDemos[57239:5352156] test after
2019-11-05 11:19:39.663194+0800 JFDemos[57239:5352156] co end

The former is more flexible, but the latter is more intuitive. The main reason is that if you have used async/await in other languages, you need to pay attention to the differences here.

Channel

Channel is the channel that transfers data between the processes. The characteristic of channel is that it can send or receive data in blocking mode.

Look at an example.

COChan *chan = [COChan chanWithBuffCount:0];
co_launch(^{
    NSLog(@"1");
    [chan send:@111];
    NSLog(@"4");
});


co_launch(^{
    NSLog(@"2");
    id value = [chan receive_nonblock];
    NSLog(@"3");
});

Here, when initializing chan, the bufferCount is set to 0, so it will block when send ing. If the cache space is not 0, it will not block before it is full. Here the output order is 1234.

Generator

Generator is not a basic feature, but a programming paradigm, which is often implemented based on the cooperation process. In short, the generator is a lazy calculation sequence, and each time a call such as next() is triggered outside, it will execute a section of logic next.

For example, use coobjc to lazy calculate Fibonacci sequence:

COCoroutine *fibonacci = co_sequence(^{
  yield(@(1));
  int cur = 1;
  int next = 1;
  while(co_isActive()){
    yield(@(next));
    int tmp = cur + next;
    cur = next;
    next = tmp;
  }
});
co_launch(^{
  for(int i = 0; i < 10; i++){
    val = [[fibonacci next] intValue];
  }
});

Generator is very suitable for use in some scenarios where queues or recursion are required. It can change the data that needs to be prepared all at once into on-demand preparation.

Actor

actor is a message based concurrent programming model. About the concurrent programming model and some problems of multithreading, before Briefly discussed , not here.

Actor can be understood as a container, with its own state and behavior. Each actor has a Mailbox, and actors communicate with each other through Mailbox to trigger actor's behavior.

Mail should be implemented as immutable object, so in essence, actors do not share memory, which avoids a lot of problems in multithreading programming.

Similarly, there is a CSP model that abstracts communication as a Channel. In the Actor model, each Actor has a Mailbox, and the Actor needs to know the other party to send. In the CSP model, the Channel is a Channel. Entities send messages to the Channel. Other entities can subscribe to the Channel. Entities can be anonymous and have lower coupling.

Although coobjc implements the Channel, it seems to prefer the Actor model. Coobjc encapsulates the Actor model for us. The simple use is as follows:

COActor *countActor = co_actor_onqueue(get_test_queue(), ^(COActorChan *channel) {
    int count = 0;
    for(COActorMessage *message in channel){
        if([[message stringType] isEqualToString:@"inc"]){
            count++;
        }
        else if([[message stringType] isEqualToString:@"get"]){
            message.complete(@(count));
        }
    }
});
co_launch(^{
    [countActor sendMessage:@"inc"];
    [countActor sendMessage:@"inc"];
    [countActor sendMessage:@"inc"];
    int currentCount = [await([countActor sendMessage:@"get"]) intValue];
    NSLog(@"count: %d", currentCount);
});
co_launch_onqueue(dispatch_queue_create("counter queue1", NULL), ^{
    [countActor sendMessage:@"inc"];
    [countActor sendMessage:@"inc"];
    [countActor sendMessage:@"inc"];
    [countActor sendMessage:@"inc"];
    int currentCount = [await([countActor sendMessage:@"get"]) intValue];
    NSLog(@"count: %d", currentCount);
});

As you can see, the conflict between multiple threads is avoided here. In many scenarios, it is better than multithreading model, and it is also the development trend in recent years.

Summary

coobjc provides co process capability for objc and swift, as well as some convenient methods and programming paradigms based on CO process. However, compared with other native languages such as Javascript/dart/go, the syntax added by this hack method is not particularly friendly after all.

At present, the decline of objc and the rise of swift are obvious, and the native support of swift for async/await is only within one or two years. It's a little embarrassing that coobjc appears at this time.

Other reference

The thinking and best practice of programming method based on CO process in mobile terminal research and development

Design of coobjc framework

[coobjc usage](

Tags: iOS Programming Swift Javascript

Posted on Tue, 05 Nov 2019 05:18:42 -0500 by nishanthc12