多線程-線程間通信、線程安全問題

前言

說到多線程同步問題就不得不提多線程中的鎖機(jī)制,多線程操作過程中往往多個線程是并發(fā)執(zhí)行的,同一個資源可能被多個線程同時訪問,造成資源搶奪,這個過程中如果沒有鎖機(jī)制往往會造成重大問題。比如常見的車票的銷售問題。


線程同步

所謂線程同步就是為了防止多個線程搶奪同一個資源造成的數(shù)據(jù)安全問題,所采取的一種措施。主要的方法有以下幾種:

  • 互斥鎖

使用@synchronized解決線程同步問題相比較NSLock要簡單一些,但是效率是眾多鎖中最差的。首先選擇一個對象作為同步對象(一般使用self),然后將”加鎖代碼”(爭奪資源的讀取、修改代碼)放到代碼塊中。 注意:鎖定1份代碼只用1把鎖,用多把鎖是無效的。使用互斥鎖,在同一個時間,只允許一條線程執(zhí)行鎖中的代碼.因?yàn)榛コ怄i的代價非常昂貴,所以鎖定的代碼范圍應(yīng)該盡可能小,只要鎖住資源讀寫部分的代碼即可。使用互斥鎖也會影響并發(fā)的目的。

   @synchronized(self) {
     //1.先檢查票數(shù)
        int count = leftTicketsCount;
        if (count>0) {
            //暫停一段時間
            [NSThread sleepForTimeInterval:0.002];
            //2.票數(shù)-1
            leftTicketsCount= count-1;
            //獲取當(dāng)前線程
            NSThread *current=[NSThread currentThread];
            NSLog(@"%@--賣了一張票,還剩余%d張票", current.name, leftTicketsCount);
        }
        else {
            //退出線程
            [NSThread exit];
        }
   }
  • 同步鎖NSLock

iOS中對于資源搶占的問題可以使用同步鎖NSLock來解決,使用時把需要加鎖的代碼(以后暫時稱這段代碼為”加鎖代碼“)放到NSLock的lock和unlock之間。

Paste_Image.png

同步鎖時如果一個線程A已經(jīng)加鎖,線程B就無法進(jìn)入。那么B怎么知道是否資源已經(jīng)被其他線程鎖住呢?可以通過tryLock方法,此方法會返回一個BOOL型的值,如果為YES說明獲取鎖成功,否則失敗。

  • 使用GCD解決資源搶占問題

在GCD中提供了一種信號機(jī)制,也可以解決資源搶占問題(和同步鎖的機(jī)制并不一樣)。GCD中信號量是dispatch_semaphore_t類型,支持信號通知和信號等待。每當(dāng)發(fā)送一個信號通知,則信號量+1;每當(dāng)發(fā)送一個等待信號時信號量-1,;如果信號量為0則信號會處于等待狀態(tài),直到信號量大于0開始執(zhí)行。根據(jù)這個原理我們可以初始化一個信號量變量,默認(rèn)信號量設(shè)置為1,每當(dāng)有線程進(jìn)入“加鎖代碼”之后就調(diào)用信號等待命令(此時信號量為0)開始等待,此時其他線程無法進(jìn)入,執(zhí)行完后發(fā)送信號通知(此時信號量為1),其他線程開始進(jìn)入執(zhí)行,如此一來就達(dá)到了線程同步目的。

  dispatch_semaphore_t _semaphore;//定義一個信號量
 
  #pragma mark 請求圖片數(shù)據(jù)
  -(NSData *)requestData:(int )index{
  NSData *data;
  NSString *name;

  # 信號等待
  # 第二個參數(shù):等待時間

  dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
  if (_imageNames.count>0) {
      name=[_imageNames lastObject];
      [_imageNames removeObject:name];
  }
  //信號通知
  dispatch_semaphore_signal(_semaphore);
  if(name){
      NSURL *url=[NSURL URLWithString:name];
      data=[NSData dataWithContentsOfURL:url];
  }
  return data;
  }
  • NSCondition 實(shí)現(xiàn)控制線程通信

