线程同步与线程通信

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

线程安全问题

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

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

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

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

 LCAccount.h

 @interface LCAccount : NSObject

 // 封装账户编号、账户余额两个属性

 @property (nonatomic, copy)NSString* accountNO;// 账户编号

 @property (nonatomic, readonly)CGFloat balance;// 账户余额

 - (id)initWithAccountNo:(NSString*)accountNo  balance:(CGFloat)balance;

 - (void)draw:(CGFloat)drawAmount;

 @end

 该LCAccount类还需要提供一个draw:方法,该方法用于从该账号中取钱。

  LCAccount.m

 @implementation LCAccount

 - (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] == [LCAccount class])

 {

   LCAccount* target = (LCAccount*)anObject;

   return [target.accountNo isEqualToString:self.accountNo];

 }

 return NO;

 }

 @end

 LCViewController.m

 @implementation LCViewController

 LCAccount* account;

 - (void)viewDidLoad

 {

   [super viewDidLoad];

   // 创建一个账号

   account = [[LCAccount alloc] initWithAccountNo:@”” balance: 1000.0 ];

 }

 - (IBAction)draw:(id)sender

 {

    // 创建第1个线程对象

   NSThread* thread1 = [[NSThread alloc] initWithTarget:self

 selector:@selector(drawMethod:)

 object:[NSNumber numberWithInt:]];

 // 创建第2个线程对象

   NSThread* thread2 = [[NSThread alloc] initWithTarget:self

 selector:@selector(drawMethod:)

 object:[NSNumber numberWithInt:]];

 // 启动两条线程

 [thread1 start];

 [thread2 start];

 }

 - (void)drawMethod:(NSNumber *)drawAmount

 {

    // 直接调用accont对象的draw方法来执行取钱操作

   [account draw:drawAmount.doubleValue];

 }

 @end

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

使用@synchronized实现同步

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

同步代码块的语法如下:

@synchronized(obj)

{

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

}

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

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

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

 // 提供一个线程安全的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(@”\tyue为: %g”, self.balance);

 }

 else

 {

   NSLog(@”%@取钱失败!余额不足!”, [NSThread currentThread].name);

 }

 }// 同步代码块结束,该线程释放同步锁

 }

说明

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

特征

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

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

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

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

减少线程安全的负面

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

释放对同步监视器的锁定

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

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

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

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

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

同步琐(NSLock)

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

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

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

   @implementation X

   NSLock *lock;

   - (id)init

 {

    self = [super init];

    if(self)

 {

   lock = [[NSLock alloc] init];

 }

 return self;

 }

 // 定义需要保证线程安全的方法

 - (void) m

 {

    [lock lock];

    // 需要保证线程安全的代码

    // … method body

   [lock unlock];

 }

 …

 @end

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

 LCAccount.m

 @implementation LCAccount

 NSLock *lock;

 - (id)init

 {

   self = [super init];

   if(self)

   {

      lock = [[NSLock alloc] init];

 }

 return self;

 }

 - (id)initWithAccountNo:(NSString*)aAccount balance:(CGFloat)aBalance

 {

   self = [super init];

   if(self)

   {

     lock = [[NSLock alloc] init];

     _accountNo = aAccount;

     _balance = aBalance;

 }

 return self;

 }

 // 提供一个线程安全的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];

 }

 // 省略hash和isEqual:方法

 …

 @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来控制线程的通信。

