IOS多线程系统学习之线程同步与线程通信

多线程编程是有趣的事情,它很容易突然出现“错误情况”,这是由于系统的线程调度具有一定的随机性造成的。不过,即使程序偶然出现“错误情况”,这是由于系统的线程调度具有一定的随机性造成的。不过,即使程序偶然出现问题,那也是由于编程不当所引起的,当使用多个线程来访问同一个数据时,很容易“偶然”出现线程安全问题。
线程安全问题
关于线程安全问题,有一个景点的问题:银行取钱的问题。银行取钱基本可以分为如下几个步骤。
1,用户输入账号,密码,系统判断用户的 账号,密码是否匹配。
2,用户输入取款金额。
3,判断用户账户余额是否大于取款金额。
4,如果余额大于取款金额,则取款成功;如果余额小于取款金额,则取款失败。

演示代码:
账户类接口代码:

#import <Foundation/Foundation.h>

@interface FKAccount : NSObject
// 封装账户编号、账户余额两个属性
@property (nonatomic, copy) NSString* accountNo;
@property (nonatomic, readonly) CGFloat balance;
- (id)initWithAccountNo:(NSString*)accountNo
                balance:(CGFloat)balance;
- (void) draw:(CGFloat)drawAmount;
@end
#import "FKAccount.h"

@implementation FKAccount
- (id)initWithAccountNo:(NSString*)aAccount
                balance:(CGFloat)aBalance
{
    self = [super init];
    if (self) {
        _accountNo = aAccount;
        _balance = aBalance;
    }
    return self;
}
// 提供一个draw方法来完成取钱操作
- (void) draw:(CGFloat)drawAmount
{
    // 账户余额大于取钱数目
    if (self.balance >= drawAmount)
    {
        // 吐出钞票
        NSLog(@"%@取钱成功!吐出钞票:%g", [NSThread currentThread].name
            , drawAmount);
//      [NSThread sleepForTimeInterval:0.001];
        // 修改余额
        _balance = _balance - drawAmount;
        NSLog(@"\t余额为: %g" , self.balance);
    }
    else
    {
        NSLog(@"%@取钱失败!余额不足!", [NSThread currentThread].name);
    }
}
- (NSUInteger) hash
{
    return [self.accountNo hash];
}
- (BOOL)isEqual:(id)anObject
{
    if(self == anObject)
        return YES;
    if (anObject != nil
        && [anObject class] == [FKAccount class])
    {
        FKAccount* target = (FKAccount*)anObject;
        return [target.accountNo isEqualToString:self.accountNo];
    }
    return NO;
}
@end
#import "FKViewController.h"
#import "FKAccount.h"

@interface FKViewController ()

@end

@implementation FKViewController
FKAccount* account;
- (void)viewDidLoad
{
    [super viewDidLoad];
    // 创建一个账号
    account = [[FKAccount alloc] initWithAccountNo:@"321231" balance: 1000.0];
}
- (IBAction)draw:(id)sender
{


// 创建第1个线程对象 NSThread* thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(drawMethod:) object:[NSNumber numberWithInt:800]]; // 创建第2个线程对象 NSThread* thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(drawMethod:) object:[NSNumber numberWithInt:800]]; // 启动2条线程 [thread1 start]; [thread2 start];

}
- (void) drawMethod:(NSNumber*) drawAmount
{
    // 直接调用account对象的draw方法来执行取钱
    [account draw:drawAmount.doubleValue];
}

运行结果1如下:

2015-12-18 10:49:00.942 DrawTest[21771:4480081] 取钱成功!吐出钞票:800
2015-12-18 10:49:00.942 DrawTest[21771:4480080] 取钱成功!吐出钞票:800
2015-12-18 10:49:00.943 DrawTest[21771:4480081]     余额为: 200
2015-12-18 10:49:00.943 DrawTest[21771:4480080]     余额为: -600

按照正常的执行逻辑,应该是第1个线程可以取到钱,第2个线程显示”余额不足”。但运行结果并不是期望的结果(不过也有可能看到运行正确的结果),这正是多线程编程突然出现的“偶然”错误——因为线程调度的不确定性。我们来分析运行结果,账户余额只有1000时取出了1600,而且账户余额出现了负值,这不是银行希望的结果。虽然上面程序是人为地使用Thread.sleep(1)来强制线程调度的切换,但这种切换也是完全有可能发生的——100000次操作只要有1次出现了错误,那就是编程错误引起的。

使用@synchronized实现同步
之所以会出现运行结果1所示的结果,是因为线程执行体的方法不具备同步安全性——程序中有两个并发线程在修改FKAccount对象。而且系统恰好在红色字体代码处执行线程切换,切换给另一个修改FKAccount对象的编程,所以就出现了问题。
为了解决这个问题,Object-C的多线程支持引入了同步,使用同步的通用方法就是@synchronized修饰代码块,被@synchronsized修饰的代码块可以建成为 同步代码块。同步代码块的语法格式如下:

@synchronsized(obj){
...
//此处的代码就是同步代码块
}

修改后的代码如下:


