iOS多线程之1. NSThread

   当新打开一个APP的时候,系统会新创建一个进程。这个进程会默认新创建一个线程,把这个线程命名为主线程。多线程主要应用于与服务器进行数据传输等一些耗时操作。为了防止阻塞主线程,影响用户交互,我们必须要新建子线程来执行一些耗时操作。本文主要通过介绍NSThread的使用方法,来探讨线程的生命周期、线程安全,线程间通信。

1.线程的生命周期

  线程的生命周期分为:1.创建线程;2.调度任务;3.销毁线程。一个NSThread对象就是一条线程,获得一个NSThread对象的方式有两种。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 方式1
    NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
        //调度任务,例如下载图片,往服务器上传文件等一切耗费时间的操作
        NSLog(@"thread1--------执行任务");
     }];
    // 开始执行线程中的任务,相当于调度任务
    [thread1 start];
    
    //方式 2
   //argument:id类型,为向方法(executeTask:)中传递的参数
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(executeTask:) object:@{@"key" : @"value"}];
    [thread2 start];
}

- (void)executeTask:(id)argument
{
    /*
    thread2--------执行任务-------argument = {
    key = value;
    }
    **/
    NSLog(@"thread2--------执行任务-------argument = %@",argument);
}

  创建一个NSThread对象并往线程中添加了任务之后,必须执行[thread start];才会执行线程中的任务。[thread start];只能执行一次,否则会报attempt to start the thread again的错误。
  当线程中的任务执行完之后,系统会自动执行[NSThread exit]销毁线程,释放内存。在销毁线程之前,[NSThread exit]这个方法会发送一个通知NSThreadWillExitNotification通知观察者线程即将销毁。由于通知的发出是同步的,所以回调的执行在线程销毁之前。所以我们可以用下面的方法来监测线程的销毁。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
        //调度任务,例如下载图片,往服务器上传文件等一切耗费时间的操作
        NSLog(@"thread1--------执行任务");
     }];
    // 开始执行线程中的任务,相当于任务调度
    thread1.name = @"thread1";
    [thread1 start];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(threadWillExit:) name:NSThreadWillExitNotification object:nil];
}

- (void)threadWillExit:(NSNotification *)noti
{
  //   日志: thread1 马上退出了
  NSLog(@"%@ 马上退出了",[[NSThread currentThread] name]);
}

  NSThread提供了很多属性和方法,方便我们使用。下面主要介绍常用的几个属性和方法。

// 类属性,获取当前线程  [NSThread currentThread]
@property (class, readonly, strong) NSThread *currentThread;
// 类属性,获取主线程  [NSThread mainThread]
@property (class, readonly, strong) NSThread *mainThread;
//线程的名字
@property (nullable, copy) NSString *name;
2.线程安全

  一块数据如果被多个线程同时访问,就容易发生数据错乱和数据安全问题。下面举一个卖票的例子。

- (void)viewDidLoad {
    [super viewDidLoad];

    // 一共50张票
    self.ticketNum  = 50;
   
    // 三个售票员同时开始卖票
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
    thread1.name = @"售票员A";
    self.thread1 = thread1;
    
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
    thread2.name = @"售票员B";
    self.thread2 = thread2;
    
    NSThread *thread3 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
    thread3.name = @"售票员C";
    self.thread3 = thread3;
    
    [thread1 start];
    [thread2 start];
    [thread3 start];
    
}

- (void)sellTickets
{
    while (1) {
        int count = self.ticketNum;
        
        //1.先检查票数
        if (count > 0) {
            //暂停一段时间
            [NSThread sleepForTimeInterval:0.02];
            //2.票数-1
            
            self.ticketNum = count-1;
            //获取当前线程
            NSThread *current= [NSThread currentThread];
            NSLog(@"%@--卖了一张票,还剩余%d张票",current,self.ticketNum);
        }
        
        if (self.ticketNum == 0){
            //退出线程
            [NSThread exit];
        }
    }
}

  因为日志太长,仅贴出部分日志。