*/

 LCAccount.m

 @implementation LCAccount

 NSCondition* cond;

 BOOL flag;

 - (id)init

 {

 self = [super init];

 if(self)

 {

 cond = [[NSCondition alloc] init];

 }

 return self;

 }

 -(id)initWithAccountNo:(NSString*)aAccount balance:(CGFloat)aBalance

 {

 self = [super init];

 if(self)

 {

 cond = [[NSCondition alloc] init];

 _accountNo = aAccount;

 _balance = aBalance;

 }

 return self;

 }

 // 提供一个线程安全的draw方法来完成取钱操作

 - (void)draw:(CGFloat)drawAmount

 {

 // 加锁

 [cond lock];

 // 如果flag为NO,则表明账户中还没有人存钱进去,取钱方法阻塞

 if(!flag)

 {

 [cond wait];

 }

 else

 {

 // 执行取钱操作

 NSLog(”%@ 取钱:%g”, [NSThread currentThread].name,    drawAmount);

 _balance -= drawAmount;

 NSLog(@”账户余额为:”%g”, self.balance);

 // 将标识账户是否已有存款的旗标设为NO

 flag = NO;

 // 唤醒其他线程

 [cond broadcast];

 }

 [cond unlock];

 }

 - (void)deposit:(CGFloat)depositAmount

 {

 [cond lock];

 // 如果flag为YES,则表明账户中已有人存钱进去了,存钱方法阻塞

 if(flag)//

 {

 [cond wait];

 }

 else

 {

 // 执行存款操作

 NSLog(@”%@ 存款:%g”, [NSThread currentThread].name, depositAmount);

 _balance += depositAmount;

 NSLog(@”账户余额为:%g”, self.balance);

 // 将标识账户是否已有存款的旗标设为YES

 flag = YES;

 // 唤醒其他线程

 [cond broadcast];

 }

 [cond unlock];

 }

 // 此处省略了hash和isEqual:方法

 …

 @end

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

/*

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

*/

 ViewController.m

 @implementation ViewController

 LCAccount* account;

 - (void)viewDidLoad

 {

    [super viewDidLoad];

    // 创建一个账号

   account = [[LCAccount alloc] initWithAccountNo:@”” balance:1000.0];

 }

 - (IBAction)depositDraw:(id)sender

 {

    // 创建 启动3个存钱者线程

    [NSThread detachNewThreadSelector:@selector(drawMethod:) toTarget:self withObject:[NSNumber numberWithDouble:800.0]];

    [NSThread detachNewThreadSelector:@selector(drawMethod:) toTarget:self withObject:[NSNumber numberWithDouble:800.0]];

 [NSThread detachNewThreadSelector:@selector(drawMethod:) toTarget:self withObject:[NSNumber numberWithDouble:800.0]];

    // 创建,启动取钱者线程

    [NSThread detachNewThreadSelector:@selector(depositMethod:) toTarget:self withObject:[NSNumber numberWithDouble:800.0]];

 }

 - (void)drawMethod:(NSNumber*)drawAmount

 {

    [NSThread currentThread].name = @”甲”;

    // 重复100次执行取钱操作

    for(int i = ;i < ; i++)

    {

      [account draw:drawAmount.doubleValue];

 }

 }

 - (void)depositMethod:(NSNumber*) depositAmount

 {

   [NSThread currentThread].name = @”乙”;

   // 重复100次执行存款操作
for(int i = ; i < ; i++)
{
[account deposit:depositAmount.doubleValue];
}
}
@end

