iOS多線程——你要知道的NSThread都在這里

你要知道的iOS多線程N(yùn)SThread、GCD、NSOperation、RunLoop都在這里

轉(zhuǎn)載請(qǐng)注明出處 http://www.lxweimin.com/p/973f0a5e0ec3

本系列文章主要講解iOS中多線程的使用,包括:NSThread、GCD、NSOperation以及RunLoop的使用方法詳解,本系列文章不涉及基礎(chǔ)的線程/進(jìn)程、同步/異步、阻塞/非阻塞、串行/并行,這些基礎(chǔ)概念,有不明白的讀者還請(qǐng)自行查閱。本系列文章將分以下幾篇文章進(jìn)行講解,讀者可按需查閱。

組織架構(gòu)說(shuō)明

本系列文章是按照相關(guān)多線程類的抽象層次撰寫(xiě)的,也就是說(shuō)NSThreadFoundation框架提供的最基礎(chǔ)的多線程類,每一個(gè)NSThread類的對(duì)象即代表一個(gè)線程,接下來(lái)蘋(píng)果為開(kāi)發(fā)者封裝了GCD(Grand Central Dispatch)GCD相比于NSThread來(lái)說(shuō),提供了便捷的操作方法,開(kāi)發(fā)者不需要再關(guān)注于管理線程的生命周期,也不需要自行管理一個(gè)線程池用于線程的復(fù)用,但GCD是以C函數(shù)對(duì)外提供接口,因此Foundation框架在GCD的基礎(chǔ)上進(jìn)行了面向?qū)ο蟮姆庋b,提供了面向?qū)ο蟮亩嗑€程類NSOperationNSOperationQueue,抽象層次更高。

由于OCC語(yǔ)言的超集,開(kāi)發(fā)者也可以選擇使用POSIX標(biāo)準(zhǔn)的線程pthreadpthreadNSThread都是對(duì)內(nèi)核mach kernelmach thread的封裝,所以在開(kāi)發(fā)時(shí)一般不會(huì)使用pthread

RunLoop是與線程相關(guān)的一個(gè)基本組成,想要線程在執(zhí)行完任務(wù)后不退出,在沒(méi)有任務(wù)時(shí)睡眠以節(jié)省CPU資源都需要RunLoop的實(shí)現(xiàn),因此,正確的理解線程就需要深入理解RunLoop相關(guān)知識(shí)。

NSThread的使用姿勢(shì)全解

組織架構(gòu)說(shuō)明中講到,NSThread是對(duì)內(nèi)核mach kernel中的mach thread的封裝,所以,每一個(gè)NSThread的對(duì)象其實(shí)就是一個(gè)線程,我們創(chuàng)建一個(gè)NSThread對(duì)象也就意味著我們創(chuàng)建了一個(gè)新的線程。初始化創(chuàng)建NSThread的方法有如下幾種:

/*
使用target對(duì)象的selector作為線程的任務(wù)執(zhí)行體,該selector方法最多可以接收一個(gè)參數(shù),該參數(shù)即為argument
*/
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

/*
使用block作為線程的任務(wù)執(zhí)行體
*/
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

/*
類方法,返回值為void
使用一個(gè)block作為線程的執(zhí)行體,并直接啟動(dòng)線程
上面的實(shí)例方法返回NSThread對(duì)象需要手動(dòng)調(diào)用start方法來(lái)啟動(dòng)線程執(zhí)行任務(wù)
*/
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

/*
類方法,返回值為void
使用target對(duì)象的selector作為線程的任務(wù)執(zhí)行體,該selector方法最多接收一個(gè)參數(shù),該參數(shù)即為argument
同樣的,該方法創(chuàng)建完線程后會(huì)自動(dòng)啟動(dòng)線程不需要手動(dòng)觸發(fā)
*/
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

下面分別舉幾個(gè)栗子:

/*
說(shuō)明: 本文的栗子都是在單視圖的工程中執(zhí)行,防止主線程退出后,其他線程被退出,不方便實(shí)驗(yàn)。
*/

//線程的任務(wù)執(zhí)行體并接收一個(gè)參數(shù)arg
- (void)firstThread:(id)arg
{
    for (int i = 0; i < 10; i++)
    {
        NSLog(@"Task %@ %@", [NSThread currentThread], arg);
    }
    NSLog(@"Thread Task Complete");
}

