iOS 多线程管理之 NSThread

NSThread 是苹果官方提供的,使用起来比 pthread 更加面向对象,简单易用,可以直接操作线程对象。不过也需要需要程序员自己管理线程的生命周期(主要是创建),我们在开发的过程中偶尔使用 NSThread。比如我们会经常调用 [NSThread currentThread] 来显示当前的进程信息。
  1. 构造方法
    1. init 方法
      1. 创建一个带有方法体的线程
        - (instancetype)initWithTarget:方法所属的对象 selector:需要执行的方法 object:方法需要的参数;
      2. 创建一个块线程
        - (instancetype)initWithBlock:无参数无返回值的块;
    2. 创建线程以后自动启动线程
      1. 创建一个带有方法体的线程
        + (void)detachNewThreadSelector:需要执行的方法 toTarget:方法所属的对象 withObject:方法需要的参数;
      2. 创建一个块线程
        + (void)detachNewThreadWithBlock:无参数无返回值的块;通过这个方法,可以直接添加一个线程添加到可执行线程池中。
    3. 隐式创建线程
      - (void)performSelectorInBackground:需要执行的方法 withObject:方法需要的参数;
  2. 常用属性
    1. 线程名
      @property (nullable, copy) NSString *name;
    2. 线程的优先级
      @property double threadPriority;
      注意:设置优先级的值的时候,由于这个属性的值的范围与操作系统的优先级有映射,一般默认的线程优先级可能是 0.5 ,但最后的决定权是根据内核来决定。谁也没法保证当前线程的优先级的值就是当前属性的属性值。
    3. 线程状态
      1. 处理
        @property (readonly, getter=isExecuting) BOOL executing;
      2. 完成
        @property (readonly, getter=isFinished) BOOL finished;
      3. 取消
        @property (readonly, getter=isCancelled) BOOL cancelled;
  3. 终止子线程方法
    1. 在当前线程下调用 exit 方法+ (void)exit; 调用完这个方法以后,线程的 cannelled 属性为 YES。
  4. 线程睡眠
    1. 睡眠一段时间
      + (void)sleepForTimeInterval:休眠时间;
    2. 睡眠到一个时间点
      + (void)sleepUntilDate:(NSDate *)date;
  5. 线程之间的通信
    1. 系统提供的线程之间通信的方法
      1. 在主线程当中进行操作
        1. - (void)performSelectorOnMainThread:在主线程需要执行的方法 withObject:方法当中的参数 waitUntilDone:是否等待当前线程执行完毕子再执行该方法 modes:NSRunLoop当中的模式;
        2. - (void)performSelectorOnMainThread:在主线程需要执行的方法 withObject:方法当中的参数 waitUntilDone:如果为 YES,则需要执行完当前线程以后,才执行主线程当中的方法。如果为 NO,则无需执行完当前线程就调用主线程执行该方法;
      2. 在指定线程当中进行操作
        1. - (void)performSelector:需要执行的方法 onThread:在哪个线程当中执行该方法 withObject:方法当中需要的参数 waitUntilDone:是否等待当前线程执行完毕子再执行该方法 modes:NSRunLoop当中的模式
        2. - (void)performSelector:需要执行的方法 onThread:在哪个线程执行该方法 withObject:方法当中的参数 waitUntilDone:是否等待当前线程执行完毕子再执行该方法
      3. 在当期线程当中执行操作
        1. - (id)performSelector:需要执行的方法;
        2. - (id)performSelector:需要执行的方法 withObject:方法当中的参数;
        3. - (id)performSelector:需要执行的方法 withObject:方法需要的参数1 withObject:方法需要的参数2;

    样例1:异步下载图片 — 通过方法来控制线程通信
    当我们需要从网络上加载一张图片的时候,如果我们直接在主线程上进行下载,会出现图片下载完成之前,手机点击无任何反应。只有图片下载好并且加载完成以后才能使用手机。如果这个时候网络突然出现问题。则主线程一直处于阻塞状态。影响用户体验。这个时候需要将下载任务分担给子线程,主线程只负责图片的下载完成的刷新。
    实现效果:当我们点击按钮的时候,图片通过子线程进行加载。
    实现步骤
    1. 从子线程当中下载图片。
    2. 当子线程下载好图片时,从主线程刷新图片

    实现代码

