Original address: kyson.cn/index.php/a…

background

An article (hereinafter referred to as “CooBJC introduction article”) flooded wechat moments yesterday: Just now, Ali open source iOS coprogramming development framework CooBJC! . Most iOS devs are probably confused:

  • What is a coroutine?
  • What does a coroutine do?
  • Why use it?

Therefore, I would like to popularize the knowledge of coroutines, run the Example of CoobJC, and analyze the source code of CooBJC.

Analysis of the

Coroutines wikipedia is here: coroutines. The quote explains:

A coroutine is a component of a computer program that promotes non-preemptive multitasking subroutines that allow execution to be suspended and resumed. Coroutines are more general and flexible than subroutines, but are not as widely used in practice. Coroutines are derived from Simula and Modula-2 languages, but other languages are also supported. Coroutines are better suited for implementing familiar program components such as cooperative multitasking, exception handling, event loops, iterators, infinite lists, and pipes. According to Gartner, Marvin Conway invented the term Coroutine in 1958 and used it to build assembler programs.

Yeah, I don’t know much about it. But at least we know

  • The English word for coroutine is “coroutine”, so we can understand the name of Ali’s librarycoobjcThe meaning of. So where did the word come from? I dig a little deeper, and coroutines are literally “co-operative routines.”
  • Coroutines are associated with processes or threads
  • Coroutines have a long history, butObjective-CIs not supported. The author has found that many modern languages support coroutines. Like Python and Swift, even C supports coroutines.

The purpose of coroutines is actually mentioned in coobJC’s introductory article to optimize asynchronous operations in iOS. The following problems have been solved:

  • “Nested hell”
  • Error handling is complex and verbose
  • It’s easy to forget to call the Completion handler
  • Conditional execution becomes difficult
  • It becomes extremely difficult to combine returns from calls that are independent of each other
  • Execution continues in the wrong thread
  • Hard to locate cause of multithreaded crash
  • Lock and semaphore abuse can cause stuttering and stalling

That sounds powerful, but the most obvious benefit is that you can simplify your code; The coobJC introduction also stated that performance is guaranteed: CooBJC’s advantage is significant when the order of magnitude of threads is greater than 1000 or more. To prove the conclusion of this article, let’s run the coobjc source code. Download the coobJC source code here. The directory structure is as follows:

coobjc
coobjc

  • cokitAnd subdirectories that are based on the UIKit layercoobjcencapsulation
  • coobjcDirectory iscoobjctheObjective-CVersion of the source code
  • coswiftDirectory iscoobjctheSwiftVersion of the source code
  • ExampleThere are two directories, one isObjective-COne is the implementation ofSwiftVersion of the implementation of Demo

CoobjcBaseExample project: Open the coobjcBaseExample project and run the pod update with the following result:

If you open your Podfile, you can find the coobJC library in it, as well as Specta, Expecta, and OCMock. These three libraries are not covered here, but you need to know that they are used for unit testing.

Let’s first look at what the implementation logic of this list looks like. It is easy to locate the page in KMDiscoverListViewController on its network request (this is the movie list) code is as follows:

- (void)requestMovies
{
    co_launch(^{
        NSArray *dataArray = [[KMDiscoverSource discoverSource] getDiscoverList:@"1"];
        [self.refreshControl endRefreshing];
        
        if(dataArray ! = nil) { [self processData:dataArray]; }else{ [self.networkLoadingViewController showErrorView]; }}); }Copy the code

It’s easy to understand the code here

        NSArray *dataArray = [[KMDiscoverSource discoverSource] getDiscoverList:@"1"];
Copy the code

Is to request network data, its implementation is as follows:

- (NSArray*)getDiscoverList:(NSString *)pageLimit;
{
    NSString *url = [NSString stringWithFormat:@"%@&page=%@", [self prepareUrl], pageLimit];
    id json = [[DataService sharedInstance] requestJSONWithURL:url];
    NSDictionary* infosDictionary = [self dictionaryFromResponseObject:json jsonPatternFile:@"KMDiscoverSourceJsonPattern.json"];
    return [self processResponseObject:infosDictionary];
}
Copy the code