- (void)viewWillAppear:(BOOL)animated
{    
    [super viewWillAppear: YES];
    
    /*
    創(chuàng)建一個(gè)線程,線程任務(wù)執(zhí)行體為firstThread:方法
    該方法可以接收參數(shù)@"Hello, World"
    */
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(firstThread:) object:@"Hello, World"];
    //設(shè)置線程的名字,方便查看
    [thread setName:@"firstThread"];
    //啟動(dòng)線程
    [thread start];    
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear: YES];
    NSLog("ViewDidAppear");
}

上面的栗子沒(méi)有什么實(shí)際意義,僅僅為了展示如何創(chuàng)建并啟動(dòng)線程,啟動(dòng)程序后就可以看到程序輸出了10次

Task <NSThread: 0x1c446f780>{number = 4, name = firstThread} Hello, World

上面輸出了線程的名稱,還輸出了我們傳入的參數(shù),通過(guò)很簡(jiǎn)單的代碼就可以創(chuàng)建一個(gè)新的線程來(lái)執(zhí)行任務(wù),在開(kāi)發(fā)中盡量將耗時(shí)的操作放在其他線程中執(zhí)行,只將更新UI的操作放在主線程中執(zhí)行。

一般情況下,通過(guò)上述方法創(chuàng)建的線程在執(zhí)行完任務(wù)執(zhí)行體后就會(huì)退出并銷毀,可以在firstThread:方法的第二個(gè)NSLog方法和viewDidAppear:方法的輸出上打斷點(diǎn),然后運(yùn)行程序查看線程信息,在第一個(gè)斷點(diǎn)時(shí)即firstThread:方法的斷點(diǎn)中,程序中線程信息如下圖:

線程執(zhí)行體執(zhí)行時(shí)應(yīng)用線程信息

從上圖可以看到,現(xiàn)在程序中有一個(gè)線程名為firstThread,該線程即為我們創(chuàng)建的NSThread對(duì)象,而com.apple.main-thread(serial)即為主線程的名稱,其中serial是指明主線程是串行的,這個(gè)內(nèi)容會(huì)在GCD中進(jìn)行講解,我們可以通過(guò)類方法[NSThread mainThread]來(lái)獲取主線程。接下來(lái)繼續(xù)執(zhí)行到第二個(gè)斷點(diǎn),程序中線程信息如下圖:

線程執(zhí)行體執(zhí)行完成后應(yīng)用線程信息

從上圖可以看到,firstThread線程不見(jiàn)了,因?yàn)樵趫?zhí)行完任務(wù)執(zhí)行體后該線程就退出并被銷毀了,

通過(guò)這個(gè)栗子也說(shuō)明了,我們無(wú)法復(fù)用NSThread,盡管線程的創(chuàng)建相比進(jìn)程更加輕量級(jí),但創(chuàng)建一個(gè)線程遠(yuǎn)比創(chuàng)建一個(gè)普通對(duì)象要消耗資源,而主線程和接收事件處理的線程仍然存在,這正是因?yàn)?code>RunLoop的作用,這個(gè)內(nèi)容也會(huì)在RunLoop部分進(jìn)行講解。

接下來(lái)繼續(xù)講解創(chuàng)建NSThread的其他方法,具體栗子如下:

//栗子2:
/*
通過(guò)傳入block的方式創(chuàng)建一個(gè)線程,線程執(zhí)行體即為block的內(nèi)容
但該方式創(chuàng)建線程無(wú)法傳入?yún)?shù)
*/
NSThread *thread = [[NSThread alloc] initWithBlock:^{
    for (int i = 0; i < 100; i++)
    {
        NSLog(@"Task %@", [NSThread currentThread]);
    }
}];
//設(shè)置線程名稱
[thread setName:@"firstThread"];
//啟動(dòng)線程
[thread start];

//栗子3:
/*
通過(guò)類方法創(chuàng)建并自動(dòng)啟動(dòng)一個(gè)線程
該線程的執(zhí)行體即為傳入的block
*/
[NSThread detachNewThreadWithBlock:^{
    for (int i = 0; i < 100; i++)
    {
        NSLog(@"Task %@", [NSThread currentThread]);
    }
}];

