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

前言

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


線程同步

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

  • 互斥鎖

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

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

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

Paste_Image.png

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

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

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

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

  # 信號等待
  # 第二個參數:等待時間

  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 實現控制線程通信

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

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

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

        //創建完圖片則發出信號喚醒其他等待線程
        [_condition signal];
    }
   [_condition unlock];
  }

iOS中的其他鎖

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

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

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


Paste_Image.png

還有一種方式可以達到線程同步,那就是同步執行

  • 同步執行 :我們可以使用多線程的知識,把多個線程都要執行此段代碼添加到同一個串行隊列,這樣就實現了線程同步的概念。當然這里可以使用 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. 設置 queue 的 maxConcurrentOperationCount 為 1
    #3. 如果后續操作需要Block中的結果,就需要調用每個操作的waitUntilFinished,阻塞當前線程,一直等到當前操作完成,才允許執行后面的。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];
    #后續要做的事
    

PS:原子和非原子屬性

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

關于atomic的實現最開始的方式如下,我們可以看到其實現原理也是通過加鎖實現的。

- (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:^{
    
    }];
    
  • 線程間通信

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

    A. performSelect On The Thread

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

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

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

    下面是一處使用源碼:

    #define kMsg1 100
    #define kMsg2 101
    
    - (void)viewDidLoad {
    [super viewDidLoad];
    
    //1. 創建主線程的port
     // 子線程通過此端口發送消息給主線程
    NSPort *myPort = [NSMachPort port];
    
    //2. 設置port的代理回調對象
    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. 當前主線程的port
    NSPort *localPort = [message valueForKeyPath:@"localPort"];
    
    //3. 接收到消息的port(來自其他線程)
    NSPort *remotePort = [message valueForKeyPath:@"remotePort"];
    
    if (msgId == kMsg1)
    {
      //向子線的port發送消息
      [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. 設置子線程名字
    [[NSThread currentThread] setName:@"MyWorkerClassThread"];

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

    //4. 創建自己port
    myPort = [NSPort port];

    //5.
    myPort.delegate = self;

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

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

    }
}

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

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

    //發送消息到主線程,操作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和接收到消息,如果想實現,在一個線程中發通知,在另一個線程中接收到事件,需要用到通知的 重定向技術,這其中用到了進程中的通信。了解更多看這里Notification與多線程


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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

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