iOS-----线程同步与线程通信的更多相关文章

  1. 关于Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇高质量的博文)

    Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇质量高的博文) 前言:在学习多线程时,遇到了一些问题,这里我将这些问题都分享出来,同时也分享了几篇其他博客主的博客,并且将我个人的理解也分享 ...

  2. iOS开发——高级篇——线程同步、线程依赖、线程组

    前言 对于iOS开发中的网络请求模块,AFNet的使用应该是最熟悉不过了,但你是否把握了网络请求正确的完成时机?本篇文章涉及线程同步.线程依赖.线程组等专用名词的含义,若对上述名词认识模糊,可先进行查 ...

  3. C# 多线程编程第二步——线程同步与线程安全

    上一篇博客学习了如何简单的使用多线程.其实普通的多线程确实很简单,但是一个安全的高效的多线程却不那么简单.所以很多时候不正确的使用多线程反倒会影响程序的性能. 下面先看一个例子 : class Pro ...

  4. Java线程同步和线程通信

    一.线程同步 当多个线程访问同一个数据时,非常容易出现线程安全问题.这时候就需要用线程同步. 不可变类总是线程安全的,因为它的对象状态是不可改变的,但可变类对象需要额外的方法来保证线程安全. 1.同步 ...

  5. Java并发——线程安全、线程同步、线程通信

    线程安全 进程间"共享"对象 多个“写”线程同时访问对象. 例:Timer实例的num成员,即add()方法是用的次数.即Timer实例是资源对象. class TestSync ...

  6. Java多线程(二) —— 线程安全、线程同步、线程间通信(含面试题集)

    一.线程安全 多个线程在执行同一段代码的时候,每次的执行结果和单线程执行的结果都是一样的,不存在执行结果的二义性,就可以称作是线程安全的. 讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会 ...

  7. Java-多线程第三篇3种创建的线程方式、线程的生命周期、线程控制、线程同步、线程通信

    1.Java使用Thread类代表线程.     所有的线程对象必须是Thread类或其子类的实例. 当线程继承Thread类时,直接使用this即可获取当前线程,Thread对象的getName() ...

  8. 多线程,线程类三种方式,线程调度,线程同步,死锁,线程间的通信,阻塞队列,wait和sleep区别?

    重难点梳理 知识点梳理 学习目标 1.能够知道什么是进程什么是线程(进程和线程的概述,多进程和多线程的意义) 2.能够掌握线程常见API的使用 3.能够理解什么是线程安全问题 4.能够知道什么是锁 5 ...

  9. Python并发编程-进程 线程 同步锁 线程死锁和递归锁

    进程是最小的资源单位,线程是最小的执行单位 一.进程 进程:就是一个程序在一个数据集上的一次动态执行过程. 进程由三部分组成: 1.程序:我们编写的程序用来描述进程要完成哪些功能以及如何完成 2.数据 ...

  10. Python3 进程 线程 同步锁 线程死锁和递归锁

    进程是最小的资源单位,线程是最小的执行单位 一.进程 进程:就是一个程序在一个数据集上的一次动态执行过程. 进程由三部分组成: 1.程序:我们编写的程序用来描述进程要完成哪些功能以及如何完成 2.数据 ...

随机推荐

  1. RDD的源码

    RDD是一个抽象类定义了所有RDD共有的一些属性和方法,下面介绍了主要的属性和方法. abstract class RDD[T: ClassTag]( @transient private var _ ...

  2. Flume+Morphlines实现数据的实时ETL

    转载:http://mp.weixin.qq.com/s/xCSdkQo1XMQwU91lch29Uw Apache Flume介绍: Apache Flume是一个Apache的开源项目,是一个分布 ...

  3. POI - Excel API

    一.概述    1. Apache POI是Apache软件基金会的开放源码函式库,POI提供API给java程式对Microsoft Office格式档案读和写的功能.    2. 结构       ...

  4. 587. Erect the Fence(凸包算法)

    问题 给定一群树的坐标点,画个围栏把所有树围起来(凸包). 至少有一棵树,输入和输出没有顺序. Input: [[1,1],[2,2],[2,0],[2,4],[3,3],[4,2]] Output: ...

  5. Kernel space是啥?

    今天因为查一个Java zero copy的问题,遇到了kernel space.之前是耳闻过内核空间的,但是看到kernel space不知道是啥.知道的太少,除了学习,我也做不了啥.因为自己认知有 ...

  6. Sublime Text 3图标更改

    Sublime Text 3图标更改 步骤: 1.下载ico图标 2.然后更改图标 注意:重点讲解下,如何将png文件转换为ico图标: 网络上单独找sublime text 3的ico图标比较不好找 ...

  7. Harbor 企业级 Docker Registry

    HarBor项目:https://github.com/vmware/harbor 下载:https://github.com/vmware/harbor/releases 安装文档:https:// ...

  8. MSBI

    https://blog.csdn.net/fanyingnedu/article/details/78597207 Familiarity with Microsoft BI Stack - SSI ...

  9. 初探动态规划(DP)

    学习qzz的命名,来写一篇关于动态规划(dp)的入门博客. 动态规划应该算是一个入门oier的坑,动态规划的抽象即神奇之处,让很多萌新 萌比. 写这篇博客的目标,就是想要用一些容易理解的方式,讲解入门 ...

  10. Windows系统下解决“telnet不是外部或内部命令”的问题

    在学习Node.js时,需要使用Telnet连接Node TCP服务器,在命令行中运行: $ telnet 127.0.0.1 9000 时,命令行工具会报错:“telnet不是外部或内部命令”. 这 ...