//栗子4:
/*
通過(guò)類方法創(chuàng)建并自動(dòng)啟動(dòng)一個(gè)線程
該線程的執(zhí)行體為self的firstThread:方法,并傳入相關(guān)參數(shù)
*/
[NSThread detachNewThreadSelector:@selector(firstThread:) toTarget:self withObject:@"Hello, World!"];

上述把所有NSThread的創(chuàng)建方法都講解了一遍,實(shí)例方法和類方法的區(qū)別就在于,實(shí)例方法會(huì)返回NSThread對(duì)象,當(dāng)需要啟動(dòng)線程時(shí)需要手動(dòng)觸發(fā)start方法,而類方法沒(méi)有返回值,創(chuàng)建線程后立即啟動(dòng)該線程。這里說(shuō)的啟動(dòng)線程start方法,僅僅是將線程的狀態(tài)從新建轉(zhuǎn)為就緒,何時(shí)執(zhí)行該線程的任務(wù)需要系統(tǒng)自行調(diào)度。

接下來(lái)再看NSThread中幾個(gè)比較常用的屬性和方法:

/*
類屬性,用于獲取當(dāng)前線程
如果是在主線程調(diào)用則返回主線程對(duì)象
如果在其他線程調(diào)用則返回其他的當(dāng)前線程
什么線程調(diào)用,就返回什么線程
*/
@property (class, readonly, strong) NSThread *currentThread;

//類屬性,用于返回主線程,不論在什么線程調(diào)用都返回主線程
@property (class, readonly, strong) NSThread *mainThread;

/*
設(shè)置線程的優(yōu)先級(jí),范圍為0-1的doule類型,數(shù)字越大優(yōu)先級(jí)越高
我們知道,系統(tǒng)在進(jìn)行線程調(diào)度時(shí),優(yōu)先級(jí)越高被選中到執(zhí)行狀態(tài)的可能性越大
但是我們不能僅僅依靠?jī)?yōu)先級(jí)來(lái)判斷多線程的執(zhí)行順序,多線程的執(zhí)行順序無(wú)法預(yù)測(cè)
*/
@property double threadPriority;

//線程的名稱,前面的栗子已經(jīng)介紹過(guò)了
@property (nullable, copy) NSString *name

//判斷線程是否正在執(zhí)行
@property (readonly, getter=isExecuting) BOOL executing;

//判斷線程是否結(jié)束
@property (readonly, getter=isFinished) BOOL finished;

//判斷線程是否被取消
@property (readonly, getter=isCancelled) BOOL cancelled;

/*
讓線程睡眠,立即讓出當(dāng)前時(shí)間片,讓出CPU資源,進(jìn)入阻塞狀態(tài)
類方法,什么線程執(zhí)行該方法,什么線程就會(huì)睡眠
*/
+ (void)sleepUntilDate:(NSDate *)date;

//同上,這里傳入時(shí)間
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

//退出當(dāng)前線程,什么線程執(zhí)行,什么線程就退出
+ (void)exit;

/*
實(shí)例方法,取消線程
調(diào)用該方法會(huì)設(shè)置cancelled屬性為YES,但并不退出線程
*/
- (void)cancel;

接下來(lái)再舉一個(gè)栗子:

//按鈕點(diǎn)擊事件處理器
- (void)btnClicked
{
    //取消線程
    [self.thread cancel];
}

- (void)viewWillAppear:(BOOL)animated
{    
     self.thread = [[NSThread alloc] initWithBlock:^{
        for (int i = 0; i < 100; i++)
        {
            //獲取當(dāng)前正在執(zhí)行的線程,即self.thread
            NSThread *currentThread = [NSThread currentThread];
            //判斷線程是否被取消
            if ([currentThread isCancelled])
            {
                //如果被取消就退出當(dāng)前正在執(zhí)行的線程,即self.thread
                [NSThread exit];
            }
            NSLog(@"Task %@", currentThread);
            //循環(huán)內(nèi),每次循環(huán)睡1s
            [NSThread sleepForTimeInterval:1];
        }
    }];
    [self.thread setName:@"firstThread"];
    //啟動(dòng)線程
    [self.thread start];    
}

