iOS 多线程之NSThread

一  先从什么是线程开始

如果去面试,一定会被问到多线程的问题。那么什么是线程?

线程是操作系统能够运算调度的最小单位,它被包含在进程中,是进程中的实际运算单位,一条线程指的是进程中单一顺序的控制流。一个进程可以并发多个线程,每条线程并行执行不同的任务。

或者可以说线程是运行时执行的一组指令序列。每个进程至少包含一个线程

 

这么说还是比较抽象,打个比方说线程就像工厂在运作的每条不同的生产流水线,而进程就是这整个生产任务。

二 iOS中的多线程

在iOS开发中,我们在很多情况下很多的耗时操作都需要用到多线程。UI线程如果卡顿就会影响用户体验,用户体验差这个APP估计就被卸载了。所以,关于多线程的使用还是很重要的。

在iOS中,进程启动时候主要线程被称为主线程。所有的UI元素都需要在主线程中创建和管理。与用户交互相关的所有中断最终都会分发到UI线程。Cocoa编程不允许别的线程更新UI元素。这意味着,无论何时应用在后台线程执行了耗时操作,比如网络或者其他的处理,代码都必须将上下文切换到主线程再更新UI,比如进度条指示任务进度或者标签展示处理效果。

线程的开销:

虽然应用有多个线程看起来非常赞,但是每个线程都有一定的开销,从而影响到应用的性能,线程不仅仅有创建时候的时间开销,还会消耗内核的内存,即应用的内存空间。

每个线程大约消耗1KB 的线程内存,用以存储与线程有关的数据结构与属性。这块内存是联动内存无法被分页。

主线程的栈空间大小是1M,每个二级线程的空间大小默认是512KB。完整的栈并不会被 立即创建出来实际的栈空间大小会随着使用而曾长。因此,即使主线程有1M的栈空间大小,某个时间点的实际栈空间大小很可能要小的多。

在线程起动 前,栈空间的大小可以被改变,栈空间的最小值是16KB,而且数值必须是4的倍数 。示例代码演示如何在启动线程配置栈的大小

+(NSThread *)createThreadWithTarget:(id)target selector:(SEL )selector object:(id)argument stackSize:(NSUInteger)size
{
    if(size%4096 !=0)
    {
        return nil;
    }
    NSThread * thread = [[NSThread alloc]initWithTarget:target selector:selector object:nil];
    thread.stackSize = size;
    return thread;
}

启动一个新的线程耗时开销是很大的,因此并不是线程越多越好。(iOS的优化大多数是空间跟时间的相互调试。)
多线程是一种易发生各种问题的多线程编程的技术,比如多个线程更新相同的资源会导致数据的不一致(数据竞争),停止等待的线程会导致多个线程相互持续等待(死锁),使用太多的线程会消耗大量内存。

尽管多线程有以上的影响,但是还是应当使用多线程编程。因为使用多线程编程可以保证应用程序的相应性能。

 

 

 

 

 

我们常用到的多线程技术包括:NSThread,NSOperation,GCD.

下面来探讨一下这三种多线程技术的使用方法和使用场景以及各自优劣。

 

1  NSThread (参考博客:iOS多线程)

NSThread 是苹果官方提供的,使用起来比 pthread 更加面向对象,简单易用,可以直接操作线程对象。不过也需要需要程序员自己管理线程的生命周期(主要是创建),我们在开发的过程中偶尔使用 NSThread。比如我们会经常调用[NSThread currentThread]来显示当前的进程信息。

NSThread 创建线程

(1)创建线程需要手动启动线程 :

- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
 //创建一个线程
    NSThread * thread = [[NSThread alloc]initWithTarget:self selector:@selector(thereadActino:) object:nil];
    [thread start];
//线程方法的执行
-(void)thereadActino:(NSThread *)thread
{
    NSLog(@"%@",[NSThread currentThread]);
    NSLog(@"%@",thread);

}

执行结果:

2019-03-10 11:32:34.581831+0800 TestUntilPro[25187:2186739] {number = 3, name = (null)}

number 是线程的Id ,name 是线程的名称,例如,主线程name 是main。

(2)创建线程自动启动线程的方法:

[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
//不需要手动开启
-(void)run{
    NSLog(@"%@",[NSThread currentThread]);
}

(3)隐事创建线程(在后台)

 [self performSelectorInBackground:@selector(run) withObject:nil];

-(void)run{
    NSLog(@"%@",[NSThread currentThread]);
}

(4)线程相关的方法(参考自博客:iOS多线程总结)

// 获得主线程
+ (NSThread *)mainThread;    

// 判断是否为主线程(对象方法)
- (BOOL)isMainThread;

// 判断是否为主线程(类方法)
+ (BOOL)isMainThread;    

// 获得当前线程
NSThread *current = [NSThread currentThread];

// 线程的名字——setter方法
- (void)setName:(NSString *)n;    

// 线程的名字——getter方法
- (NSString *)name;  

(5)线程的状态控制(启动、暂停或者阻塞,销毁强制停止)

启动:

-(void)start;

阻塞:

+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 线程进入阻塞状态

强制停止:

+ (void)exit;
// 线程进入死亡状态

(6)线程之间的通信

// 在主线程上执行操作
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
  // equivalent to the first method with kCFRunLoopCommonModes

// 在指定线程上执行操作
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);

// 在当前线程上执行操作,调用 NSObject 的 performSelector:相关方法
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

(关于线程安全部分,博客:iOS线程总结写的感觉不错,可供参考)

声明:版权,解释权属于原作者,此处只是引用增强理解。

这里试了一下iOS多线程总结中作者的例子,增强理解:

举了这样一个列子:火车票售卖。火车票数是一定的,同时有两个窗口在卖(两个线程都可以访问这个数据)。

先来看下不考虑线程安全的时候:

//初始化火车票剩余数量
      self.trainTicket = 50;
    
    //同时开启两个火车售卖窗口:(两个线程)分别叫做A窗口和B窗口。
    
    //A 窗口:
    NSThread * saleWindowA = [[NSThread alloc]initWithTarget:self selector:@selector(saleTrainTicket) object:nil];
    saleWindowA.name = @"窗口A";
    NSThread *saleWindowB = [[NSThread alloc]initWithTarget:self selector:@selector(saleTrainTicket) object:nil];
    saleWindowB.name = @"窗口B";
    //开始售卖火车票
    [saleWindowA start];
    [saleWindowB start];

//
//模拟火车票售卖
-(void)saleTrainTicket
{
    while (1) {
        if(self.trainTicket>0)
        {
            self.trainTicket--;
            NSLog(@"剩余票数:%ld,当前访问窗口:%@",self.trainTicket,[NSThread currentThread].name);
        }
        else
        {
            NSLog(@"所有的票数均已经售卖完毕");
            break;
        }
    }
    
    
}

这种情况下控制台的打印情况是:

019-03-10 12:33:05.569823+0800 TestUntilPro[25685:2263819] 剩余票数:36,当前访问窗口:窗口A
2019-03-10 12:33:05.569964+0800 TestUntilPro[25685:2263819] 剩余票数:35,当前访问窗口:窗口A
2019-03-10 12:33:05.569997+0800 TestUntilPro[25685:2263820] 剩余票数:34,当前访问窗口:窗口B
2019-03-10 12:33:05.570086+0800 TestUntilPro[25685:2263819] 剩余票数:33,当前访问窗口:窗口A
2019-03-10 12:33:05.570212+0800 TestUntilPro[25685:2263820] 剩余票数:32,当前访问窗口:窗口B
2019-03-10 12:33:05.570289+0800 TestUntilPro[25685:2263819] 剩余票数:31,当前访问窗口:窗口A
2019-03-10 12:33:05.570404+0800 TestUntilPro[25685:2263820] 剩余票数:30,当前访问窗口:窗口B
2019-03-10 12:33:05.600906+0800 TestUntilPro[25685:2263819] 剩余票数:29,当前访问窗口:窗口A
2019-03-10 12:33:05.600906+0800 TestUntilPro[25685:2263820] 剩余票数:29,当前访问窗口:窗口B
2019-03-10 12:33:05.601202+0800 TestUntilPro[25685:2263820] 剩余票数:28,当前访问窗口:窗口B
2019-03-10 12:33:05.601258+0800 TestUntilPro[25685:2263819] 剩余票数:27,当前访问窗口:窗口A
2019-03-10 12:33:05.601361+0800 TestUntilPro[25685:2263820] 剩余票数:26,当前访问窗口:窗口B
2019-03-10 12:33:05.601371+0800 TestUntilPro[25685:2263819] 剩余票数:25,当前访问窗口:窗口A
2019-03-10 12:33:05.601498+0800 TestUntilPro[25685:2263820] 剩余票数:24,当前访问窗口:窗口B
2019-03-10 12:33:05.601521+0800 TestUntilPro[25685:2263819] 剩余票数:23,当前访问窗口:窗口A
2019-03-10 12:33:05.601717+0800 TestUntilPro[25685:2263820] 剩余票数:22,当前访问窗口:窗口B
2019-03-10 12:33:05.601724+0800 TestUntilPro[25685:2263819] 剩余票数:21,当前访问窗口:窗口A
2019-03-10 12:33:05.617636+0800 TestUntilPro[25685:2263820] 剩余票数:20,当前访问窗口:窗口B
2019-03-10 12:33:05.617636+0800 TestUntilPro[25685:2263819] 剩余票数:19,当前访问窗口:窗口A
2019-03-10 12:33:05.617754+0800 TestUntilPro[25685:2263820] 剩余票数:18,当前访问窗口:窗口B