NSCondition 的對象實(shí)際上作為一個鎖和一個線程檢查器:鎖主要為了當(dāng)檢測條件時保護(hù)數(shù)據(jù)源,執(zhí)行條件引發(fā)的任務(wù);線程檢查器主要是根據(jù)條件決定是否繼續(xù)運(yùn)行線程,即線程是否被阻塞。單純解決線程同步問題不是NSCondition設(shè)計的主要目的,NSCondition更重要的是解決線程之間的調(diào)度關(guān)系(當(dāng)然,這個過程中也必須先加鎖、解鎖)。NSCondition可以調(diào)用wati方法控制某個線程處于等待狀態(tài),直到其他線程調(diào)用signal(此方法喚醒一個線程,如果有多個線程在等待則任意喚醒一個)或者broadcast(此方法會喚醒所有等待線程)方法喚醒該線程才能繼續(xù)。

  //初始化鎖對象
  _condition=[[NSCondition alloc]init];

  #pragma mark 創(chuàng)建圖片
  -(void)createImageName{
    [_condition lock];
    //如果當(dāng)前已經(jīng)有圖片了則不再創(chuàng)建,線程處于等待狀態(tài)
    if (_imageNames.count>0) {
        NSLog(@"createImageName wait, current:%i",_currentIndex);
        [_condition wait];
    }else{
        NSLog(@"createImageName work, current:%i",_currentIndex);
        //生產(chǎn)者,每次生產(chǎn)1張圖片
        [_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",_currentIndex++]];

        //創(chuàng)建完圖片則發(fā)出信號喚醒其他等待線程
        [_condition signal];
    }
   [_condition unlock];
  }

iOS中的其他鎖

在iOS開發(fā)中,除了同步鎖有時候還會用到一些其他鎖類型,在此簡單介紹一下:

NSRecursiveLock:遞歸鎖,有時候“加鎖代碼”中存在遞歸調(diào)用,遞歸開始前加鎖,遞歸調(diào)用開始后會重復(fù)執(zhí)行此方法以至于反復(fù)執(zhí)行加鎖代碼最終造成死鎖,這個時候可以使用遞歸鎖來解決。使用遞歸鎖可以在一個線程中反復(fù)獲取鎖而不造成死鎖,這個過程中會記錄獲取鎖和釋放鎖的次數(shù),只有最后兩者平衡鎖才被最終釋放。
NSDistributedLock:分布鎖,它本身是一個互斥鎖,基于文件方式實(shí)現(xiàn)鎖機(jī)制,可以跨進(jìn)程訪問。
pthread_mutex_t:同步鎖,基于C語言的同步鎖機(jī)制,使用方法與其他同步鎖機(jī)制類似。

有一張圖片簡單的比較了各種鎖的加解鎖性能:


Paste_Image.png

還有一種方式可以達(dá)到線程同步,那就是同步執(zhí)行

  • 同步執(zhí)行 :我們可以使用多線程的知識,把多個線程都要執(zhí)行此段代碼添加到同一個串行隊列,這樣就實(shí)現(xiàn)了線程同步的概念。當(dāng)然這里可以使用 GCD 和 NSOperation 兩種方案,我都寫出來。

    #GCD
    #需要一個全局變量queue,要讓所有線程的這個操作都加到一個queue中
    dispatch_sync(queue, ^{
        NSInteger ticket = lastTicket;
        [NSThread sleepForTimeInterval:0.1];
        NSLog(@"%ld - %@",ticket, [NSThread currentThread]);
        ticket -= 1;
        lastTicket = ticket;
    });
    
    #NSOperation & NSOperationQueue
    #1. 全局的 NSOperationQueue, 所有的操作添加到同一個queue中
    # 2. 設(shè)置 queue 的 maxConcurrentOperationCount 為 1
    #3. 如果后續(xù)操作需要Block中的結(jié)果,就需要調(diào)用每個操作的waitUntilFinished,阻塞當(dāng)前線程,一直等到當(dāng)前操作完成,才允許執(zhí)行后面的。waitUntilFinished 要在添加到隊列之后!
    
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    NSInteger ticket = lastTicket;
    [NSThread sleepForTimeInterval:1];
    NSLog(@"%ld - %@",ticket, [NSThread currentThread]);
    ticket -= 1;
    lastTicket = ticket;
    }];
    
    [queue addOperation:operation];
    [operation waitUntilFinished];
    #后續(xù)要做的事
    

PS:原子和非原子屬性

atomic 的本意是指屬性的存取方法是線程安全的,并不保證整個對象是線程安全的。比如setter函數(shù)里面改變兩個成員變量,如果你用nonatomic的話,getter可能會取到只更改了其中一個變量時候的狀態(tài),這樣取到的東西會有問題。
atomic:能夠?qū)崿F(xiàn)“單寫多讀”的數(shù)據(jù)保護(hù),同一時間只允許一個線程修改屬性值,但是允許多個線程同時讀取屬性值,在多線程讀取數(shù)據(jù)時,有可能出現(xiàn)“臟”數(shù)據(jù) - 讀取的數(shù)據(jù)可能會不正確。原子屬性是默認(rèn)屬性,atomic(原子屬性)在setter方法內(nèi)部加了一把自旋鎖如果不需要考慮線程安全,要指定 nonatomic。