上面的栗子也比較簡(jiǎn)單,在視圖中加入了一個(gè)按鈕,點(diǎn)擊按鈕就會(huì)讓我們創(chuàng)建的線程執(zhí)行退出方法,在viewWillAppear:方法中創(chuàng)建并啟動(dòng)了一個(gè)線程,這個(gè)線程每次循環(huán)都會(huì)判斷當(dāng)前線程是否被取消,如果取消就退出當(dāng)前線程,接下來(lái)線程就會(huì)被銷毀,每次循環(huán)執(zhí)行完后都會(huì)讓當(dāng)前線程睡眠一秒,這里可能很多人都會(huì)有誤區(qū),讓線程睡眠會(huì)使得線程進(jìn)入阻塞狀態(tài),當(dāng)睡眠時(shí)間到后就會(huì)從阻塞狀態(tài)進(jìn)入就緒狀態(tài),被系統(tǒng)線程調(diào)度為執(zhí)行狀態(tài)后才能繼續(xù)執(zhí)行,所以這里睡1s并不是說(shuō)精準(zhǔn)的1s后再繼續(xù)執(zhí)行,只是1s后從阻塞態(tài)進(jìn)入就緒態(tài),之后何時(shí)執(zhí)行由系統(tǒng)調(diào)度決定。還需要說(shuō)明的是cancel方法并不會(huì)讓線程退出,僅僅是將cancelled屬性置為YES,退出需要我們手動(dòng)觸發(fā)exit方法。

所以執(zhí)行上述代碼后,每一秒多會(huì)輸出一次,當(dāng)我們點(diǎn)擊按鈕后該線程就會(huì)將cancelled屬性置為YES,在線程下次執(zhí)行時(shí)就會(huì)執(zhí)行exit方法退出線程,退出線程會(huì)立即終止當(dāng)前執(zhí)行的任務(wù),也就是說(shuō)exit方法后的代碼不會(huì)再執(zhí)行了。

退出線程有如下三種情況:

  • 任務(wù)執(zhí)行體執(zhí)行完成后正常退出
  • 任務(wù)執(zhí)行體執(zhí)行過(guò)程中發(fā)生異常也會(huì)導(dǎo)致當(dāng)前線程退出
  • 執(zhí)行NSThread類的exit方法退出當(dāng)前線程

關(guān)于優(yōu)先級(jí)的栗子就不再贅述了,可以自行實(shí)驗(yàn),比如,啟動(dòng)兩個(gè)線程,使用for循環(huán)來(lái)輸出文本,并設(shè)置不同的優(yōu)先級(jí),可以發(fā)現(xiàn),優(yōu)先級(jí)高的線程獲取到時(shí)間片即能夠執(zhí)行輸出的機(jī)會(huì)高于優(yōu)先級(jí)低的。

接下來(lái)舉一個(gè)多線程下載圖片的簡(jiǎn)單栗子:

- (void)viewWillAppear:(BOOL)animated
{
    //創(chuàng)建一個(gè)線程用來(lái)下載圖片    
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1508398116220&di=ba2b7c9bf32d0ecef49de4fb19741edb&imgtype=0&src=http%3A%2F%2Fwscont2.apps.microsoft.com%2Fwinstore%2F1x%2Fea9a3c59-bb26-4086-b823-4a4869ffd9f2%2FScreenshot.398115.100000.jpg"]]];
        //圖片下載完成之后使用主線程來(lái)執(zhí)行更新UI的操作
        [self performSelectorOnMainThread:@selector(updateImage:) withObject:image waitUntilDone:NO];
    }];
    //啟動(dòng)線程
    [thread start];    
}

//主線程執(zhí)行當(dāng)前更新UI的方法
- (void)updateImage:(UIImage*)image
{
    self.imageView.image = image;
}

上面使用了NSObject提供的performSelectorOnMainThread:WithObject:watiUntilDone:方法,該方法就是用于使用主線程執(zhí)行相關(guān)方法,iOS對(duì)于更新UI的操作有規(guī)定,必須放在主線程執(zhí)行,否則會(huì)產(chǎn)生運(yùn)行時(shí)警告,最重要的是,不在主線程執(zhí)行無(wú)法預(yù)知什么時(shí)候才會(huì)進(jìn)行更新操作,可能會(huì)產(chǎn)生各種意外。

NSThread 鎖機(jī)制 經(jīng)典的生產(chǎn)者消費(fèi)者問(wèn)題

提到多線程必然會(huì)考慮競(jìng)爭(zhēng)條件OC也為我們提供了同步的機(jī)制以及鎖的機(jī)制,接下來(lái)舉一個(gè)炒雞經(jīng)典的銀行取錢的栗子:

