线程同步与线程通信

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

线程安全问题

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

  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. 查T结果与Z结果的P值[转载]

    T检验P值表格来自:https://blog.csdn.net/m0_37777649/article/details/74937242 Z检验表格来自:https://wenku.baidu.com ...

  2. 制造抽象基类--《C++必知必会》 条款33

    抽象类,含有纯虚函数的类,不可以创建对象. 然而,有时我们并不需要某个函数定义为纯虚函数,但是任然希望此类像抽象类一样,不可以创建对象. 方法1:通过确保类中不存在共有构造函数来模拟抽象基类的性质.意 ...

  3. 工作记录之开发HRM项目总结

    开发HRM项目的步骤: 1:首先保留HRM项目中的两张表(Employee,Position)把这两张表中的数据内容复制到Operator-Portlet这个项目中,这两张表的service.xml配 ...

  4. 基于.net core webapi和mongodb的日志系统

    开发环境vs2017,.NET Core2.1, 数据写入到mongodb.思路就是1.提供接口写入日志,2.基于接口封装类库.3.引入类库使用 源码在最后 为什么要写它 很多开源项目像nlog.lo ...

  5. NiFi REST API 的使用

    一.概述 为方便用户使用 NiFi 进行二次开发,NiFi 为开发者提供了 NIFI REST API. 关于 API 的详细介绍,请参见官网 NIFI REST API.这里仅给出一些注意的小细节. ...

  6. ZOJ Monthly, January 2019

    A: Little Sub and Pascal's Triangle Solved. 题意: 求杨辉三角第n行奇数个数 思路: 薛聚聚说找规律,16说Lucas 答案是 $2^p \;\;p 为 n ...

  7. jmeter命令行运行与生成报告

    一.     使用命令行方式运行Jmeter 1.1 为什么 使用GUI方式启动jmeter,运行线程较多的测试时,会造成内存和CPU的大量消耗,导致客户机卡死. 所以正确的打开方式是在GUI模式下调 ...

  8. laravel 多图上传

    前台  name="photo[]" 后台获取  $request->file('photo');//获取多个图片循环

  9. bzoj1604 / P2906 [USACO08OPEN]牛的街区Cow Neighborhoods

    P2906 [USACO08OPEN]牛的街区Cow Neighborhoods 考虑维护曼哈顿距离:$\left | x_{1}-x_{2} \right |+\left | y_{1}-y_{2} ...

  10. 在JAVA可移植性的来源的三方面

    软件可移植性的概念是与软件从某一环境转移到另一环境下的难易程度.为获得较高的可移植性,在设计过程中常采用通用的程序设计语言和运行支撑环境.尽量不用与系统的底层相关性强的语言.下面介绍JAVA的可移植性 ...