關(guān)于atomic的實(shí)現(xiàn)最開始的方式如下,我們可以看到其實(shí)現(xiàn)原理也是通過加鎖實(shí)現(xiàn)的。

- (void)setCurrentImage:(UIImage *)currentImage
{
  @synchronized(self) {
  if (_currentImage != currentImage) {
      [_currentImage release];
      _currentImage = [currentImage retain];
      // do something
      }
  }
}
- (UIImage *)currentImage
{
  @synchronized(self) {
      return _currentImage;
  }
}

線程間通信

線程間通信用到的比較多的包括倆個方面: 其他線程向主線程的通信,其他倆個線程間的通信。

  • 從其他線程回到主線程的方法
    我們都知道在其他線程操作完成后必須到主線程更新UI。所以,介紹完所有的多線程方案后,我們來看看有哪些方法可以回到主線程。

    #NSThread
    [self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:NO];
    
    #GCD
     dispatch_async(dispatch_get_main_queue(), ^{
    
    });
    
    #NSOperationQueue
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
    
    }];
    
  • 線程間通信

    線程間通信和進(jìn)程間通信從本質(zhì)上講是相似的。線程間通信就是在進(jìn)程內(nèi)的兩個執(zhí)行流之間進(jìn)行數(shù)據(jù)的傳遞,就像兩條并行的河流之間挖出了一道單向流動長溝,使得一條河流中的水可以流入另一條河流,物質(zhì)得到了傳遞。

    A. performSelect On The Thread

    框架為我們提供了強(qiáng)制在某個線程中執(zhí)行方法的途徑,如果兩個非主線程的線程需要相互間通信,可以先將自己的當(dāng)前線程對象注冊到某個全局的對象中去,這樣相 互之間就可以獲取對方的線程對象,然后就可以使用下面的方法進(jìn)行線程間的通信了,由于主線程比較特殊,所以框架直接提供了在出線程執(zhí)行的方法。

    #在主線程上執(zhí)行操作,例如給UIImageVIew設(shè)置圖片
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait
     //在指定線程上執(zhí)行操作
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg waitUntilDone:(BOOL)wait
    
     #在分線程中下載完圖片后通知主線程更新 UI,通過如下方法,傳遞參數(shù)。
    [self.imageView performSelector:@selector(setImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:NO];
    

    B.Mach Port
    在蘋果的Thread Programming Guide的Run Pool一節(jié)的Configuring a Port-Based Input Source 這一段中就有使用Mach Port進(jìn)行線程間通信的例子。其實(shí)質(zhì)就是父線程創(chuàng)建一個NSMachPort對象,在創(chuàng)建子線程的時候以參數(shù)的方式將其傳遞給子線程,這樣子線程中就可以向這個傳過來的 NSMachPort對象發(fā)送消息,如果想讓父線程也可以向子線程發(fā)消息的話,那么子線程可以先向父線程發(fā)個特殊的消息,傳過來的是自己創(chuàng)建的另一個 NSMachPort對象,這樣父線程便持有了子線程創(chuàng)建的port對象了,可以向這個子線程的port對象發(fā)送消息了。當(dāng)然各自的port對象需要設(shè)置delegate以及schdule到自己所在線程的RunLoop中,這樣來了消息之后,處理port消息的delegate方法會被調(diào)用,你就可以自己處理消息了。

    下面是一處使用源碼:

    #define kMsg1 100
    #define kMsg2 101
    
    - (void)viewDidLoad {
    [super viewDidLoad];
    
    //1. 創(chuàng)建主線程的port
     // 子線程通過此端口發(fā)送消息給主線程
    NSPort *myPort = [NSMachPort port];
    
    //2. 設(shè)置port的代理回調(diào)對象
    myPort.delegate = self;
    
    //3. 把port加入runloop,接收port消息
    [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
    
    NSLog(@"---myport %@", myPort);
    //4. 啟動次線程,并傳入主線程的port
    MyWorkerClass *work = [[MyWorkerClass alloc] init];
    [NSThread detachNewThreadSelector:@selector(launchThreadWithPort:)
                           toTarget:work
                         withObject:myPort];
    }
    - (void)handlePortMessage:(NSMessagePort*)message{
    
    NSLog(@"接到子線程傳遞的消息!%@",message);
    
    //1. 消息id
    NSUInteger msgId = [[message valueForKeyPath:@"msgid"] integerValue];
    
    //2. 當(dāng)前主線程的port
    NSPort *localPort = [message valueForKeyPath:@"localPort"];
    
    //3. 接收到消息的port(來自其他線程)
    NSPort *remotePort = [message valueForKeyPath:@"remotePort"];
    
    if (msgId == kMsg1)
    {
      //向子線的port發(fā)送消息
      [remotePort sendBeforeDate:[NSDate date]
                           msgid:kMsg2
                      components:nil
                            from:localPort
                        reserved:0];
    
    } else if (msgId == kMsg2){
        NSLog(@"操作2....\n");
      }
    }
    

MyWorkerClass

#import "MyWorkerClass.h"
@interface MyWorkerClass() <NSMachPortDelegate> {
    NSPort *remotePort;
    NSPort *myPort;
  }
@end
#define kMsg1 100
#define kMsg2 101

@implementation MyWorkerClass

- (void)launchThreadWithPort:(NSPort *)port {


  @autoreleasepool {

    //1. 保存主線程傳入的port
    remotePort = port;

    //2. 設(shè)置子線程名字
    [[NSThread currentThread] setName:@"MyWorkerClassThread"];

    //3. 開啟runloop
    [[NSRunLoop currentRunLoop] run];

    //4. 創(chuàng)建自己port
    myPort = [NSPort port];

    //5.
    myPort.delegate = self;

    //6. 將自己的port添加到runloop
    //作用1、防止runloop執(zhí)行完畢之后推出
    //作用2、接收主線程發(fā)送過來的port消息
    [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];

    //7. 完成向主線程port發(fā)送消息
    [self sendPortMessage];

    }
}

/**
 *   完成向主線程發(fā)送port消息
 */
- (void)sendPortMessage {

    NSMutableArray *array  =[[NSMutableArray alloc]initWithArray:@[@"1",@"2"]];
//發(fā)送消息到主線程,操作1
[remotePort sendBeforeDate:[NSDate date]
                     msgid:kMsg1
                components:array
                      from:myPort
                  reserved:0];

    //發(fā)送消息到主線程,操作2
    //    [remotePort sendBeforeDate:[NSDate date]
    //                         msgid:kMsg2
    //                    components:nil
    //                          from:myPort
    //                      reserved:0];
}


#pragma mark - NSPortDelegate

/**
 *  接收到主線程port消息
 */
- (void)handlePortMessage:(NSPortMessage *)message
{
    NSLog(@"接收到父線程的消息...\n");

//    unsigned int msgid = [message msgid];
//    NSPort* distantPort = nil;
//
//    if (msgid == kCheckinMessage)
//    {
//        distantPort = [message sendPort];
//
//    }
//    else if(msgid == kExitMessage)
//    {
//        CFRunLoopStop((__bridge CFRunLoopRef)[NSRunLoop currentRunLoop]);
//    }
}
@end

另外Notification在多線程中的使用需要注意

Notification在多線程中只在同一個線程中POST和接收到消息,如果想實(shí)現(xiàn),在一個線程中發(fā)通知,在另一個線程中接收到事件,需要用到通知的 重定向技術(shù),這其中用到了進(jìn)程中的通信。了解更多看這里Notification與多線程。


本文參考文章:
IOS多線程開發(fā)其實(shí)很簡單
iOS線程通信和進(jìn)程通信的例子(NSMachPort和NSTask,NSPipe)
http://www.cnblogs.com/samyangldora/p/4631815.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原文地址 http://www.cnblogs.com/kenshincui/p/3983982.html 大家都...
    怎樣m閱讀 1,421評論 0 1
  • Object C中創(chuàng)建線程的方法是什么?如果在主線程中執(zhí)行代碼,方法是什么?如果想延時執(zhí)行代碼、方法又是什么? 1...
    AlanGe閱讀 1,913評論 0 17
  • 接上文iOS多線程--并行開發(fā)一 4、線程同步 說到多線程就不得不提多線程中的鎖機(jī)制,多線程操作過程中往往多個線程...
    John_LS閱讀 829評論 1 5
  • 你猜AKi是不是一只有個性的狗 ?答案是:是的,準(zhǔn)確的說他是蠢萌蠢萌的。但是,它也有一丁點(diǎn)聰明。 AKi 有...
    大家都叫我佛立西閱讀 542評論 8 6
  • 親愛的,聽說你遠(yuǎn)征去了廣州,一去就是幾個月,最近好嗎? 憑我對你的了解,你不會輕易找人訴苦的。這么多年來,苦辣酸甜...
    章節(jié)閱讀 255評論 0 0

友情鏈接更多精彩內(nèi)容