//定義一個(gè)Account類
@interface Account: NSObject
//賬號(hào)
@property (nonatomic, strong) NSString *accountNumber;
//余額
@property (nonatomic, assign) double balance;
//取錢操作
- (void)draw:(id)money;

@end

@implementation Account

@synthesize accountNumber = _accountNumber;
@synthesize balance = _balance;

- (void)draw:(id)money
{
    double drawMoney = [money doubleValue];
    //判斷余額是否足夠
    if (self.balance >= drawMoney)
    {
        //當(dāng)前線程睡1毫秒
        //[NSThread sleepForTimeInterval:0.001];
        self.balance -= drawMoney;
        NSLog(@"%@ draw money %lf balance left %lf", [[NSThread currentThread] name], drawMoney, self.balance);
    }
    else
    {
        //余額不足,提示
        NSLog(@"%@ Balance Not Enouth", [[NSThread currentThread] name]);
    }
}

@end

//ViewController.m
- (void)viewWillAppear:(BOOL)animated
{    
    Account *account = [[Account alloc] init];
    account.accountNumber = @"1603121434";
    account.balance = 1500.0;
    
    NSThread *thread1 = [[NSThread alloc] initWithTarget:account selector:@selector(draw:) object:@(1000)];
    [thread1 setName:@"Thread1"];
    
    NSThread *thread2 = [[NSThread alloc] initWithTarget:account selector:@selector(draw:) object:@(1000)];
    [thread2 setName:@"Thread2"];
    
    [thread1 start];
    [thread2 start];    
}

上面這個(gè)栗子很簡(jiǎn)單,定義了一個(gè)Account類表示銀行賬戶,然后定義了取錢的操作,在draw:方法里,注釋了[NSThread sleepForTimeInterval:0.001];代碼,然后在視圖中創(chuàng)建了兩個(gè)線程,都去取錢,運(yùn)行上述程序我們發(fā)現(xiàn)線程1取到錢了,線程2提示余額不足,但這個(gè)結(jié)果不一定正確,我們提到過(guò),多線程的執(zhí)行順序是無(wú)法預(yù)測(cè)的,哪怕線程2的優(yōu)先級(jí)比線程1低,也有可能線程2先執(zhí)行,所以我們把注釋的一行去掉注釋,來(lái)模擬第一個(gè)線程進(jìn)入到取錢的判斷條件體以后被系統(tǒng)線程調(diào)度切換,此時(shí)的輸出結(jié)果為:

Thread1 draw money 1000.000000 balance left 500.000000
Thread2 draw money 1000.000000 balance left -500.000000

這就是競(jìng)爭(zhēng)條件,這里不再贅述什么是競(jìng)爭(zhēng)條件,線程1進(jìn)入判斷體后還沒(méi)有進(jìn)行取錢的操作就被切換到就緒態(tài),系統(tǒng)切換線程2執(zhí)行,由于線程1還沒(méi)有進(jìn)行取錢操作,所以余額是滿足要求的,線程2也進(jìn)入了判斷體,這樣兩個(gè)線程都可以取到錢。

解決競(jìng)爭(zhēng)條件的方法很多,比如鎖機(jī)制和同步代碼塊,接下來(lái)分別舉兩個(gè)栗子:

//栗子2:
- (void)draw:(id)money
{
    @synchronized (self) {
        double drawMoney = [money doubleValue];
        
        if (self.balance >= drawMoney)
        {
            [NSThread sleepForTimeInterval:0.001];
            self.balance -= drawMoney;
            NSLog(@"%@ draw money %lf balance left %lf", [[NSThread currentThread] name], drawMoney, self.balance);
        }
        else
        {
            NSLog(@"%@ Balance Not Enouth", [[NSThread currentThread] name]);
        }
    }
}

//栗子3:
- (void)draw:(id)money
{
    /*
    self.lock在ViewController的初始化函數(shù)中進(jìn)行初始化操作
    self.lock = [[NSLock alloc] init];
    */
    [self.lock lock];
    double drawMoney = [money doubleValue];
    
    if (self.balance >= drawMoney)
    {
        [NSThread sleepForTimeInterval:0.001];
        self.balance -= drawMoney;
        NSLog(@"%@ draw money %lf balance left %lf", [[NSThread currentThread] name], drawMoney, self.balance);
    }
    else
    {
        NSLog(@"%@ Balance Not Enouth", [[NSThread currentThread] name]);
    }
    [self.lock unlock];
}