可以看到,总有几次数句是不准的,不符合我们的预期。

这就需要线程同步的处理。

给线程加锁,在一个线程操作这个数据的时候不允许其他的线程操作。

@sychronized来保证线程安全,考虑了线程安全的代码如下:

//模拟火车票售卖
-(void)saleTrainTicket
{
    while (1) {
        @synchronized (self)
        {
            if(self.trainTicket>0)
            {
                self.trainTicket--;
                NSLog(@"剩余票数:%ld,当前访问窗口:%@",self.trainTicket,[NSThread currentThread].name);
                 [NSThread sleepForTimeInterval:2];
            }
            else
            {
                NSLog(@"所有的票数均已经售卖完毕");
                break;
            }
        }
    }
}

控制台打印效果:

2019-03-13 10:28:28.575372+0800 MotherBaby[6479:706449] 49
2019-03-13 10:28:28.575661+0800 MotherBaby[6479:706449] 当前访问窗口:{number = 4, name = 窗口A}
2019-03-13 10:28:30.579055+0800 MotherBaby[6479:706449] 48
2019-03-13 10:28:30.579583+0800 MotherBaby[6479:706449] 当前访问窗口:{number = 4, name = 窗口A}
2019-03-13 10:28:32.585009+0800 MotherBaby[6479:706449] 47
2019-03-13 10:28:32.585405+0800 MotherBaby[6479:706449] 当前访问窗口:{number = 4, name = 窗口A}
2019-03-13 10:28:34.590772+0800 MotherBaby[6479:706449] 46
2019-03-13 10:28:34.591913+0800 MotherBaby[6479:706449] 当前访问窗口:{number = 4, name = 窗口A}
2019-03-13 10:28:36.596916+0800 MotherBaby[6479:706449] 45
2019-03-13 10:28:36.597151+0800 MotherBaby[6479:706449] 当前访问窗口:{number = 4, name = 窗口A}
2019-03-13 10:28:38.600658+0800 MotherBaby[6479:706449] 44
2019-03-13 10:28:38.601208+0800 MotherBaby[6479:706449] 当前访问窗口:{number = 4, name = 窗口A}
2019-03-13 10:28:40.604369+0800 MotherBaby[6479:706449] 43
2019-03-13 10:28:40.604776+0800 MotherBaby[6479:706449] 当前访问窗口:{number = 4, name = 窗口A}
2019-03-13 10:28:42.609203+0800 MotherBaby[6479:706448] 42
2019-03-13 10:28:42.609677+0800 MotherBaby[6479:706448] 当前访问窗口:{number = 3, name = 窗口B}
2019-03-13 10:28:44.614921+0800 MotherBaby[6479:706448] 41
2019-03-13 10:28:44.615339+0800 MotherBaby[6479:706448] 当前访问窗口:{number = 3, name = 窗口B}
2019-03-13 10:28:46.619954+0800 MotherBaby[6479:706448] 40
2019-03-13 10:28:46.620611+0800 MotherBaby[6479:706448] 当前访问窗口:{number = 3, name = 窗口B}
2019-03-13 10:28:48.624240+0800 MotherBaby[6479:706448] 39
2019-03-13 10:28:48.625007+0800 MotherBaby[6479:706448] 当前访问窗口:{number = 3, name = 窗口B}
2019-03-13 10:28:50.627750+0800 MotherBaby[6479:706448] 38
2019-03-13 10:28:50.628121+0800 MotherBaby[6479:706448] 当前访问窗口:{number = 3, name = 窗口B}

可以看到这个是我们想要的结果。保证了线程同步。

你可能感兴趣的:(iOS 多线程之NSThread)