您的位置:首页 > 移动开发 > IOS开发

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

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

线程安全问题

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

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对象的锁定后,才可以执行被唤醒的线程。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  ios 多线程 线程安全