2019-03-27 15:43:03.848178+0800 TestAppIOS[399:12301] <NSThread: 0x28354d0c0>{number = 4, name = 售票员B}--卖了一张票,还剩余49张票
2019-03-27 15:43:03.848178+0800 TestAppIOS[399:12302] <NSThread: 0x28354cf40>{number = 5, name = 售票员C}--卖了一张票,还剩余49张票
2019-03-27 15:43:03.848194+0800 TestAppIOS[399:12300] <NSThread: 0x28354d200>{number = 3, name = 售票员A}--卖了一张票,还剩余49张票
2019-03-27 15:43:03.868778+0800 TestAppIOS[399:12301] <NSThread: 0x28354d0c0>{number = 4, name = 售票员B}--卖了一张票,还剩余48张票
2019-03-27 15:43:03.872740+0800 TestAppIOS[399:12302] <NSThread: 0x28354cf40>{number = 5, name = 售票员C}--卖了一张票,还剩余48张票
2019-03-27 15:43:03.872797+0800 TestAppIOS[399:12300] <NSThread: 0x28354d200>{number = 3, name = 售票员A}--卖了一张票,还剩余48张票

  通过日志打印我们可以看出,一共50张票,但是ABC每个售票员都卖了50张票,这明显是不对的。那问题出在哪里呢?三个线程同时访问门票的数量(共享数据),发生了数据错乱。
1.售票员A查询门票数量的时候,发现门票还有50张,卖了一张,还剩49张。
2.售票员B查询门票数量的时候(此时售票员A还没有把门票卖出去),发现门票还有50张,卖了一张,还剩49张。
3.售票员C查询门票数量的时候(此时售票员A.B还没有把门票卖出去),发现门票还有50张,卖了一张,还剩49张。
4.这就出现了一个怪现象,每个售票员都卖出1张票,却还剩49张门票的原因。
  那怎样解决这个问题呢?一个售票员卖票的时候,其他两个人等着。等这个售票员卖完了,这俩售票员其中的一个再卖。

- (void)sellTickets
{
    while (1) {
        // 加一把锁
        @synchronized (self) {
            int count = self.ticketNum;
            //1.先检查票数
            if (count > 0) {
                //暂停一段时间
                [NSThread sleepForTimeInterval:0.02];
                //2.票数-1
                
                self.ticketNum = count-1;
                //获取当前线程
                NSThread *current= [NSThread currentThread];
                NSLog(@"%@--卖了一张票,还剩余%d张票",current,self.ticketNum);
            }
        }
        
        if (self.ticketNum == 0){
            //退出线程
            [NSThread exit];
        }
    }
}

  除了@synchronized,还有NSLock也可以达到相同的效果,解决多线程资源共享产生的数据安全问题。@synchronized就是对括号里的代码加锁,一个线程执行完了之后,另一个线程才能执行。(售票员A卖票呢,此时BC不能卖。等A卖完了,BC才能卖。BC再去查询票的数量的时候就变成了49张(因为A卖了一张))。
  锁完美解决了多线程带来的数据安全问题,不过锁需要消耗大量的CPU资源,所以我们开发的时候尽量避免这种场景。

3.线程间通信

  理论上讲线程都不是孤立存在的,需要相互传递消息。最常见的就是我们把一些耗时操作放在子线程,例如下载图片,但是下载完毕我们不能在子线程更新UI,因为只有主线程才可以更新UI和处理用户的触摸事件,否则程序会崩溃。此时,我们就需要把子线程下载完毕的数据传递到主线程,让主线程更新UI,这就是线程间的通信。

原理


002MKIl6zy75OmRx7rP61.jpeg

代码

// 点击屏幕开始下载图片
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"当前线程1=%@",[NSThread currentThread]);
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
    NSLog(@"当前线程2=%@",[NSThread currentThread]);
       NSString *strURL = @"http://pic33.nipic.com/20130916/3420027_192919547000_2.jpg";
        UIImage *image = [self downloadImageWithURL:strURL];
        if (image) {
            [self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO];
        }
    }];
    [thread start];
}

日志

2016-11-04 13:47:04.532 TTTTTTTTTT[10584:122182] 当前线程1=<NSThread: 0x600000260c80>{number = 1, name = main}
2016-11-04 13:47:04.533 TTTTTTTTTT[10584:122269] 当前线程2=<NSThread: 0x600000265d80>{number = 3, name = (null)}

  以上就是关于NSThread的大部分知识了。在开发中,我们真正应用NSThread的时候并不多,因为NSThread需要我们自己创建线程,调度任务。而线程并不是创建的越多越好,虽然多线程可以提高CPU的利用效率,但是创建多了,反而会拉低CPU运行速度,因为线程本身也需要消耗内存。所以创建几个线程,得根据CPU的当时状况来判断。这对iOS程序员来说是解决不了的问题,因为我们无法知道CPU的运行状况。所以NSThread虽然简单,但是有很多不确定的情况。我们经常使用的还是GCD和NSOperation,至于原因,后文我会详细讲解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容