文丨清楓
協(xié)程
今年年初阿里開源的coobjc
,可謂是另iOS
開發(fā)者們大開眼界柱锹。coobjc
這個(gè)名字哪自,以co
開頭,其實(shí)可以分解為co-objc
禁熏,co
就是coroutine(協(xié)程)
單詞縮寫壤巷。
協(xié)程與子例程一樣,協(xié)程(coroutine)
也是一種程序組件瞧毙。相對(duì)子例程而言胧华,協(xié)程更為一般和靈活,但在實(shí)踐中使用沒(méi)有子例程那樣廣泛宙彪。協(xié)程源自 Simula
和 Modula-2
語(yǔ)言矩动,但也有其他語(yǔ)言支持。
協(xié)程不是進(jìn)程或線程释漆,其執(zhí)行過(guò)程更類似于子例程悲没,或者說(shuō)不帶返回值的函數(shù)調(diào)用。
一個(gè)程序可以包含多個(gè)協(xié)程男图,可以對(duì)比與一個(gè)進(jìn)程包含多個(gè)線程示姿,因而下面我們來(lái)比較協(xié)程和線程。我們知道多個(gè)線程相對(duì)獨(dú)立享言,有自己的上下文峻凫,切換受系統(tǒng)控制;而協(xié)程也相對(duì)獨(dú)立览露,有自己的上下文荧琼,但是其切換由自己控制,由當(dāng)前協(xié)程切換到其他協(xié)程由當(dāng)前協(xié)程來(lái)控制差牛。
協(xié)程的概念在60年代就已經(jīng)提出命锄,目前在服務(wù)端中應(yīng)用比較廣泛,在高并發(fā)場(chǎng)景下使用極其合適偏化,可以極大降低單機(jī)的線程數(shù)脐恩,提升單機(jī)的連接和處理能力,但是在移動(dòng)研發(fā)中侦讨,iOS
和android
目前都不支持協(xié)程的使用驶冒。
coobjc實(shí)現(xiàn)了什么(來(lái)自官方文檔)
這個(gè)庫(kù)為 Objective-C
和 Swift
提供了協(xié)程功能苟翻。coobjc支持 await、generator 和 actor model骗污,接口參考了 C#
崇猫、Javascript
和 Kotlin
中的很多設(shè)計(jì)。還提供了 cokit 庫(kù)為 Foundation
和 UIKit
中的部分 API
提供了協(xié)程化支持需忿,包括 NSFileManager
, JSON
, NSData
, UIImage
等诅炉。coobjc
也提供了元組的支持。
coobjc
是由手機(jī)淘寶架構(gòu)團(tuán)隊(duì)推出的能在 iOS
上使用的協(xié)程開發(fā)框架屋厘,目前支持 Objective-C
和 Swift
中使用涕烧,底層使用匯編和 C
語(yǔ)言進(jìn)行開發(fā),上層進(jìn)行提供了 Objective-C
和 Swift
的接口汗洒,目前以 Apache
開源協(xié)議進(jìn)行了開源议纯。
iOS異步編程的問(wèn)題
基于 Block
的異步編程回調(diào)是目前 iOS
使用最廣泛的異步編程方式,iOS
系統(tǒng)提供的 GCD
庫(kù)讓異步開發(fā)變得很簡(jiǎn)單方便仲翎,但是基于這種編程方式的缺點(diǎn)也有很多痹扇,主要有以下幾點(diǎn):
- 容易進(jìn)入"嵌套地獄"
- 錯(cuò)誤處理復(fù)雜和冗長(zhǎng)
- 容易忘記調(diào)用
completion handler
- 條件執(zhí)行變得很困難
- 從互相獨(dú)立的調(diào)用中組合返回結(jié)果變得極其困難
- 在錯(cuò)誤的線程中繼續(xù)執(zhí)行
- 難以定位原因的多線程崩潰
- 鎖和信號(hào)量濫用帶來(lái)的卡頓、卡死
上述問(wèn)題反應(yīng)到線上應(yīng)用本身就會(huì)出現(xiàn)大量的多線程崩潰
解決方案
上述問(wèn)題在很多系統(tǒng)和語(yǔ)言中都會(huì)遇到溯香,解決問(wèn)題的標(biāo)準(zhǔn)方式就是使用協(xié)程。這里不介紹太多的理論浓恶,簡(jiǎn)單說(shuō)協(xié)程就是對(duì)基礎(chǔ)函數(shù)的擴(kuò)展玫坛,可以讓函數(shù)異步執(zhí)行的時(shí)候掛起然后返回值。協(xié)程可以用來(lái)實(shí)現(xiàn) generator
包晰,異步模型以及其他強(qiáng)大的能力湿镀。
Kotlin
是這兩年由 JetBrains
推出的支持現(xiàn)代多平臺(tái)應(yīng)用的靜態(tài)編程語(yǔ)言,支持 JVM
伐憾,Javascript
勉痴,目前也可以在iOS
上執(zhí)行,這兩年在開發(fā)者社區(qū)中也是比較火树肃。
在Kotlin
語(yǔ)言中基于協(xié)程的 async/await
蒸矛,generator/yield
等異步化技術(shù)都已經(jīng)成了語(yǔ)法標(biāo)配,Kotlin 協(xié)程官方文檔胸嘴。
官方文檔
- 協(xié)程框架設(shè)計(jì) 文檔雏掠。
- coobjc Objective-C Guide 文檔。
- coobjc Swift Guide 文檔劣像。
-
cokit framework 文檔, 學(xué)習(xí)如何使用系統(tǒng)接口封裝的
api
乡话。
coobjc的設(shè)計(jì)
最底層是協(xié)程內(nèi)核,包含了棧切換的管理耳奕、協(xié)程調(diào)度器的實(shí)現(xiàn)绑青、協(xié)程間通信
channel
的實(shí)現(xiàn)等诬像。中間層是基于協(xié)程的操作符的包裝,目前支持
async/await
闸婴、Generator
坏挠、Actor
等編程模型。最上層是對(duì)系統(tǒng)庫(kù)的協(xié)程化擴(kuò)展掠拳,目前基本上覆蓋了
Foundation
和UIKit
的所有IO
和耗時(shí)方法癞揉。
核心實(shí)現(xiàn)原理
協(xié)程的核心思想是控制調(diào)用棧的主動(dòng)讓出和恢復(fù)。一般的協(xié)程實(shí)現(xiàn)都會(huì)提供兩個(gè)重要的操作:
Yield
:是讓出cpu
的意思溺欧,它會(huì)中斷當(dāng)前的執(zhí)行喊熟,回到上一次Resume
的地方。
Resume
:繼續(xù)協(xié)程的運(yùn)行姐刁。執(zhí)行Resume
后芥牌,回到上一次協(xié)程Yield
的地方。
基于線程的代碼執(zhí)行時(shí)候聂使,是沒(méi)法做出暫停操作的壁拉,現(xiàn)在要做的事情就是要代碼執(zhí)行能夠暫停,還能夠再恢復(fù)柏靶。 基本上代碼執(zhí)行都是一種基于調(diào)用棧的模型弃理,所以如果能把當(dāng)前調(diào)用棧上的狀態(tài)都保存下來(lái),然后再能從緩存中恢復(fù)屎蜓,那就能夠?qū)崿F(xiàn)yield
和 resume
痘昌。
實(shí)現(xiàn)這樣操作有幾種方法呢?
第一種:利用glibc
的 ucontext
組件(云風(fēng)的庫(kù))炬转。
第二種:使用匯編代碼來(lái)切換上下文(實(shí)現(xiàn)c協(xié)程)辆苔,原理同ucontext
。
第三種:利用C語(yǔ)言語(yǔ)法switch-case的奇淫技巧來(lái)實(shí)現(xiàn)(Protothreads
)扼劈。
第四種:利用了 C 語(yǔ)言的 setjmp
和 longjmp
驻啤。
第五種:利用編譯器支持語(yǔ)法糖。
上述第三種和第四種只是能過(guò)做到跳轉(zhuǎn)荐吵,但是沒(méi)法保存調(diào)用棧上的狀態(tài)骑冗,看起來(lái)基本上不能算是實(shí)現(xiàn)了協(xié)程,只能算做做demo
捍靠,第五種除非官方支持沐旨,否則自行改寫編譯器通用性很差。而第一種方案的 ucontext
在iOS
上是廢棄了的榨婆,不能使用磁携。coobjc
使用的是第二種方案,自己用匯編模擬一下 ucontext
良风。
coobjc的實(shí)現(xiàn)代碼
在coroutine_context.h
文件中聲明的方法:
extern int coroutine_getcontext (coroutine_ucontext_t *__ucp);
extern int coroutine_setcontext (coroutine_ucontext_t *__ucp);
extern int coroutine_begin (coroutine_ucontext_t *__ucp);
extern void coroutine_makecontext (coroutine_ucontext_t *__ucp, IMP func, void *arg, void *stackTop);
原有的C協(xié)程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;
}
可改寫成:
#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;
}
結(jié)果同樣是不斷輸出以下內(nèi)容:
Hello world
Hello world
Hello world
Hello world
...
協(xié)程結(jié)構(gòu)的設(shè)計(jì)
在coroutine.h
文件中看到如下結(jié)構(gòu):
/**
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.
struct coroutine *prev;
struct coroutine *next;
void *autoreleasepage; // If enable autorelease, the custom autoreleasepage.
void *chan_alt; // If blocking by a channel, record the alt
bool is_cancelled; // The coroutine is cancelled
int8_t is_scheduler; // The coroutine is a scheduler.
};
typedef struct coroutine coroutine_t;
/**
Define the linked list of scheduler's queue.
*/
struct coroutine_list {
coroutine_t *head;
coroutine_t *tail;
};
typedef struct coroutine_list coroutine_list_t;
從以上結(jié)構(gòu)我們不難看出谊迄,結(jié)構(gòu)體的內(nèi)容包含了一個(gè)協(xié)程的所在狀態(tài)和所所持有的信息闷供。協(xié)程隊(duì)列的增減是通過(guò)鏈表結(jié)構(gòu)實(shí)現(xiàn)。
/**
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;
上面的結(jié)構(gòu)是管理每個(gè)協(xié)程的調(diào)度器結(jié)構(gòu)统诺。
/**
Close and free a coroutine if dead.
@param co coroutine object
*/
void coroutine_close_ifdead(coroutine_t *co);
/**
Add coroutine to scheduler, and resume the specified coroutine whatever.
*/
void coroutine_resume(coroutine_t *co);
/**
Add coroutine to scheduler, and resume the specified coroutine if idle.
*/
void coroutine_add(coroutine_t *co);
/**
Yield the specified coroutine now.
*/
void coroutine_yield(coroutine_t *co);
上面可以看到協(xié)程的生命周期歪脏。
使用coobjc
async/await
- 創(chuàng)建協(xié)程
使用 co_launch
方法創(chuàng)建協(xié)程
co_launch(^{
...
});
co_launch
創(chuàng)建的協(xié)程默認(rèn)在當(dāng)前線程進(jìn)行調(diào)度
- await 異步方法
在協(xié)程中我們使用 await 方法等待異步方法執(zhí)行結(jié)束,得到異步執(zhí)行結(jié)果
- (void)viewDidLoad{
...
co_launch(^{
NSData *data = await(downloadDataFromUrl(url));
UIImage *image = await(imageFromData(data));
self.imageView.image = image;
});
}
上述代碼將原本需要 dispatch_async
兩次的代碼變成了順序執(zhí)行粮呢,代碼更加簡(jiǎn)潔
- 錯(cuò)誤處理
在協(xié)程中婿失,我們所有的方法都是直接返回值的,并沒(méi)有返回錯(cuò)誤啄寡,我們?cè)趫?zhí)行過(guò)程中的錯(cuò)誤是通過(guò) co_getError()
獲取的豪硅,比如我們有以下從網(wǎng)絡(luò)獲取數(shù)據(jù)的接口,在失敗的時(shí)候挺物, promise 會(huì) reject:error
懒浮。
- (COPromise*)co_GET:(NSString*)url
parameters:(NSDictionary*)parameters{
COPromise *promise = [COPromise promise];
[self GET:url parameters:parameters progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
[promise fulfill:responseObject];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
[promise reject:error];
}];
return promise;
}
那我們?cè)趨f(xié)程中可以如下使用:
co_launch(^{
id response = await([self co_GET:feedModel.feedUrl parameters:nil]);
if(co_getError()){
//處理錯(cuò)誤信息
}
...
});
生成器
- 創(chuàng)建生成器
我們使用 co_sequence
創(chuàng)建生成器
COCoroutine *co1 = co_sequence(^{
int index = 0;
while(co_isActive()){
yield_val(@(index));
index++;
}
});
在其他協(xié)程中,我們可以調(diào)用 next
方法识藤,獲取生成器中的數(shù)據(jù)
co_launch(^{
for(int i = 0; i < 10; i++){
val = [[co1 next] intValue];
}
});
- 使用場(chǎng)景
生成器可以在很多場(chǎng)景中進(jìn)行使用砚著,比如消息隊(duì)列、批量下載文件痴昧、批量加載緩存等:
int unreadMessageCount = 10;
NSString *userId = @"xxx";
COSequence *messageSequence = sequenceOnBackgroundQueue(@"message_queue", ^{
//在后臺(tái)線程執(zhí)行
while(1){
yield(queryOneNewMessageForUserWithId(userId));
}
});
//主線程更新UI
co(^{
for(int i = 0; i < unreadMessageCount; i++){
if(!isQuitCurrentView()){
displayMessage([messageSequence take]);
}
}
});
通過(guò)生成器稽穆,我們可以把傳統(tǒng)的生產(chǎn)者加載數(shù)據(jù)->通知消費(fèi)者模式,變成消費(fèi)者需要數(shù)據(jù)->告訴生產(chǎn)者加載模式赶撰,避免了在多線程計(jì)算中秧骑,需要使用很多共享變量進(jìn)行狀態(tài)同步,消除了在某些場(chǎng)景下對(duì)于鎖的使用扣囊。
Actor
_ Actor 的概念來(lái)自于 Erlang ,在 AKKA 中绒疗,可以認(rèn)為一個(gè) Actor 就是一個(gè)容器侵歇,用以存儲(chǔ)狀態(tài)、行為吓蘑、Mailbox 以及子 Actor 與 Supervisor 策略惕虑。Actor 之間并不直接通信,而是通過(guò) Mail 來(lái)互通有無(wú)磨镶。_
- 創(chuàng)建 actor
我們可以使用 co_actor_onqueue
在指定線程創(chuàng)建 actor
COActor *actor = co_actor_onqueue(^(COActorChan *channel) {
... //定義 actor 的狀態(tài)變量
for(COActorMessage *message in channel){
...//處理消息
}
}, q);
- 給 actor 發(fā)送消息
actor 的 send
方法可以給 actor 發(fā)送消息
COActor *actor = co_actor_onqueue(^(COActorChan *channel) {
... //定義actor的狀態(tài)變量
for(COActorMessage *message in channel){
...//處理消息
}
}, q);
// 給actor發(fā)送消息
[actor send:@"sadf"];
[actor send:@(1)];
元組
- 創(chuàng)建元組
使用 co_tuple
方法來(lái)創(chuàng)建元組
COTuple *tup = co_tuple(nil, @10, @"abc");
NSAssert(tup[0] == nil, @"tup[0] is wrong");
NSAssert([tup[1] intValue] == 10, @"tup[1] is wrong");
NSAssert([tup[2] isEqualToString:@"abc"], @"tup[2] is wrong");
可以在元組中存儲(chǔ)任何數(shù)據(jù)
- 元組取值
可以使用 co_unpack
方法從元組中取值
id val0;
NSNumber *number = nil;
NSString *str = nil;
co_unpack(&val0, &number, &str) = co_tuple(nil, @10, @"abc");
NSAssert(val0 == nil, @"val0 is wrong");
NSAssert([number intValue] == 10, @"number is wrong");
NSAssert([str isEqualToString:@"abc"], @"str is wrong");
co_unpack(&val0, &number, &str) = co_tuple(nil, @10, @"abc", @10, @"abc");
NSAssert(val0 == nil, @"val0 is wrong");
NSAssert([number intValue] == 10, @"number is wrong");
NSAssert([str isEqualToString:@"abc"], @"str is wrong");
co_unpack(&val0, &number, &str, &number, &str) = co_tuple(nil, @10, @"abc");
NSAssert(val0 == nil, @"val0 is wrong");
NSAssert([number intValue] == 10, @"number is wrong");
NSAssert([str isEqualToString:@"abc"], @"str is wrong");
NSString *str1;
co_unpack(nil, nil, &str1) = co_tuple(nil, @10, @"abc");
NSAssert([str1 isEqualToString:@"abc"], @"str1 is wrong");
- 在協(xié)程中使用元組
首先創(chuàng)建一個(gè) promise 來(lái)處理元組里的值
COPromise<COTuple*>*
cotest_loadContentFromFile(NSString *filePath){
return [COPromise promise:^(COPromiseFullfill _Nonnull resolve, COPromiseReject _Nonnull reject) {
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSData *data = [[NSData alloc] initWithContentsOfFile:filePath];
resolve(co_tuple(filePath, data, nil));
}
else{
NSError *error = [NSError errorWithDomain:@"fileNotFound" code:-1 userInfo:nil];
resolve(co_tuple(filePath, nil, error));
}
}];
}
然后溃蔫,你可以像下面這樣獲取元組里的值:
co_launch(^{
NSString *tmpFilePath = nil;
NSData *data = nil;
NSError *error = nil;
co_unpack(&tmpFilePath, &data, &error) = await(cotest_loadContentFromFile(filePath));
XCTAssert([tmpFilePath isEqualToString:filePath], @"file path is wrong");
XCTAssert(data.length > 0, @"data is wrong");
XCTAssert(error == nil, @"error is wrong");
});
使用元組你可以從 await
返回值中獲取多個(gè)值
協(xié)程的優(yōu)點(diǎn)
- 簡(jiǎn)明
- 概念少:只有很少的幾個(gè)操作符,相比響應(yīng)式幾十個(gè)操作符琳猫,簡(jiǎn)直不能再簡(jiǎn)單了
- 原理簡(jiǎn)單: 協(xié)程的實(shí)現(xiàn)原理很簡(jiǎn)單伟叛,整個(gè)協(xié)程庫(kù)只有幾千行代碼
- 易用
- 使用簡(jiǎn)單:它的使用方式比 GCD 還要簡(jiǎn)單,接口很少
- 改造方便:現(xiàn)有代碼只需要進(jìn)行很少的改動(dòng)就可以協(xié)程化脐嫂,同時(shí)我們針對(duì)系統(tǒng)庫(kù)提供了大量協(xié)程化接口
- 清晰
- 同步寫異步邏輯:同步順序方式寫代碼是人類最容易接受的方式统刮,這可以極大的減少出錯(cuò)的概率
- 可讀性高: 使用協(xié)程方式編寫的代碼比 block 嵌套寫出來(lái)的代碼可讀性要高很多
- 性能
- 調(diào)度性能更快:協(xié)程本身不需要進(jìn)行內(nèi)核級(jí)線程的切換紊遵,調(diào)度性能快,即使創(chuàng)建上萬(wàn)個(gè)協(xié)程也毫無(wú)壓力
- 減少卡頓卡死: 協(xié)程的使用以幫助開發(fā)減少鎖侥蒙、信號(hào)量的濫用暗膜,通過(guò)封裝會(huì)引起阻塞的 IO 等協(xié)程接口,可以從根源上減少卡頓鞭衩、卡死学搜,提升應(yīng)用整體的性能
參考文獻(xiàn)
官方文檔
剛剛,阿里開源 iOS 協(xié)程開發(fā)框架 coobjc
阿里開源 iOS 協(xié)程開發(fā)框架coobjc源碼分析