// 提供一个线程安全的draw方法来完成取钱操作
- (void) draw:(CGFloat)drawAmount
{
    // 使用self作为同步监视器,任何线程进入下面同步代码块之前,
    // 必须先获得对self账户的锁定——其他线程无法获得锁,也就无法修改它
    // 这种做法符合:“加锁 → 修改 → 释放锁”的逻辑    
    @synchronized(self)
    {
        // 账户余额大于取钱数目
        if (self.balance >= drawAmount)
        {
            // 吐出钞票
            NSLog(@"%@取钱成功!吐出钞票:%g", [NSThread currentThread].name
                , drawAmount);
            [NSThread sleepForTimeInterval:0.001];
            // 修改余额
            _balance = _balance - drawAmount;
            NSLog(@"\t余额为: %g" , self.balance);
        }
        else
        {
            NSLog(@"%@取钱失败!余额不足!", [NSThread currentThread].name);
        }
    }//同步代码块结束,该线程释放同步锁
}

运行结果2如下:

这里写图片描述
大家可以看到结果正常了,达到了我们想要的目的。
这里写图片描述
通过这种方式可以非常方便地实现线程安全的类,线程安全的类具有如下特征:
1,该类的对象可以被多个线程安全地访问。
2,每个线程调用该对象的任意方法之后都将得到正确结果。
3,每个线程调用该对象的任意方法之后,该对象依然保持合理状态。
Foundation框架中很多类都有可变和不可变两种版本,比如NSString,NSArray就是不可变类,而NSMutableString,NSMutableArray就是可变类。其中不可变类总是线程安全的,因为它的对象不可改变。但可变类的对象需要额外的方法来保证其线程安全。例如上面的FKAccount类就是一个可变类,它的accountNo和balance两个属性都可变,当两个线程同时修改FKAccount对象的balance时,程序就可能出现异常。
可变类的线程安全是以降低程序的运行效率作为代价的。为了减少线程安全所带来的负面影响,程序可以采用如下策略。
1,不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。例如上面的FKAccount类中的accountNo属性就无须同步,所以程序只对draw方法进行同步控制。
2,如果可变类有两种运行环境:但线程环境和多线程环境,则应该为该可变类提供两种版本——线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。

释放对同步监视器的锁定
任何线程在进入同步代码之前,必须先获得对同步代码监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定。
1,当前线程的同步代码块之行结束,当前线程即释放同步监视器。
2,当线程在同步代码块中遇到goto,return终止了该代码块,该方法的继续执行时,当前线程将会释放同步监视器。
3,当线程在同步代码块中出现了错误,导致该代码异常结束时,将会释放同步监视器。典型地,当程序调用 NSThread的sleepXxx方法暂停线程时,线程不会释放同步监视器。

同步锁
Foundation 还提供了 NSLock,它通过显式定义同步锁来实现同步,在这种机制下,同步锁使用NSLock对象充当。
NSLock是控制多个线程对共享资源进行访问的工具。通常提供了对共享资源的独占访问,每次只能有一个线程对NSLock对象加锁,线程开始访问共享资源前,应先获得NSLock对象。
演示代码如下:

// 提供一个线程安全的draw方法来完成取钱操作
- (void) draw:(CGFloat)drawAmount
{
    // 显式锁定lock对象
    [lock lock];
    // 账户余额大于取钱数目
    if (self.balance >= drawAmount)
    {
        // 吐出钞票
        NSLog(@"%@取钱成功!吐出钞票:%g", [NSThread currentThread].name
            , drawAmount);
        [NSThread sleepForTimeInterval:0.001];
        // 修改余额
        _balance = _balance - drawAmount;
        NSLog(@"\t余额为: %g" , self.balance);
    }
    else
    {
        NSLog(@"%@取钱失败!余额不足!", [NSThread currentThread].name);
    }
    // 释放lock的锁定
    [lock unlock];
}

运行结果3如下:
这里写图片描述
这里写图片描述
使用NSCondition控制线程通信
当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,但我们可以通过一些机制来保证线程协调运行,也就是处理线程之间的通信。
Foundation提供了一个NSCondition类来处理线程通信,NSCondition实现了NSLocking协议, 因此也可以调用lock,unlock来实现线程同步。除此之外,NSCondition可以让那些已经锁定NSCondition对象却无法继续执行的线程释放NSCondition对象,NSCondition对象也可以唤醒其他处于等待状态的线程。
NSCondition类提供了如下3个方法。
1,- wait :该方法导致当前线程一直等待,直到其他线程调用该NSCondition的signal方法或broadcast方法来唤醒该线程。wait 方法有一个变体: -(BOOL)waitUntilDate:(NSDate *)limiteout, 用于控制等待到指定时间点,如果到了该时间点,该线程将会被自动唤醒。
2, - signal : 唤醒在此NSCondition对象上等待的单个线程。如果所有线程都在该NSCondition对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该NScondition对象的锁定后(使用wait 方法),才可以执行被唤醒的线程。
3,-broadcast : 唤醒在此NSCondition对象上等待的所有线程。只有当前线程放弃对该NSCondition对象的锁定后,才可以执行被唤醒的线程。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值