在栗子2中,我們對(duì)draw:方法添加了一個(gè)同步代碼塊,使用@synchronized包圍的代碼即為同步代碼塊,同步代碼塊需要一個(gè)監(jiān)聽(tīng)器,我們使用account對(duì)象本身作為監(jiān)聽(tīng)器,因?yàn)槭?code>account對(duì)象產(chǎn)生的競(jìng)爭(zhēng)條件,當(dāng)執(zhí)行同步代碼塊時(shí)需要先獲取監(jiān)聽(tīng)器,如果獲取不到則線程會(huì)被阻塞,當(dāng)同步代碼塊執(zhí)行完成則釋放監(jiān)聽(tīng)器,與javasynchronized同步代碼塊一樣。

栗子3,我們使用鎖機(jī)制,創(chuàng)建了一個(gè)NSLock類的鎖對(duì)象,lock方法用于獲取鎖,如果鎖被其他對(duì)象占用則線程被阻塞,unlock方法用于釋放鎖,以便其他線程加鎖。

線程的調(diào)度對(duì)于開(kāi)發(fā)者來(lái)說(shuō)是透明的,我們不能也無(wú)法預(yù)測(cè)線程執(zhí)行的順序,但有時(shí)我們需要線程按照一定條件來(lái)執(zhí)行,這時(shí)就需要線程間進(jìn)行通信,NSCondition就提供了線程間通信的方法,查看一下NSCondition的聲明文件:

NS_CLASS_AVAILABLE(10_5, 2_0)
@interface NSCondition : NSObject <NSLocking> {
@private
    void *_priv;
}

/*
調(diào)用NSCondition對(duì)象wait方法的線程會(huì)阻塞,直到其他線程調(diào)用該對(duì)象的signal方法或broadcast方法來(lái)喚醒
喚醒后該線程從阻塞態(tài)改為就緒態(tài),交由系統(tǒng)進(jìn)行線程調(diào)度
執(zhí)行wait方法時(shí)內(nèi)部會(huì)自動(dòng)執(zhí)行unlock方法釋放鎖,并阻塞線程
*/
- (void)wait;

//同上,只是該方法是在limit到達(dá)時(shí)喚醒線程
- (BOOL)waitUntilDate:(NSDate *)limit;

/*
喚醒在當(dāng)前NSCondition對(duì)象上阻塞的一個(gè)線程
如果在該對(duì)象上wait的有多個(gè)線程則隨機(jī)挑選一個(gè),被挑選的線程則從阻塞態(tài)進(jìn)入就緒態(tài)
*/
- (void)signal;

/*
同上,該方法會(huì)喚醒在當(dāng)前NSCondition對(duì)象上阻塞的所有線程
*/
- (void)broadcast;

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

NS_ASSUME_NONNULL_END

NSCondition實(shí)現(xiàn)了NSLocking協(xié)議,所以NSCondition同樣具有鎖的功能,與NSLock一樣可以獲取鎖與釋放鎖的操作。了解了NSCondition基本方法,就可以實(shí)現(xiàn)生產(chǎn)者消費(fèi)者問(wèn)題了:

@interface Account: NSObject

@property (nonatomic, strong) NSString *accountNumber;
@property (nonatomic, assign) double balance;
@property (nonatomic, strong) NSCondition *condition;
@property (nonatomic, assign) BOOL haveMoney;

- (void)deposite:(id)money;
- (void)draw:(id)money;

@end

@implementation Account

@synthesize accountNumber = _accountNumber;
@synthesize balance = _balance;
@synthesize condition = _condition;
@synthesize haveMoney = _haveMoney;

//NSCondition的getter,用于創(chuàng)建NSCondition對(duì)象
- (NSCondition*)condition
{
    if (_condition == nil)
    {
        _condition = [[NSCondition alloc] init];
    }
    return _condition;
}