// 当点击按钮以后,执行以下事件
- (IBAction)showImage:(UIButton *)sender {
    [NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];
}
- (void)downloadImage {
    NSURL *url = [NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1556552942866&di=24c024e198e7cb243247413e19fb50a8&imgtype=jpg&src=http%3A%2F%2Fupload.ikanchai.com%2F2016%2F0704%2F1467601486394.jpg"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];
    // 当图片下载完成以后,才在主线程上执行刷新动作
    [self performSelectorOnMainThread:@selector(setImageInImageView:) withObject:image waitUntilDone:YES];
}
- (void)setImageInImageView:(UIImage *)image {
    self.imageView.image = image;
}

    样例2:存取钱 — 利用 NSCondition 来进行控制线程通信
    模拟银行取钱存钱的过程。
    假设卡里有 1000 块,小张和小王分别从两个窗口进行存取钱。现在要求必须先取钱再存钱。每次小张往里面存 800 块,小王每次从里面取 800 块。两人做 100 次。

    首先需要创建一个账户

import UIKit

class ITAccount: NSObject {
	  // 创建一个 NSCondition 对象,负责管理线程的状态
    var condition:NSCondition!
	  // 创建一个标记,看是否在里面存钱了
    var hasMoney = true
    var accountNO:String
    var balance:Double
    init(accountNO:String, balance:Double) {
        self.accountNO = accountNO
        self.balance = balance
        self.condition = NSCondition()
    }
    // 取钱方法
    func draw(drawAmount:Double) {
		  // 先将条件上锁,保证其他线程不影响该线程的执行
        condition.lock()
		  // 判断有没有人向里面存钱,如果没人,让当期线程进入等待状态。等待有人先把钱存进账户,再执行当前线程。
        if !hasMoney {
            condition.wait()
        } else {
            print("\(Thread.current.name ?? "") 取了\(drawAmount)元")
            Thread.sleep(forTimeInterval: 0.01)
            balance -= drawAmount
            print("\t 卡里还剩\(balance)元")
			  // 当取钱成功以后,需要将账户状态设置成已取钱。不能再取钱。
            hasMoney = false
			  // 这个时候通知其他线程,进入就绪状态(检查一下有没有其他线程等待访问)
            condition.broadcast()
        }
		  // 当边界区代码执行完毕以后,解除条件锁
        condition.unlock()
    }
    // 存钱的方法
    func deposit(drawAmount:Double) {
        condition.lock()
		  // 判断有人往里面存钱了,如果有人往里面存钱了,则当期线程进入阻塞状态,等待其他线程唤醒
        if hasMoney {
            condition.wait()
        } else {
            print("从\(Thread.current.name ?? "")存款成功")
            balance += drawAmount
            print("现在卡里有\(balance)元")
            hasMoney = true
            condition.broadcast()
        }
        condition.unlock()
    }
}

    转换到控制器当中,创建一个按钮,当点击按钮以后开始执行这个存取钱操作

import UIKit

class ViewController: UIViewController {
    var account:ITAccount!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        account = ITAccount(accountNO: "123456", balance: 1000)
        let button = UIButton(type: .system)
        button.frame = CGRect(x: 10, y: 50, width: view.bounds.width - 20, height: 30)
        button.setTitle("并发取钱", for: .normal)
        button.addTarget(self, action: #selector(drawMoney(sender:)), for: .touchUpInside)
        view.addSubview(button)
    }
    @objc func drawMoney(sender:UIButton) {
		  // 这个线程负责取钱
        let thread1 = Thread(target: self, selector: #selector(draw(moneyAmount:)), object: 800)
        thread1.name = "中国银行"
		  // 这个线程负责存钱
        let thread2 = Thread(target: self, selector: #selector(deposit(moneyAmount:)), object: 800)
        thread2.name = thread1.name
        thread1.start()
        thread2.start()
    }
    @objc func draw(moneyAmount:NSNumber) {
        for _ in 0...100 {
            account.draw(drawAmount: moneyAmount.doubleValue)
        }
    }
    @objc func deposit(moneyAmount:NSNumber) {
        for _ in 0...100 {
            account.deposit(drawAmount: moneyAmount.doubleValue)
        }
    }
}
  1. NSThread 线程安全与线程同步
    • 线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
      若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。出现线程安全的问题是由于线程的调度具有随机性造成的。
    • 线程同步:可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。
      比如:两个人互相聊天,两个人不能同时说话,需要先让一个人说完了(一个线程已结束),另外一个人才能继续回应(另一个线程开始执行)。

  关于线程同步的案例:

  1. 买票
    当我们到车站去买票的时候,买票的时候通常有多个窗口处于开放状态。这个情况下我们需要创建多个线程,模拟多个窗口开始卖票。假设现在有 100 张票待出售。有 2 个线程同时开始卖票。
    当不考虑线程安全的时候,实现如下:
- (void)viewDidLoad {
    [super viewDidLoad];
	   UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    [button setTitle:@"非线程安全卖票" forState:UIControlStateNormal];
    button.frame = CGRectMake(10, 50, CGRectGetWidth(self.view.bounds - 30), 30);
    [button addTarget:self action:@selector(unsafeBuyTicketButton:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}
- (void)unsafeBuyTicketButton:(UIButton *)sender {
    self.leftTickets = 50;
    self.beijingStation = [[NSThread alloc] initWithTarget:self selector:@selector(unsafeBuyTicket) object:nil];
    self.beijingStation.name = @"北京站";
    self.shanghaiStation = [[NSThread alloc] initWithTarget:self selector:@selector(unsafeBuyTicket) object:nil];
	  self.shanghaiStation.name = @"上海站";
    [self.beijingStation start];
    [self.shanghaiStation start];
}
- (void)unsafeBuyTicket {
    while (self.leftTickets) {
        self.leftTickets--;
        NSLog(@"现在%@卖出一张票,还剩%ld张票", NSThread.currentThread.name, self.leftTickets);
        [NSThread sleepForTimeInterval:0.1];
    }
}

  执行结果如下(这个地方只是截取部分结果,更加突出非线程安全)

2019-04-29 00:20:36.213789+0800 买票[3525:338766] 现在上海站卖出一张票,还剩48张票
2019-04-29 00:20:36.213796+0800 买票[3525:338765] 现在北京站卖出一张票,还剩49张票
2019-04-29 00:20:36.317764+0800 买票[3525:338766] 现在上海站卖出一张票,还剩47张票
2019-04-29 00:20:36.317764+0800 买票[3525:338765] 现在北京站卖出一张票,还剩47张票
2019-04-29 00:20:36.422135+0800 买票[3525:338765] 现在北京站卖出一张票,还剩45张票
2019-04-29 00:20:36.422135+0800 买票[3525:338766] 现在上海站卖出一张票,还剩46张票
2019-04-29 00:20:36.525773+0800 买票[3525:338765] 现在北京站卖出一张票,还剩44张票
2019-04-29 00:20:36.525775+0800 买票[3525:338766] 现在上海站卖出一张票,还剩44张票
2019-04-29 00:20:36.630788+0800 买票[3525:338766] 现在上海站卖出一张票,还剩43张票
2019-04-29 00:20:36.630788+0800 买票[3525:338765] 现在北京站卖出一张票,还剩43张票
2019-04-29 00:20:36.733148+0800 买票[3525:338766] 现在上海站卖出一张票,还剩41张票
2019-04-29 00:20:36.733148+0800 买票[3525:338765] 现在北京站卖出一张票,还剩41张票
2019-04-29 00:20:36.836007+0800 买票[3525:338766] 现在上海站卖出一张票,还剩40张票
2019-04-29 00:20:36.836008+0800 买票[3525:338765] 现在北京站卖出一张票,还剩40张票
2019-04-29 00:20:36.940403+0800 买票[3525:338765] 现在北京站卖出一张票,还剩38张票
2019-04-29 00:20:36.940403+0800 买票[3525:338766] 现在上海站卖出一张票,还剩38张票
2019-04-29 00:20:37.045444+0800 买票[3525:338765] 现在北京站卖出一张票,还剩37张票
2019-04-29 00:20:37.045469+0800 买票[3525:338766] 现在上海站卖出一张票,还剩37张票
......
2019-04-29 00:20:37.460871+0800 买票[3525:338765] 现在北京站卖出一张票,还剩29张票
2019-04-29 00:20:37.460871+0800 买票[3525:338766] 现在上海站卖出一张票,还剩29张票
2019-04-29 00:20:37.565210+0800 买票[3525:338765] 现在北京站卖出一张票,还剩28张票
2019-04-29 00:20:37.565210+0800 买票[3525:338766] 现在上海站卖出一张票,还剩28张票
......
2019-04-29 00:20:38.393183+0800 买票[3525:338765] 现在北京站卖出一张票,还剩12张票
2019-04-29 00:20:38.393183+0800 买票[3525:338766] 现在上海站卖出一张票,还剩12张票
......
2019-04-29 00:20:38.598336+0800 买票[3525:338766] 现在上海站卖出一张票,还剩9张票
2019-04-29 00:20:38.598336+0800 买票[3525:338765] 现在北京站卖出一张票,还剩9张票
2019-04-29 00:20:38.700762+0800 买票[3525:338766] 现在上海站卖出一张票,还剩7张票
2019-04-29 00:20:38.700762+0800 买票[3525:338765] 现在北京站卖出一张票,还剩7张票
2019-04-29 00:20:38.804648+0800 买票[3525:338766] 现在上海站卖出一张票,还剩5张票
2019-04-29 00:20:38.804648+0800 买票[3525:338765] 现在北京站卖出一张票,还剩5张票

  这里面的有些票数是没按照倒序往下排,有些是票卖重了。究其原因是由于有些情况下两个线程同时在购买车票(车票是两者的共享变量)。而在现实生活当中,如果一个人在买票的时候,另外一个人应该是无法购买到相同位置的票。所以需要将买票的那段代码添加同步锁。保证在买票的时候只有一个窗口可以买票。
  所以为了保证线程安全,需要在取钱的部分进行加锁

-  (void)safeBuyTicket {
    // 由于需要修改车票数,所以需要先将对象自身进行加锁。
    @synchronized (self) {
        while (self.leftTickets) {
            self.leftTickets--;
            NSLog(@"现在%@卖出一张票,还剩%ld张票", NSThread.currentThread.name, self.leftTickets);
            [NSThread sleepForTimeInterval:0.2];
        }
        NSLog(@"对不起,%@没票了", NSThread.currentThread.name);
    }
    // 当卖票过程结束以后,锁解除
}

  这个时候再去执行。得到以下结果

2019-04-29 09:01:36.569563+0800 买票[3894:363026] 现在北京站卖出一张票,还剩49张票
2019-04-29 09:01:36.774933+0800 买票[3894:363026] 现在北京站卖出一张票,还剩48张票
2019-04-29 09:01:36.977431+0800 买票[3894:363026] 现在北京站卖出一张票,还剩47张票
2019-04-29 09:01:37.182415+0800 买票[3894:363026] 现在北京站卖出一张票,还剩46张票
2019-04-29 09:01:37.387306+0800 买票[3894:363026] 现在北京站卖出一张票,还剩45张票
2019-04-29 09:01:37.587751+0800 买票[3894:363026] 现在北京站卖出一张票,还剩44张票
2019-04-29 09:01:37.791243+0800 买票[3894:363026] 现在北京站卖出一张票,还剩43张票
2019-04-29 09:01:37.993895+0800 买票[3894:363026] 现在北京站卖出一张票,还剩42张票
2019-04-29 09:01:38.199246+0800 买票[3894:363026] 现在北京站卖出一张票,还剩41张票
2019-04-29 09:01:38.400462+0800 买票[3894:363026] 现在北京站卖出一张票,还剩40张票
2019-04-29 09:01:38.602914+0800 买票[3894:363026] 现在北京站卖出一张票,还剩39张票
2019-04-29 09:01:38.808258+0800 买票[3894:363026] 现在北京站卖出一张票,还剩38张票
2019-04-29 09:01:39.013592+0800 买票[3894:363026] 现在北京站卖出一张票,还剩37张票
2019-04-29 09:01:39.215573+0800 买票[3894:363026] 现在北京站卖出一张票,还剩36张票
2019-04-29 09:01:39.417993+0800 买票[3894:363026] 现在北京站卖出一张票,还剩35张票
2019-04-29 09:01:39.623340+0800 买票[3894:363026] 现在北京站卖出一张票,还剩34张票
2019-04-29 09:01:39.827254+0800 买票[3894:363026] 现在北京站卖出一张票,还剩33张票
2019-04-29 09:01:40.027850+0800 买票[3894:363026] 现在北京站卖出一张票,还剩32张票
2019-04-29 09:01:40.229999+0800 买票[3894:363026] 现在北京站卖出一张票,还剩31张票
2019-04-29 09:01:40.431019+0800 买票[3894:363026] 现在北京站卖出一张票,还剩30张票
2019-04-29 09:01:40.633772+0800 买票[3894:363026] 现在北京站卖出一张票,还剩29张票
2019-04-29 09:01:40.839112+0800 买票[3894:363026] 现在北京站卖出一张票,还剩28张票
2019-04-29 09:01:41.044466+0800 买票[3894:363026] 现在北京站卖出一张票,还剩27张票
2019-04-29 09:01:41.245871+0800 买票[3894:363026] 现在北京站卖出一张票,还剩26张票
2019-04-29 09:01:41.451108+0800 买票[3894:363026] 现在北京站卖出一张票,还剩25张票
2019-04-29 09:01:41.653174+0800 买票[3894:363026] 现在北京站卖出一张票,还剩24张票
2019-04-29 09:01:41.853563+0800 买票[3894:363026] 现在北京站卖出一张票,还剩23张票
2019-04-29 09:01:42.058787+0800 买票[3894:363026] 现在北京站卖出一张票,还剩22张票
2019-04-29 09:01:42.264138+0800 买票[3894:363026] 现在北京站卖出一张票,还剩21张票
2019-04-29 09:01:42.469487+0800 买票[3894:363026] 现在北京站卖出一张票,还剩20张票
2019-04-29 09:01:42.670215+0800 买票[3894:363026] 现在北京站卖出一张票,还剩19张票
2019-04-29 09:01:42.871871+0800 买票[3894:363026] 现在北京站卖出一张票,还剩18张票
2019-04-29 09:01:43.077231+0800 买票[3894:363026] 现在北京站卖出一张票,还剩17张票
2019-04-29 09:01:43.282586+0800 买票[3894:363026] 现在北京站卖出一张票,还剩16张票
2019-04-29 09:01:43.484710+0800 买票[3894:363026] 现在北京站卖出一张票,还剩15张票
2019-04-29 09:01:43.690056+0800 买票[3894:363026] 现在北京站卖出一张票,还剩14张票
2019-04-29 09:01:43.895408+0800 买票[3894:363026] 现在北京站卖出一张票,还剩13张票
2019-04-29 09:01:44.098013+0800 买票[3894:363026] 现在北京站卖出一张票,还剩12张票
2019-04-29 09:01:44.300308+0800 买票[3894:363026] 现在北京站卖出一张票,还剩11张票
2019-04-29 09:01:44.501114+0800 买票[3894:363026] 现在北京站卖出一张票,还剩10张票
2019-04-29 09:01:44.706466+0800 买票[3894:363026] 现在北京站卖出一张票,还剩9张票
2019-04-29 09:01:44.911829+0800 买票[3894:363026] 现在北京站卖出一张票,还剩8张票
2019-04-29 09:01:45.117033+0800 买票[3894:363026] 现在北京站卖出一张票,还剩7张票
2019-04-29 09:01:45.322381+0800 买票[3894:363026] 现在北京站卖出一张票,还剩6张票
2019-04-29 09:01:45.527740+0800 买票[3894:363026] 现在北京站卖出一张票,还剩5张票
2019-04-29 09:01:45.733103+0800 买票[3894:363026] 现在北京站卖出一张票,还剩4张票
2019-04-29 09:01:45.938453+0800 买票[3894:363026] 现在北京站卖出一张票,还剩3张票
2019-04-29 09:01:46.143808+0800 买票[3894:363026] 现在北京站卖出一张票,还剩2张票
2019-04-29 09:01:46.349174+0800 买票[3894:363026] 现在北京站卖出一张票,还剩1张票
2019-04-29 09:01:46.550451+0800 买票[3894:363026] 现在北京站卖出一张票,还剩0张票
2019-04-29 09:01:46.751257+0800 买票[3894:363026] 对不起,北京站没票了
2019-04-29 09:01:46.751632+0800 买票[3894:363027] 对不起,上海站没票了

  这个时候结果正常。可以按照顺序进行售票。
  2. 模拟取钱的过程
  到银行去取钱的时候,有多个窗口可以办理业务。现在假设有 2 个人用同一个账户进行取钱。那么将会创建 2 个线程,分别进行取钱。假设现在卡里有 1000 块。每个人同时从卡里取 800 块。

  先声明账户类(ITAccount)

NS_ASSUME_NONNULL_BEGIN

@interface ITAccount : NSObject
@property (nonatomic, copy) NSString *accountNO; 银行卡号
@property (nonatomic, assign) double balance; 卡内余额
- (instancetype)initWithAccount:(NSString *)acccountNO balance:(CGFloat)balance;
/**
 从卡里取钱
 @param amount 需要取多少钱
 */
- (void)drawAmount:(double)amount;
@end

NS_ASSUME_NONNULL_END

  实现账户类(ITAccount)

@implementation ITAccount
- (instancetype)initWithAccount:(NSString *)acccountNO balance:(double)balance {
    self = [super init];
    if (self) {
        _accountNO = acccountNO;
        _balance = balance;
    }
    return self;
}
// 判断卡里面是
- (void)drawAmount:(CGFloat)amount {
	// 通过判断卡里面的余额,来决定能否取钱
	if (self.balance >= amount) {
		NSLog(@"%@取钱成功!取出现金%g元", NSThread.currentThread.name, amount);
		self.balance -= amount;
		[NSThread sleepForTimeInterval:0.2];
      NSLog(@"现在还剩%g", self.balance);
  } else {
		NSLog(@"对不起,从%@取钱失败,无法取钱", NSThread.currentThread.name);
  }
}
@end

  再从控制器当中创建一个按钮,点击按钮,模拟 2 个人取钱。

@interface ViewController ()
@property (nonatomic, strong) ITAccount *account;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.account = [[ITAccount alloc] initWithAccount:@"123456" balance:1000];
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    button.frame = CGRectMake(10, 50, CGRectGetWidth(self.view.bounds) - 20, 30);
    [button setTitle:@"并发编程" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(draw:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}
// 当点击按钮的时候,开始模拟两个人取钱
- (void)draw:(UIButton *)sender{
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(drawMethod:) object:@800];
    thread1.name = @"中国银行";
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(drawMethod:) object:@800];
    thread2.name = @"中国工商银行";
    [thread1 start];
    [thread2 start];
}
// 如何去取钱
- (void)drawMethod:(NSNumber *)drawAmount {
    [self.account drawAmount:drawAmount.doubleValue];
}
@end

  最后结果如下

2019-04-28 23:41:01.168169+0800 取钱[3001:290346] 中国银行取钱成功!取出现金800元
2019-04-28 23:41:01.168207+0800 取钱[3001:290347] 中国工商银行取钱成功!取出现金800元
2019-04-28 23:41:01.370037+0800 取钱[3001:290346] 现在还剩-600
2019-04-28 23:41:01.370037+0800 取钱[3001:290347] 现在还剩-600

  出现这个结果的原因是:两个线程对共享变量进行并发访问(相当于两个人同时取钱),导致钱数为负数。
  如何解决:将需要共享的部分添加同步锁。当有线程访问该共享变量的时候,为该变量添加一把锁。使得其他线程无法访问到该变量。直到该线程对该变量操作完毕以后,即可解锁。
  在取钱的时候,账户相当于共享变量。需要将取钱的这个过程上锁。不能让一个线程取钱的时候另外的线程同样也开始取钱。于是需要将取钱的方法进行上锁。

  需要修改 ITAccount 的取钱方法

- (void)drawAmount:(CGFloat)amount {
    // 将当前账户作为同步锁对象,如果哪个人需要取钱,需要先将当前账户锁定,防止其它账户能够并发访问到当前账户。
    @synchronized (self) {
        // 通过判断卡里面的余额,来决定能否取钱
        if (self.balance >= amount) {
            NSLog(@"%@取钱成功!取出现金%g元", NSThread.currentThread.name, amount);
            self.balance -= amount;
            [NSThread sleepForTimeInterval:0.2];
            NSLog(@"现在还剩%g", self.balance);
        } else {
            NSLog(@"对不起,从%@取钱失败,无法取钱", NSThread.currentThread.name);
        }
    }
    // 当取钱结束以后,即解除同步锁。
}

  修改好以后再运行该程序

2019-04-29 00:09:04.758388+0800 取钱[3314:329183] 中国银行取钱成功!取出现金800元
2019-04-29 00:09:04.963739+0800 取钱[3314:329183] 现在还剩200
2019-04-29 00:09:04.964052+0800 取钱[3314:329184] 对不起,从中国工商银行取钱失败,无法取钱

  一个完整的多线程取钱步骤就完成了。
  通过线程同步,可以保证线程共享区当中的内容能够安全的访问。

参考链接:
iOS 多线程:『pthread、NSThread』详尽总结 - 简书
iOS开发之多线程编程总结(一) - 简书

十分感谢上面两位大神的帮助,如此博文当中有错误还请大神雅正。

你可能感兴趣的:(iOS开发)