多线程-线程间通信、线程安全问题

前言

说到多线程同步问题就不得不提多线程中的锁机制,多线程操作过程中往往多个线程是并发执行的,同一个资源可能被多个线程同时访问,造成资源抢夺,这个过程中如果没有锁机制往往会造成重大问题。比如常见的车票的销售问题。


线程同步

所谓线程同步就是为了防止多个线程抢夺同一个资源造成的数据安全问题,所采取的一种措施。主要的方法有以下几种:

  • 互斥锁

使用@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之间。

多线程-线程间通信、线程安全问题_第1张图片
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语言的同步锁机制,使用方法与其他同步锁机制类似。

有一张图片简单的比较了各种锁的加解锁性能:


多线程-线程间通信、线程安全问题_第2张图片
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()  {
    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

你可能感兴趣的:(多线程-线程间通信、线程安全问题)