- (void)draw:(id)money
{
    //設(shè)置消費(fèi)者取錢20次
    int count = 0;
    while (count < 20)
    {
        //首先使用condition上鎖,如果其他線程已經(jīng)上鎖則阻塞
        [self.condition lock];
        //判斷是否有錢
        if (self.haveMoney)
        {
            //有錢則進(jìn)行取錢的操作,并設(shè)置haveMoney為NO
            self.balance -= [money doubleValue];
            self.haveMoney = NO;
            count += 1;
            NSLog(@"%@ draw money %lf %lf", [[NSThread currentThread] name], [money doubleValue], self.balance);
            //取錢操作完成后喚醒其他在次condition上等待的線程
            [self.condition broadcast];
        }
        else
        {
            //如果沒(méi)有錢則在次condition上等待,并阻塞
            [self.condition wait];
            //如果阻塞的線程被喚醒后會(huì)繼續(xù)執(zhí)行代碼
            NSLog(@"%@ wake up", [[NSThread currentThread] name]);
        }
        //釋放鎖
        [self.condition unlock];
    }
}

- (void)deposite:(id)money
{
    //創(chuàng)建了三個(gè)取錢線程,每個(gè)取錢20次,則存錢60次
    int count = 0;
    while (count < 60)
    {   
        //上鎖,如果其他線程上鎖了則阻塞
        [self.condition lock];
        //判斷如果沒(méi)有錢則進(jìn)行存錢操作
        if (!self.haveMoney)
        {
            //進(jìn)行存錢操作,并設(shè)置haveMoney為YES
            self.balance += [money doubleValue];
            self.haveMoney = YES;
            count += 1;
            NSLog(@"Deposite money %lf %lf", [money doubleValue], self.balance);
            //喚醒其他所有在condition上等待的線程
            [self.condition broadcast];
        }
        else
        {
            //如果有錢則等待
            [self.condition wait];
            NSLog(@"Deposite Thread wake up");
        }
        //釋放鎖
        [self.condition unlock];
    }
}

@end

- (void)viewWillAppear:(BOOL)animate
{
    
    [super viewWillAppear:YES];

    Account *account = [[Account alloc] init];
    account.accountNumber = @"1603121434";
    account.balance = 0;
    //消費(fèi)者線程1,每次取1000元
    NSThread *thread = [[NSThread alloc] initWithTarget:account selector:@selector(draw:) object:@(1000)];
    [thread setName:@"consumer1"];
    
    //消費(fèi)者線程2,每次取1000元
    NSThread *thread2 = [[NSThread alloc] initWithTarget:account selector:@selector(draw:) object:@(1000)];
    [thread2 setName:@"consumer2"];
    
    //消費(fèi)者線程3,每次取1000元
    NSThread *thread3 = [[NSThread alloc] initWithTarget:account selector:@selector(draw:) object:@(1000)];
    [thread3 setName:@"consumer3"];
    
    //生產(chǎn)者線程,每次存1000元
    NSThread *thread4 = [[NSThread alloc] initWithTarget:account selector:@selector(deposite:) object:@(1000)];
    [thread4 setName:@"productor"];

    [thread start];
    [thread2 start];
    [thread3 start];
    [thread4 start];
}

上面這個(gè)栗子也比較簡(jiǎn)單,關(guān)于NSCondition需要注意的就是它的wait方法,在執(zhí)行wait方法前按照邏輯當(dāng)然是要先獲取鎖,避免競(jìng)爭(zhēng)條件,執(zhí)行wait方法后會(huì)阻塞當(dāng)前線程,直到其他線程調(diào)用這個(gè)condition來(lái)喚醒被阻塞的線程,被阻塞的線程喚醒后進(jìn)入就緒態(tài),當(dāng)被調(diào)度執(zhí)行后會(huì)重新獲取鎖并在wait方法下一行代碼繼續(xù)執(zhí)行。還有一個(gè)要注意的地方就是是否有錢的haveMoney這個(gè)flag,這個(gè)flag存在的意義就是,當(dāng)線程被喚醒后進(jìn)入就緒態(tài),接下來(lái)系統(tǒng)線程調(diào)度具體調(diào)度哪個(gè)線程來(lái)執(zhí)行開(kāi)發(fā)者是不知道的,也就是說(shuō)我們無(wú)法預(yù)知接下來(lái)執(zhí)行的是生產(chǎn)者還是消費(fèi)者,為了避免錯(cuò)誤,加一個(gè)flag用于判斷。

上面代碼的寫(xiě)法是按照蘋(píng)果官方文檔的順序?qū)懙模嚓P(guān)于NSCondition可查閱官方文檔:Apple NSCondition

備注

由于作者水平有限,難免出現(xiàn)紕漏,如有問(wèn)題還請(qǐng)不吝賜教。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容