The code above can also guess that,

    id json = [[DataService sharedInstance] requestJSONWithURL:url];
Copy the code

RequestJSONWithURL () {DataService = requestJSONWithURL;

- (id)requestJSONWithURL:(NSString*)url CO_ASYNC{
    SURE_ASYNC
    return await([self.jsonActor sendMessage:url]);
}
Copy the code

All right. Now that I don’t understand it, let’s start at the very beginning and learn what coroutines mean and how to use them. Then cooBJC source code is analyzed.

Introduction to coroutines

Coobjc is mentioned in the introduction article

  • The first: useglibcucontextComponent (library of cloud wind).
  • Second: use assembly code to switch contexts (implement C coroutines), the principle is the sameucontext.
  • Third: use C language syntaxswitch-caseBizarre techniques to achieve (Protothreads).
  • Fourth: the use of C languagesetjmplongjmp.
  • Fifth: use the compiler to support syntax sugar.

The second option was selected. So let’s analyze one by one why Coobjc rejected the other. First of all, coobjc’s introduction mentioned that uContext is deprecated in iOS, so how do we use uContext if it’s not deprecated? The following Demo illustrates the use of uContext:

#include <stdio.h>
#include <ucontext.h>
#include <unistd.h>
 
int main(int argc, const char *argv[]){
    ucontext_t context;
    getcontext(&context);
    puts("Hello world");
    sleep(1);
    setcontext(&context);
    return 0;
}
Copy the code

Note: Sample code from Wikipedia.

Save the above code to example.c and run the compile command:

gcc example.c -o example
Copy the code

Think about what the result will be when the program runs.

kysonzhu@ubuntu:~$ ./example 
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
^C
kysonzhu@ubuntu:~$
Copy the code

Above is part of the output of the program execution, I do not know whether it is the same as you think? As you can see, the program does not exit the program after the first “Hello world”, but keeps on printing” Hello world”. It saves a context with getContext, then prints “Hello World”, and then executes the code again where it retrieves getContext with setContext, so it keeps printing “Hello World”, and in my newbie’s eyes, This is a magical jump. So the question is, what exactly is uContext?

Here I do not do more introduction, recommend an article, more detailed: Ucontext – a simple coroutine library that anyone can implement. Here we need to know that the simulation of uContext using assembly language mentioned in coobJC is actually the simulation of the setContext and getContext functions in the above example. To prove my conjecture, I opened the coobJC source library and found the only assembly file in it, coroutine_context.s

  • _coroutine_getcontext
  • _coroutine_begin
  • _coroutine_setcontext

It proved the author’s idea. These three methods are exposed in the file coroutine_context.h for subsequent calls:

extern int coroutine_getcontext (coroutine_ucontext_t *__ucp);
extern int coroutine_setcontext (coroutine_ucontext_t *__ucp);
extern int coroutine_begin (coroutine_ucontext_t *__ucp);
Copy the code

Now let’s talk about another function

int  setcontext(const ucontext_t *cut)
Copy the code

This function sets the current context to cut. The context cut of setContext should be obtained by getContext or makecontext, and will not be returned if the call succeeds. If the context is obtained by calling getContext (), the program continues to execute the call. If the context is obtained by calling makecontext, the program calls the function pointed to by the second argument to makecontext, If the func function returns, it restores the uc_link that was pointed to in context_t by the first argument to makecontext. If UC_link is NULL, the thread exits.

Let’s draw a table that compares the functions of uContext and Coobjc:

ucontext coobjc meaning
setcontext coroutine_setcontext Sets the coroutine context
getcontext coroutine_getcontext Gets the coroutine context
makecontext coroutine_create Create a coroutine context

In this way, our previous program can be rewritten as follows:

#import <coobjc/coroutine_context.h>

int main(int argc, const char *argv[]) {
    coroutine_ucontext_t context;
    coroutine_getcontext(&context);
    puts("Hello world");
    sleep(1);
    coroutine_setcontext(&context);
    return 0;
}
Copy the code

The result is still the same, printing “Hello World”.

In-depth coroutines

(1) Catalog analysis

coobjc

  • coreThe directory provides the core coroutine functions
  • apiDirectory iscoobjcBased on theObjective-CThe encapsulation
  • csp, directory from the librarylibtaskIntroduced to provide some chain operations
  • objcprovidescoobjcObject declaration cycle management for some classes in the following article, THE author will start from the corecoreCatalog began to study, we understand behind it is not complicated.

(2) Composition of coroutines

We only briefly introduced coobJC above, and we know that CoobJC basically refers to uContext. In the following example, I try to introduce the uContext first and then apply it to the coobjc corresponding method. Let’s continue our discussion of the functions mentioned above and explain their functions:

int  getcontext(ucontext_t *uctp)
Copy the code

The method is to get the current context and set the context to ucTP, which is a context structure defined as follows:

_STRUCT_UCONTEXT
{
	int                     uc_onstack;
	__darwin_sigset_t       uc_sigmask;     /* signal mask used by this context */
	_STRUCT_SIGALTSTACK     uc_stack;       /* stack used by this context */
	_STRUCT_UCONTEXT        *uc_link;       /* pointer to resuming context */
	__darwin_size_t	        uc_mcsize;      /* size of the machine context passed in */
	_STRUCT_MCONTEXT        *uc_mcontext;   /* pointer to machine specific context */
#ifdef _XOPEN_SOURCE
	_STRUCT_MCONTEXT        __mcontext_data;
#endif /* _XOPEN_SOURCE */}; /* user context */ typedef _STRUCT_UCONTEXT ucontext_t; / * /?????  user context */Copy the code

When the current context (such as the one created with Makecontext) terminates, the system restores the context pointed to by uc_link. Uc_sigmask is the set of blocking signals in this context; Uc_stack is the stack used in this context; Uc_mcontext holds a machine-specific representation of the context, including the specific register for the calling thread, and so on. In fact, it is easy to understand that the UContext actually stores the necessary data, including the data needed to save the success or failure of the case.

In comparison, coobjc is defined differently from ucontext:

/**
     The structure store coroutine's context data. */ struct coroutine { coroutine_func entry; // Process entry. void *userdata; // Userdata. coroutine_func userdata_dispose; // Userdata's dispose action.
    void *context;                          // Coroutine's Call stack data. void *pre_context; // Coroutine's source process's Call stack data. int status; // Coroutine's running status.
    uint32_t stack_size;                    // Coroutine's stack size void *stack_memory; // Coroutine's stack memory address.
    void *stack_top;                    // Coroutine's stack top address. struct coroutine_scheduler *scheduler; // The pointer to the scheduler. int8_t is_scheduler; // The coroutine is a scheduler. struct coroutine *prev; struct coroutine *next; void *autoreleasepage; // If enable autorelease, the custom autoreleasepage. bool is_cancelled; // The coroutine is cancelled }; typedef struct coroutine coroutine_t;Copy the code

Among them

    struct coroutine *prev;
    struct coroutine *next;
Copy the code

Indicates that it is a linked list structure. In coroutine. M, there are methods for adding and deleting elements in a linked list:

// add routine to the queue
void scheduler_add_coroutine(coroutine_list_t *l, coroutine_t *t) {
    if(l->tail) {
        l->tail->next = t;
        t->prev = l->tail;
    } else {
        l->head = t;
        t->prev = nil;
    }
    l->tail = t;
    t->next = nil;
}

// delete routine from the queue
void scheduler_delete_coroutine(coroutine_list_t *l, coroutine_t *t) {
    if(t->prev) {
        t->prev->next = t->next;
    } else {
        l->head = t->next;
    }
    
    if(t->next) {
        t->next->prev = t->prev;
    } else{ l->tail = t->prev; }}Copy the code

Where coroutine_list_t is used to identify the first and last nodes of the list:

/**
 Define the linked list of scheduler's queue. */ struct coroutine_list { coroutine_t *head; coroutine_t *tail; }; typedef struct coroutine_list coroutine_list_t;Copy the code

To manage all coroutine states, a scheduler is also set up:

/**
 Define the scheduler.
 One thread own one scheduler, all coroutine run this thread shares it.
 */
struct coroutine_scheduler {
    coroutine_t         *main_coroutine;
    coroutine_t         *running_coroutine;
    coroutine_list_t     coroutine_queue;
};
typedef struct coroutine_scheduler coroutine_scheduler_t;
Copy the code

As you might guess from the name, main_coroutine contains the main coroutine (either the coroutine that is to be set or the coroutine that is to be used); Running_coroutine is the currently running coroutine.

(3) Operation of coroutines

Coroutines have operations similar to threads, such as create, start, cede control, recover, and die. Accordingly, we see the following function declarations in coroutine.h:

// Close a coroutine if it is dead void coroutine_close_ifdead(coroutine_t *co); Void coroutine_resume(coroutine_t *co); // Add the coroutine to the scheduler and immediately start void coroutine_resume(coroutine_t *co); // Add the coroutine to the scheduler void coroutine_add(coroutine_t *co); Void coroutine_yield(coroutine_t *co);Copy the code

To better control the data in each operation, CooBJC also provides the following two methods:

void coroutine_setuserdata(coroutine_t *co, void *userdata, coroutine_func userdata_dispose);
void *coroutine_getuserdata(coroutine_t *co);
Copy the code

At this point, coobJC’s core code analysis is complete.

(4) Objective-C level encapsulation of coroutines

Returning to the example at the beginning of this article – (void)requestMovies method implementation, the first step is to call a co_launch() method, which will eventually be called

+ (instancetype)coroutineWithBlock:(void(^)(void))block onQueue:(dispatch_queue_t _Nullable)queue stackSize:(NSUInteger)stackSize {
    if (queue == NULL) {
        queue = co_get_current_queue();
    }
    if (queue == NULL) {
        return nil;
    }
    COCoroutine *coObj = [[self alloc] initWithBlock:block onQueue:queue];
    coObj.queue = queue;
    coroutine_t  *co = coroutine_create((void (*)(void *))co_exec);
    if (stackSize > 0 && stackSize < 1024*1024) {   // Max 1M
        co->stack_size = (uint32_t)((stackSize % 16384 > 0) ? ((stackSize/16384 + 1) * 16384) : stackSize/16384);        // Align with 16kb
    }
    coObj.co = co;
    coroutine_setuserdata(co, (__bridge_retained void *)coObj, co_obj_dispose);
    return coObj;
}

- (void)resumeNow {
    [self performBlockOnQueue:^{
        if (self.isResume) {
            return;
        }
        self.isResume = YES;
        coroutine_resume(self.co);
    }];
}
Copy the code

These two methods. In fact, the code is easy to understand, the first method is to create a coroutine, the second is to start. Finally, we will talk about the await method mentioned at the beginning of the article, which is ultimately left to Chan to handle:

- (COActorCompletable *)sendMessage:(id)message {
    COActorCompletable *completable = [COActorCompletable promise];
    dispatch_async(self.queue, ^{
        COActorMessage *actorMessage = [[COActorMessage alloc] initWithType:message completable:completable];
        [self.messageChan send_nonblock:actorMessage];
    });
    return completable;
}
Copy the code

All operations are thrown into the same thread, but are ultimately scheduled through chan. Chan is beyond the scope of this paper. If there is time, the author will analyze chan later.

conclusion

This paper introduces the concept of coroutines, compares uContext and COOBJC to illustrate the use of coroutines, and analyzes the source code of COOBJC, hoping to help you.

advertising

In order to better communicate with everyone, the little man created a wechat group. Scan my QR code to pull everyone into the group. Please note [iOS] :

Further reading

IOS unit test: Specta + Expecta + OCMock + OHHTTPStubs + KIF

Ucontext family functions as I understand them

A “flyweight” C language coprogramming library

Coroutines are not really multithreading

Ucontext – a simple coroutine library that anyone can implement