• iOS-----线程同步与线程通信


    线程同步与线程通信

    多线程是有趣的事情,它很容易突然出现”错误情况”,这是由于系统的线程调度具有一定的随机性造成的.不过,即使程序偶然出现问题,那么是由于编程不当所引起的.当使用多个线程来访问同一个数据时,很容易”偶然”出现线程安全问题.

    线程安全问题

    关于线程安全问题,有一个经典的问题:银行取钱的问题.银行取钱的基本流程基本可以分为如下几个步骤.

    1. 用户输入账户、密码、系统判断用户的账户、密码是否匹配。
    2. 用户输入取款金额.
    3. 系统判断账户余额是否大于取款金额.
    4. 如果余额大于取款金额,则取款成功;如果余额小于曲矿金额,则取款失败.

    我们不管检查账户和密码的操作,仅仅模拟了后面3步骤操作.

    下面定义一个账户类,该账户类封装了账户编号和账户余额两个属性.

      1 LCAccount.h
      2 
      3 @interface LCAccount : NSObject
      4 
      5 // 封装账户编号、账户余额两个属性
      6 
      7 @property (nonatomic, copy)NSString* accountNO;// 账户编号
      8 
      9 @property (nonatomic, readonly)CGFloat balance;// 账户余额
     10 
     11 - (id)initWithAccountNo:(NSString*)accountNo  balance:(CGFloat)balance;
     12 
     13 - (void)draw:(CGFloat)drawAmount;
     14 
     15 @end
     16 
     17 该LCAccount类还需要提供一个draw:方法,该方法用于从该账号中取钱。
     18 
     19  LCAccount.m
     20 
     21 @implementation LCAccount
     22 
     23 - (id)initWithAccountNo:(NSString *)aAccount balance:(CGFloat)aBalance
     24 
     25 {
     26 
     27   self = [super init];
     28 
     29   if(self)
     30 
     31 {
     32 
     33   _accountNo = aAccount;
     34 
     35   _balance = aBalance;
     36 
     37 }
     38 
     39 return self;
     40 
     41 }
     42 
     43 // 提供了一个draw方法来完成取钱操作
     44 
     45 - (void)draw:(CGFloat)drawAmount
     46 
     47 {
     48 
     49    // 账户余额大于取钱数目
     50 
     51   if(self.balance >= drawAmount)
     52 
     53   {
     54 
     55 // 吐出钞票
     56 
     57 NSLog(@”%@取钱成功!吐出钱票:%g”, [NSThread currentThread].name , drawAmount);// 58 
     59 //  [NSThread sleepForTimeInterval:0.001 ];
     60 
     61 // 修改余额
     62 
     63 _balance = _balance – drawAmount;
     64 
     65 NSLog(@”	 余额为:%g”, self.balance);
     66 
     67 }
     68 
     69 else
     70 
     71 {
     72 
     73   NSLog(@”%@取钱失败!余额不足!”,  [NSThread currentThread].name);//
     74 
     75 }
     76 
     77 }
     78 
     79 - (NSUInteger) hash
     80 
     81 {
     82 
     83   return [self.accountNo hash];
     84 
     85 }
     86 
     87 - (BOOL)isEqual:(id)anObject
     88 
     89 {
     90 
     91   if(self == anObject)
     92 
     93    return YES;
     94 
     95   if(anObject != nil
     96 
     97 && [anObject class] == [LCAccount class])
     98 
     99 {
    100 
    101   LCAccount* target = (LCAccount*)anObject;
    102 
    103   return [target.accountNo isEqualToString:self.accountNo];
    104 
    105 }
    106 
    107 return NO;
    108 
    109 }
    110 
    111 @end
    112 
    113  
    114 
    115 LCViewController.m
    116 
    117 @implementation LCViewController
    118 
    119 LCAccount* account;
    120 
    121 - (void)viewDidLoad
    122 
    123 {
    124 
    125   [super viewDidLoad];
    126 
    127   // 创建一个账号
    128 
    129   account = [[LCAccount alloc] initWithAccountNo:@”321231” balance: 1000.0 ];
    130 
    131 }
    132 
    133 - (IBAction)draw:(id)sender
    134 
    135 {
    136 
    137    // 创建第1个线程对象
    138 
    139   NSThread* thread1 = [[NSThread alloc] initWithTarget:self
    140 
    141 selector:@selector(drawMethod:)
    142 
    143 object:[NSNumber numberWithInt:800]];
    144 
    145 // 创建第2个线程对象
    146 
    147   NSThread* thread2 = [[NSThread alloc] initWithTarget:self
    148 
    149 selector:@selector(drawMethod:)
    150 
    151 object:[NSNumber numberWithInt:800]];
    152 
    153 // 启动两条线程
    154 
    155 [thread1 start];
    156 
    157 [thread2 start];
    158 
    159  
    160 
    161 }
    162 
    163 - (void)drawMethod:(NSNumber *)drawAmount
    164 
    165 {
    166 
    167    // 直接调用accont对象的draw方法来执行取钱操作
    168 
    169   [account draw:drawAmount.doubleValue];
    170 
    171 }
    172 
    173 @end

    按照正常的执行逻辑,应该是第1个线程可以取到钱,第2线程显示”余额不足”.但上图所示的运行结果并不是期望的结(不过也有可能看到运行正确的结果),这正是多线程编程突然出现的”偶然”错误----因为线程调度的不确定性.

    使用@synchronized实现同步

    为了解决”线程执行体的方法不具备同步安全性”的问题,Objective—C的多线程支持引入了同步,使用同步的通用方法就是@synchronized修饰代码块,被@synchornized修饰的代码块可简称为同步代码块.

    同步代码块的语法如下:

    @synchronized(obj)

    {

      …

       // 此处的代码就是同步代码块

    }

    上面语法格式中,@synchronized后面括号里的obj就是同步监视器.上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定.

    注意:任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对同步监视器的锁定.虽然Objective-C允许使用任何对象作为同步监视器,但想一下同步监视器的目的----阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器.

    对于上面的取钱模拟程序,我们应该考虑使用账户(LCAccount对象)作为同步监视器.只要我们把LCAccount类的draw:方法修改如下形式即可.

     1 // 提供一个线程安全的draw方法来完成取钱操作
     2 
     3 - (void) draw:(CGFloat)drawAmount
     4 
     5 {
     6 
     7   // 使用self作为同步监视器,任何线程进入下面的同步代码块之前
     8 
     9   // 必须先获得self账户的锁定----其他线程无法获得锁,也就无法修改它
    10 
    11   // 这种做法符合”加锁 →修改→释放锁”的逻辑
    12 
    13   @synchronized(self)
    14 
    15 {
    16 
    17    // 账户余额大于取钱数目
    18 
    19    if(self.balance >= drawAmount)
    20 
    21    {
    22 
    23      // 吐出钞票
    24 
    25      NSLog(@”%@取钱成功! 吐出钞票:%g”, [NSThread currentThread].name , drawAmount);
    26 
    27 [NSThread  sleepForTimeInterval:0.001];
    28 
    29 // 修改余额
    30 
    31 _balance = _balance – drawAmount;
    32 
    33 NSLog(@”	yue为: %g”, self.balance);
    34 
    35 }
    36 
    37 else
    38 
    39 {
    40 
    41   NSLog(@”%@取钱失败!余额不足!”, [NSThread currentThread].name);
    42 
    43 }
    44 
    45 }// 同步代码块结束,该线程释放同步锁
    46 
    47 }

    说明

    上面程序使用@synchronized将draw:方法的方法体修改成同步代码块,该同步代码块的同步监视器是LCAccount对象本声,这样做法符合”加锁→修改→释放锁”的逻辑,任何线程在修改制定资源之前,首先都要对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成后,释放对该资源的锁定.通过这种方式就可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(也称为临界区),所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全性.

    特征

    通过这种方式可以非常方便地实现线程安全的类,线程安全的类具有如下特征.

      该类的对象可以被多个线程安全地访问.

      每个线程调用该对象的任意方法之后都将得到正确结果.

      每个线程调用该对象的任意方法之后,该对象依然保持合理状态.

    减少线程安全的负面

    1. 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。例如上面的LCAccount类中的accountNo属性就无须同步,所以程序只对draw方法进行同步控制。
    2. 如果可变类有两种运行环境:单线程和多线程环境,则应该为该可变类型提供两种版本---线程不安全版本和线程安全版本,在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。

    释放对同步监视器的锁定

      任何线程在进入同步代码块之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定。

      当前线程的同步代码执行结束,当前线程即释放同步监视器。

      当线程在同步代码块中遇到goto、return终止了该代码块、该方法的继续执行时,当前线程将会释放同步监视器。

      当线程在同步代码块中出现错误,导致该代码块异常结束时,将会释放同步监视器。

    典型地,当程序调用NSThread的sleepXxx方法暂停线程时,线程不会释放同步监视器。

    同步琐(NSLock)

    Foundation还提供了NSLock,它通过显式定义同步锁对象来实现同步,在这种机制下,同步锁使用NSLock对象充当。

       NSLock是控制多个线程对共享资源进行访问的工具,通常锁定提供了对共享资源的独占访问,每次只能有一个线程对NSLock对象加锁,线程开始访问共享资源之前应先获得NSLock对象。

       在实现线程安全的控制中,使用该NSLock对象可以显式地加锁、释放锁。通常使用NSLock的代码格式如下:

     1   @implementation X
     2 
     3   NSLock *lock 4 
     5   - (id)init
     6 
     7 {
     8 
     9    self = [super init];
    10 
    11    if(self)
    12 
    13 {
    14 
    15   lock = [[NSLock alloc] init];
    16 
    17 }
    18 
    19 return self;
    20 
    21 }
    22 
    23 // 定义需要保证线程安全的方法
    24 
    25 - (void) m
    26 
    27 {
    28 
    29    [lock lock];
    30 
    31    // 需要保证线程安全的代码
    32 
    33    // … method body
    34 
    35   [lock unlock];
    36 
    37 }
    38 
    39 40 
    41 @end

    通过使用NSLock对象,我们可以把LCAccount类改为如下形式,它依然是线程安全的.

     1 LCAccount.m
     2 
     3 @implementation LCAccount
     4 
     5 NSLock *lock;
     6 
     7 - (id)init
     8 
     9 {
    10 
    11   self = [super init];
    12 
    13   if(self)
    14 
    15   {
    16 
    17      lock = [[NSLock alloc] init];
    18 
    19 }
    20 
    21 return self;
    22 
    23 }
    24 
    25 - (id)initWithAccountNo:(NSString*)aAccount balance:(CGFloat)aBalance
    26 
    27 {
    28 
    29   self = [super init];
    30 
    31   if(self)
    32 
    33   {
    34 
    35     lock = [[NSLock alloc] init];
    36 
    37     _accountNo = aAccount;
    38 
    39     _balance = aBalance;
    40 
    41 }
    42 
    43 return self;
    44 
    45 }
    46 
    47 // 提供一个线程安全的draw方法来完成取钱操作
    48 
    49 - (void)draw:(CGFloat)drawAmount
    50 
    51 {
    52 
    53    // 显式锁定lock对象
    54 
    55    [lock lock];
    56 
    57    // 账户余额大于取钱数目
    58 
    59    if(self.balance > = drawAmount)
    60 
    61   {
    62 
    63     // 吐出钞票
    64 
    65     NSLog(@”%@取钱成功!吐出钱票:%g”, [NSThread currentThread].name, drawAmount);
    66 
    67     [NSThread sleepForTimeInterval:0.001];
    68 
    69     // 修改余额
    70 
    71    _balance = _balance – drawAmount;
    72 
    73    NSLog(@”	余额为:%g”, self.balance);
    74 
    75 }
    76 
    77 else
    78 
    79 {
    80 
    81    NSLog(@”%@取钱失败!余额不足!”, [NSThread  currentThread].name);
    82 
    83 }
    84 
    85  // 释放lock的锁定
    86 
    87 [lock unlock];
    88 
    89 }
    90 
    91 // 省略hash和isEqual:方法
    92 
    93 94 
    95 @end

    定义了一个NSLock对象,程序中实现draw:方法时,进入方法开始执行后立即请求对NSLock对象进行加锁,当执行完draw:方法的取钱逻辑之后,程序释放对NSLock对象的锁定.

    提示:使用NSLock与使用同步方式有点相似,只是使用NSLock时显式使用NSLock对象作为同步锁,而使用同步代码块时系统显式使用某个对象作为同步监视器,同样都符合”加锁->修改->释放锁”的操作模式,而且使用NSLock对象时每个NSLock对象对应一个LCAccount对象,一样可以保证对于同一个LCAccount对象,同一时刻只能有一个线程进入临界区.

    使用NSCondition控制线程通信

        当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,但我们可以通过一些机制来保证线程协调运行,也就是处理线程之间的通信。

        Foundation提供了一个NSCondition类来处理线程通信,NSCondition实现了NSLocking协议,因此也可以调用lock、unlock来实现线程同步。NSCondition可以让那些已经锁定NSCondition对象却无法继续执行的线程释放NSCondition对象,NSCondition对象也可以唤醒其他处于等待状态的线程。

    NSCondition类提供了如下3个方法

    - wait:

    该方法导致当前线程一直等待,直到其他线程调用该NSCondition的signal方法或broadcast方法来唤醒该线程。wait方法有一个变体:- (BOOL)waitUntilDate:(NSDate *)limiteout,用于控制等待到指定时间点,如果到了该时间点,该线程将会被自动唤醒。

    - signal:

    唤醒在此NSCondition对象上等待的单个线程。如果所有线程都在该NSCondition对象上等待,则会选择唤醒其中一个线程,选择是任意性的。只有当前线程放弃对该NSCondition对象的锁定后(使用wait方法),才可以执行被唤醒的线程。

    - broadcast:

    唤醒在此NSCondition对象上等待的所有线程,只有当前线程放弃对该NSCondition对象的锁定后,才可以执行被唤醒的线程。

    /*

    本程序中LCACcount使用NSCondition对象来控制同步,并使用NSCondition对象来控制线程的通信。程序通过一个旗标来标识账户中是否已有存款,当旗标为“NO”时,表明账户中没有存款,存款者线程可以向下执行,当存款者把钱存入账户后,将旗标设为“YES”,并调用signal或broadcast方法来唤醒其他线程;当存款者线程进入线程体后,如果旗标为“YES”,就调用wait方法让该线程等待。

    当旗标为“YES”时,表明账户中已经存入了钱,则取钱者线程可以向下执行,当取钱者把钱从账户中取出后,将旗标设为“NO”,并调用signal或broadcast方法来唤醒其他线程;当存款者线程进入线程体后,如果旗标为“YES”,就调用wait方法让该线程等待。

    上面这种模式可以推而广之,存钱的线程可以称为生产者,而取钱的线程则可以称为消费者,生产者与消费者之间通过NSCondition进行通信,从而实现良好的协调运行。

    本程序通过为LACcount类提供draw:和deposit:两个方法,分别对应该账户的取钱、存钱等操作,因为这两个方法可能需要并发修改LCAccount类的balance成员变量,所以这两个方法都使用NSCondition来控制线程同步。除此之外,这两个方法还使用了wait、broadcast来控制线程的通信。

    */

      1 LCAccount.m
      2 
      3 @implementation LCAccount
      4 
      5 NSCondition* cond;
      6 
      7 BOOL flag;
      8 
      9 - (id)init
     10 
     11 {
     12 
     13 self = [super init];
     14 
     15 if(self)
     16 
     17 {
     18 
     19 cond = [[NSCondition alloc] init];
     20 
     21 }
     22 
     23 return self;
     24 
     25 }
     26 
     27 -(id)initWithAccountNo:(NSString*)aAccount balance:(CGFloat)aBalance
     28 
     29 {
     30 
     31 self = [super init];
     32 
     33 if(self)
     34 
     35 {
     36 
     37 cond = [[NSCondition alloc] init];
     38 
     39 _accountNo = aAccount;
     40 
     41 _balance = aBalance;
     42 
     43 }
     44 
     45 return self;
     46 
     47 }
     48 
     49 // 提供一个线程安全的draw方法来完成取钱操作
     50 
     51 - (void)draw:(CGFloat)drawAmount
     52 
     53 {
     54 
     55 // 加锁
     56 
     57 [cond lock];
     58 
     59 // 如果flag为NO,则表明账户中还没有人存钱进去,取钱方法阻塞
     60 
     61 if(!flag)
     62 
     63 {
     64 
     65 [cond wait];
     66 
     67 }
     68 
     69 else
     70 
     71 {
     72 
     73 // 执行取钱操作
     74 
     75 NSLog(”%@ 取钱:%g”, [NSThread currentThread].name,    drawAmount);
     76 
     77 _balance -= drawAmount;
     78 
     79 NSLog(@”账户余额为:”%g”, self.balance);
     80 
     81 // 将标识账户是否已有存款的旗标设为NO
     82 
     83 flag = NO;
     84 
     85 // 唤醒其他线程
     86 
     87 [cond broadcast];
     88 
     89 }
     90 
     91 [cond unlock];
     92 
     93 }
     94 
     95 - (void)deposit:(CGFloat)depositAmount
     96 
     97 {
     98 
     99 [cond lock];
    100 
    101 // 如果flag为YES,则表明账户中已有人存钱进去了,存钱方法阻塞
    102 
    103 if(flag)// 1
    104 
    105 {
    106 
    107 [cond wait];
    108 
    109 }
    110 
    111 else
    112 
    113 {
    114 
    115 // 执行存款操作
    116 
    117 NSLog(@”%@ 存款:%g”, [NSThread currentThread].name, depositAmount);
    118 
    119 _balance += depositAmount;
    120 
    121 NSLog(@”账户余额为:%g”, self.balance);
    122 
    123 // 将标识账户是否已有存款的旗标设为YES
    124 
    125 flag = YES;
    126 
    127 // 唤醒其他线程
    128 
    129 [cond broadcast];
    130 
    131 }
    132 
    133 [cond unlock];
    134 
    135 }
    136 
    137 // 此处省略了hash和isEqual:方法
    138 
    139 140 
    141 @end

    上面程序中的代码使用了wait和broadcast进行控制,对存款线程而言,当程序进入deposit:方法后,如果flag为”YES”,则表明账户中已有存款,程序调用wait方法阻塞;否则,程序向下执行存款操作,当存款操作执行完成后,系统将flag设为“YES”,然后调用broadcast来唤醒其他被阻塞的线程----如果系统中有存款者线程,存款者线程也会被唤醒,但该存款者线程执行到“1”号代码处时再次进入阻塞状态,只有执行draw:方法的取钱者线程才可以向下执行,同理,取钱者线程的执行流程也是如此。

    /*

    程序中的存款者线程循环100次重复村矿,而取钱者线程则循环100次重复取钱,存款者线程和取钱者线程分别调用LCAccount对象的deposit:、draw:方法来实现。

    */

     1 ViewController.m
     2 
     3 @implementation ViewController
     4 
     5 LCAccount* account;
     6 
     7 - (void)viewDidLoad
     8 
     9 {
    10 
    11    [super viewDidLoad];
    12 
    13    // 创建一个账号
    14 
    15   account = [[LCAccount alloc] initWithAccountNo:@”321321” balance:1000.0];
    16 
    17 }
    18 
    19 - (IBAction)depositDraw:(id)sender
    20 
    21 {
    22 
    23    // 创建 启动3个存钱者线程
    24 
    25    [NSThread detachNewThreadSelector:@selector(drawMethod:) toTarget:self withObject:[NSNumber numberWithDouble:800.0]];
    26 
    27    [NSThread detachNewThreadSelector:@selector(drawMethod:) toTarget:self withObject:[NSNumber numberWithDouble:800.0]];
    28 
    29 [NSThread detachNewThreadSelector:@selector(drawMethod:) toTarget:self withObject:[NSNumber numberWithDouble:800.0]];
    30 
    31    // 创建,启动取钱者线程
    32 
    33    [NSThread detachNewThreadSelector:@selector(depositMethod:) toTarget:self withObject:[NSNumber numberWithDouble:800.0]];
    34 
    35 }
    36 
    37 - (void)drawMethod:(NSNumber*)drawAmount
    38 
    39 {
    40 
    41    [NSThread currentThread].name = @”甲”;
    42 
    43    // 重复100次执行取钱操作
    44 
    45    for(int i = 0;i < 100; i++)
    46 
    47    {
    48 
    49      [account draw:drawAmount.doubleValue];
    50 
    51 }
    52 
    53 }
    54 
    55 - (void)depositMethod:(NSNumber*) depositAmount
    56 
    57 {
    58 
    59   [NSThread currentThread].name = @”乙”;
    60 
    61   // 重复100次执行存款操作
    62    for(int i = 0; i < 100; i++)
    63   {
    64      [account deposit:depositAmount.doubleValue];
    65    }
    66 }
    67 @end
  • 相关阅读:
    类的静态成员
    透彻分析C/C++中memset函数
    排序中的qsort和sort
    NOIP2020 T4微信步数
    NOIP2020 T3移球游戏
    GMOJ 6898. 【2020.11.27提高组模拟】第二题
    虚树学习笔记
    GMOJ 6860. 【2020.11.14提高组模拟】鬼渊传说(village)
    CSP-S 2020 T3函数调用
    CSP-S 2020 T4贪吃蛇
  • 原文地址:https://www.cnblogs.com/congli0220/p/5008083.html
Copyright © 2020-2023  润新知