「IOS」OC中多线程的使用总结

一. 概念说明

1. 进程(process):

系统中正在运行的一个程序的实例,具有一定的独立功能,是线程的容器。

2.线程(thread):

线程是进程执行的最小单位,一个进程中至少包含一个线程(主线程),进程中任务都在线程中执行(主线程或子线程)。

3.并行:

两个或多个事件在同一时间发生。比如多人赛跑,当开始指令发出后,运动员同一时间(理想情况)起跑。

4.并发:

两个或多个事件在同一时间间隔发生。比如单核计算机,实际上是多个线程之间不断切换以达到多线程执行任务的目的。

5.串行:

多个任务依次执行,当一个任务完成以后继续执行下一个任务。例如A,B,C三个任务,A执行完毕执行B,B执行完毕执行C。

二 .OC实现多线程的方法

这里插入以下创建用户线程所需的成本:
内核数据结构(大约1KB)
栈空间(子线程512KB)
主线程(1KB)
可以通过setStackSize设置栈空间的大小,但必须是4K的倍数,而且最小是16K
创建线程大约需要90ms的创建时间

PS:开启大量线程,会降低程序的性能
线程越多,CPU在调度线程上的开销就越大

1. pthread

OSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准,是基于C语言实现的跨平台的线程API。头文件为 : pthread.h

由于pthread创建的线程是C语言实现的,生命周期由程序员管理,在OC中使用频率很低(几乎不用).

实现方法:

#import <Foundation/Foundation.h>
#import <pthread.h>

//方法声明
void * runPthread(void * param);

int main(int argc, const char * argv[]) {
   
    /*
     参数说明:
     pthread_t   线程ID
     pthread_attr_t  线程属性,设置为NULL表示采用默认属性
     void * (*)(void *) 执行线程的方法体
     void *restrict 参数
     */
    
    //线程ID
    pthread_t pthreadID;
    
    //创建参数
    NSString * param = @"我是参数";
    
    //创建线程
    int result = pthread_create(&pthreadID, NULL, &runPthread, (__bridge void *)(param));
    
    if(!result){
        
        NSLog(@"线程创建成功");
        
    }else NSLog(@"线程创建失败 : %d", result);
    
    return 0;
}

void * runPthread(void * param) {
    
    NSLog(@"执行线程:%@", [NSThread currentThread]);
    NSLog(@"参数为: %@", param);
    return NULL;
    
}

//PS: C语言中的void * 相当于OC中的id, 将OC对象作为C语言参数时,在ARC环境下需要使用__bridge做桥接,MRC环境下不需要

输出结果:


pthread.png

2. NSThread

一个NSThread对象就代表一条线程。创建一个新的线程可以使用类方法或对象方法。因为NSThread属于OC方法,所以NSThread的创建需要在OC类内实现。

通过类方法创建一条NSThread

- (instancetype)init {
    
    self = [super init];
    
    if(self){
        
        //开启一条新的线程
        [NSThread detachNewThreadSelector:@selector(threadMethod) toTarget:self withObject:nil];
        
        //使用block方法创建注意版本问题
//        [NSThread detachNewThreadWithBlock:^{
//
//        }];
        
    }
    
    return self;
    
}

/**
 线程方法
 */
- (void)threadMethod {
    
    NSLog(@"当前线程:%@", [NSThread currentThread]);
    
}

执行结果:


NSThread-1.png

其他一些基本的类方法或属性介绍:

  [NSThread currentThread];  //获取当前线程
  [NSThread isMultiThreaded]; //是否是在多线程内
  [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];  //睡眠时间(从现在开始1秒),参数为NSDate类型,可自行设置
  [NSThread sleepForTimeInterval:1];  //睡眠一秒(时间秒)
  [NSThread exit];  //退出当前线程
  [NSThread threadPriority];  //获取线程优先级
  [NSThread setThreadPriority:1]; //设置线程优先级(通常不用设置),值范围0.0~1.0,默认值为0.5,值越大优先级越高,但并不保证优先级高的任务一定会先执行。
  [NSThread isMainThread]; //是否为主线程
  [NSThread callStackReturnAddresses];  //获取当前线程函数调用的栈地址的数组
  [NSThread callStackSymbols];  //获取当前线程调用的栈符号

通过类方法创建的线程比较容易,并且创建以后直接就开始执行。但是对于线程的一些属性的设置无法达到。所以也可以通过实例对象的方式来创建线程。

- (instancetype)init {
    
    self = [super init];
    
    if(self){
        
//        //调用block方法创建线程注意下版本问题
//        NSThread * newThread = [[NSThread alloc] initWithBlock:^{
//
//        }];
        
        //创建新的线程
        NSThread * newThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMethod) object:nil];
        newThread.name = @"new thread";  //设置线程名称,线程名称的设置有助于在排查线程问题时可以定位到具体线程的名字,方便查找问题
newThread.qualityOfService = NSQualityOfServiceDefault;  //线程服务质量,IOS8.0后新属性,类似于线程优先级。

//        NSQualityOfServiceUserInteractive 最高优先级,主要用于提供交互UI的操作
//        NSQualityOfServiceUserInitiated 次高优先级,主要用于执行需要立即返回的任务
//        NSQualityOfServiceDefault 默认优先级,当没有设置优先级的时候,线程默认优先级
//        NSQualityOfServiceUtility 普通优先级,主要用于不需要立即返回的任务
//        NSQualityOfServiceBackground 后台优先级,用于完全不紧急的任务

        NSLog(@"线程字典:%@", newThread.threadDictionary);
        [newThread start];  //开启线程

    }
    
    return self;
    
}

/**
 线程方法
 */
- (void)threadMethod {
    
    NSThread * currentThread = [NSThread currentThread];
    NSUInteger stackSize = [currentThread stackSize];  //获取当前线程占用的栈空间
    NSLog(@"当前线程:%@  %lu", currentThread, (unsigned long)stackSize);
    
    if([currentThread isMainThread]){  //通过实例方法判断是否为主线程
        
        NSLog(@"是主线程");
        
    }else NSLog(@"非主线程");
    
    if([currentThread isExecuting]){   //判断线程是否处于执行状态
        
        [currentThread cancel];    //结束线程,
        NSLog(@"结束线程");
   
    }
    
    if([currentThread isCancelled]){  //判断线程是否被结束
        
        NSLog(@"被动结束线程");
        
    }else NSLog(@"没有被动结束线程");
    
    if([currentThread isFinished]){  //判断线程是否执行完毕
        
        NSLog(@"执行完毕");
        
    }else NSLog(@"还在执行");
    
    NSLog(@"这是线程函数的结尾");
    
}

PS:退出线程的方法中有实例方法cancel和类方法exit的说明:
cancel结束当前线程,但是线程方法中cancel以后的代码还会继续执行,直到方法结束;exit 强行退出线程,exit后的方法不会在执行;当在主线程中调用exit后,线程将进入死亡状态,无法再次开启,界面不会崩溃,但无法操作。

在NSThread类中,还有main方法,当我们通过以下方法创建一条thread时,可以通过继承的方式重写main方法(线程的入口函数)

CustomThread * newThread = [[CustomThread alloc] init];

在NSThread类中可以看到NSObject的一些方法,为NSObject (NSThreadPerformAdditions)分类方法,那么就是说所有的NSObject对象都可以调用的方法,有以下(以下方法为主线程与子线程之间通讯的方法):

/**
调用callMainThreadMethod方法,从当前子线程中返回主线程执行此方法

@param callMainThreadMethod 回调主线程执行的方法
@param 参数
@param waitUntilDone 参数值为YES/NO,当设置YES时,会阻塞当前线程,直到被调用方法执行完毕继续执行。NO不阻塞,继续执行当前线程中的方法;当在主线程调用此方法时,设置为YES无效,等同于设置NO,不会阻塞主线程
*/
[self performSelectorOnMainThread:@selector(callMainThreadMethod) withObject:nil waitUntilDone:NO];

 /**
在执行线程中执行被调用的方法

@param callThreadMethod 被调用的方法
@param onThread 指定线程
@param withObject 参数
@param waitUntilDone 参数值为YES/NO,当设置YES时,会阻塞当前线程,直到被调用方法执行完毕继续执行。NO不阻塞,继续执行当前线程中的方法;当在主线程调用此方法时,设置为YES无效,等同于设置NO,不会阻塞主线程
*/
[self performSelector:@selector(callThreadMethod) onThread:newThread withObject:nil waitUntilDone:NO];

/**
在后台(子线程)执行指定方法

@param callBackgroundThreadMethod 被调用方法
@param withObject 参数
*/
[self performSelectorInBackground:@selector(callBackgroundThreadMethod) withObject:nil];
 
/**
当modes参数设置为NSRunLoopCommonModes时,功能同上不带modes参数的方法,此方法可自行设置RunLoop模式

@param callMainThreadMethod 被调用的方法
@param withObject 参数
@param waitUntilDone 参数值为YES/NO,当设置YES时,会阻塞当前线程,直到被调用方法执行完毕继续执行。NO不阻塞,继续执行当前线程中的方法;当在主线程调用此方法时,设置为YES无效,等同于设置NO,不会阻塞主线程
*/
[self performSelectorOnMainThread:@selector(callMainThreadMethod) withObject:nil waitUntilDone:NO modes:@[NSRunLoopCommonModes]];

/**
当modes参数设置为NSRunLoopCommonModes时,功能同上不带modes参数的方法,此方法可自行设置RunLoop模式

@param callThreadMethod 被调用的方法
@param withObject 参数
@param waitUntilDone 参数值为YES/NO,当设置YES时,会阻塞当前线程,直到被调用方法执行完毕继续执行。NO不阻塞,继续执行当前线程中的方法;当在主线程调用此方法时,设置为YES无效,等同于设置NO,不会阻塞主线程
*/
[self performSelector:@selector(callThreadMethod) onThread:newThread withObject:nil waitUntilDone:NO modes:@[NSRunLoopCommonModes]];

那么当我们使用

[self performSelector:@selector(callThreadMethod) onThread:newThread withObject:nil waitUntilDone:NO];

方法的时候,需要指定一条线程来运行指定方法,需要特殊说明和处理一下,看下面的程序:

- (instancetype)init {
    
    self = [super init];
    
    if(self){

        //创建新的线程
        NSThread * newThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMethod) object:nil];
        newThread.name = @"new thread";  //设置线程名称,线程名称的设置有助于在排查线程问题时可以定位到具体线程的名字,方便查找问题
        [newThread start];  //开启线程
        
        [self performSelector:@selector(onThreadMethod) onThread:newThread withObject:nil waitUntilDone:NO];
        
    }
    
    return self;
    
}

/**
 线程方法
 */
- (void)threadMethod {
    
    NSLog(@"func:%s", __func__);
    
}

/**
 在指定线程上运行的指定方法
 */
- (void)onThreadMethod {
    
    NSLog(@"func:%s", __func__);
    
}

运行结果:


thread-2.png

我们发现onThreadMethod方法并没有执行,因为我们创建的newThread在调用onThreadMethod方法时,线程已经运行结束了,所以无法调用方法的执行。我们都知道每条线程都有一个RunLoop的存在,主线程的RunLoop默认是开启状态,而子线程默认是非开启状态,如果我们想让线程一直持续不断的执行那么就要开启这个线程的RunLoop。

/**
 线程方法
 */
- (void)threadMethod {
    
    NSLog(@"func:%s", __func__);
    
    //开启RunLoop,因为要开启当前线程的RunLoop,所以需要在线程执行的方法中开启
    [[NSRunLoop currentRunLoop] run];
    
}

执行结果如下:


thread-3.png

但是当我们把开启RunLoop的代码放在NSLog上面时,我们看打印结果会发现,threadMethod方法的打印并没有出现,结果如下:

thread-4.png

通过结果可以判断出,当通过[[NSRunLoop currentRunLoop] run];方法开启RunLoop以后会执行一个死循环,无休止的执行,而且无法关闭,所以就无法执行开启代码后的任何代码,那么怎么解决这个问题呢?看以下代码:

/**
 线程方法
 */
- (void)threadMethod {
    
    //开启RunLoop,因为要开启当前线程的RunLoop,所以需要在线程执行的方法中开启
    while (YES) {
    
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        
    }
    
     NSLog(@"func:%s", __func__);
    
}

这个while循环的功能与上面的功能类似,执行结果也是一样的,但是我们可以通过while循环的条件来控制RunLoop的是否执行,定义一个BOOL值,当我们方法执行完毕以后将BOOL值设置为假即可。如下:

@interface Person()

@property (nonatomic, assign, getter=isFinished) BOOL finished;

@end

@implementation Person

- (instancetype)init {
    
    self = [super init];
    
    if(self){

        //设置初始值
        _finished = YES;
        
        //创建新的线程
        NSThread * newThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMethod) object:nil];
        newThread.name = @"new thread";  //设置线程名称,线程名称的设置有助于在排查线程问题时可以定位到具体线程的名字,方便查找问题
        [newThread start];  //开启线程
        
        [self performSelector:@selector(onThreadMethod) onThread:newThread withObject:nil waitUntilDone:NO];
        
    }
    
    return self;
    
}

/**
 线程方法
 */
- (void)threadMethod {
    
    //开启RunLoop,因为要开启当前线程的RunLoop,所以需要在线程执行的方法中开启
    while (self.isFinished) {
    
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        
    }
    
     NSLog(@"func:%s", __func__);
    
}

/**
 在指定线程上运行的指定方法
 */
- (void)onThreadMethod {
    
    NSLog(@"func:%s", __func__);
    self.finished = NO;
    
}

这样就完美的执行了。

下面简单介绍下OC中线程的处理过程:

当我们创建一条新的线程后,在内存中存在一块区域为线程池,当调用start方法后,线程会进入就绪状态,此时线程被添加到可调度线程池中,由CPU切换调用,当CPU调度到该线程时,线程处于运行状态,当CPU调度到其他线程时,我们的线程又处于就绪状态等待调度,如此往复。当线程处于运行状态时,如果线程中存在阻塞方法(sleep或同步锁阻塞),则线程处于阻塞状态。当线程处于阻塞状态时,线程会被移出可调度线程池,待线程处于非阻塞状态时,再次调度到可调度线程池。当线程执行完毕、强制退出、异常等情况,则线程死亡,该线程会从可调度线程池中移除销毁。

三. 线程锁

之所以引入线程锁的概念是因为资源共享的问题。当我们通过不同的线程同时操作一块内存数据时,就可能发生问题。因为网上例子很多,这里只介绍下锁的概念和基本使用方法:

1. POSIX Mutex Lock

跨平台锁,C语言实现

//创建锁
pthread_mutex_t mutex;
    
//初始化锁
pthread_mutex_init(&mutex, NULL);
    
//加锁
pthread_mutex_lock(&mutex);
    
//解锁
pthread_mutex_unlock(&mutex);

//来释放锁数据结构
pthread_mutex_destroy(&mutex);

PS:以上只是方法说明,具体的使用请放在具体的函数中使用。pthread_mutex_t定义为全局,pthread_mutex_lock,pthread_mutex_unlock放在具体的需要加锁的代码块中。

2.@synchronized 指令

@synchronized隐式的创建一种其他锁能实现的功能,通过不同线程共用同一标识符的方式来达到互斥的目的。格式如下:

@synchronized(anyObject) {
  
    //需要被保护的代码段
    
    }

anyObject为任意id类型,一般使用self

@synchronized块隐式地将异常处理程序添加到受保护的代码中。这个处理程序会在抛出异常时自动释放互斥锁。这意味着,为了使用@synchronized指令,您还必须在代码中启用Objective-C异常处理

以上引自官方文档。

PS:anyObject必须为不同线程共用的对象,如果是局部变量则达不到保护的目的。

@synchronized指令隐式的添加了一些异常处理程序,所以会带来一定的额外开销,如果大量使用synchronized指令则效率较低。

3.NSLock

最基本的互斥锁。OC中,所有锁都实现是了NSLocking协议(除NSDistributedLock外),而基本的锁和解锁方法也就定义在NSLocking中。

基本使用代码如下:

 //创建锁对象
NSLock * lock = [[NSLock alloc] init];
lock.name = @"NSLock 锁";  //设置锁名称

if([lock tryLock]){   //tryLock 尝试获取锁,失败返回NO,不会阻塞当前线程。
 
    //需要被保护的代码段
    [lock unlock];  //解锁
    
    }
   
//    [lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];// 在指定时间之前获取锁,此方法会阻塞当前线程,直到时间结束,如果在指定时间内未获取到锁则返回NO。
//     [lock lock]; //加锁

4.NSRecursiveLock递归锁

“递归锁” 顾名思义,常用在递归函数中以防止递归阻塞线程。当然也可以在非递归函数中使用。NSRecursiveLock定义的锁,可以被同一线程多次获取,而不会导致死锁问题。递归锁会记录成功获取的次数。每个成功锁的获取必须通过相应的调用来平衡解锁g该锁。只有当锁和解锁调用彼此平衡时,锁才会被释放,其他线程才能获取。使用属性与NSLock相同。

//创建递归锁
NSRecursiveLock * recursiveLock = [[NSRecursiveLock alloc] init];

void recursiveMethod(int x) {
    
    [recursiveLock lock];   //加锁
    
    if(x != 0){
        
        --x;
        recursiveMethod(x);
        
    }
    
    [recursiveLock unlock]; //解锁
    
}

PS:在递归函数中加锁,如不使用递归锁则会造成死锁,线程将会被锁死。

因为在获取锁与解锁平衡之前,递归锁不会被释放,所以会消耗一定的性能。长时间持有递归所会导致其他线程阻塞,直到递归完成。所以如果对性能需求较高,可以通过重写代码来消除递归,可或得更好的性能。

5.NSConditionLock条件锁

顾名思义,条件锁根据特定的条件进行锁或者解锁,
通常,当线程需要以特定顺序执行任务时,使用条件锁。可以使用任何组合进行锁操作。根据需求进行组合。此锁在线程没有获取到锁的情况下会阻塞,即使用特定条件在队列上进行等待。

//创建条件锁
NSConditionLock * conditionLock = [[NSConditionLock alloc] initWithCondition:6];

NSLog(@"条件锁的条件:%ld", (long)conditionLock.condition);
        
//开启线程
[NSThread detachNewThreadWithBlock:^{
       
    for(int i = 0; i < 10; i++){
 
    //尝试当条件成立时加锁
    BOOL isLock = [conditionLock tryLockWhenCondition:i];
  
        if(isLock){
   
            NSLog(@"加锁成功:%d", i);
   
            //根据条件解锁
            [conditionLock unlockWithCondition:6];
//          [conditionLock unlock];  //解锁
                    
        }else NSLog(@"加锁失败:%d", i);
 
   }
   
//        conditionLock.name = @"NSConditionLock";  //锁名称
//        [conditionLock tryLock]; //尝试加锁,成功YES,失败NO
//        [conditionLock unlock]; //解锁
//        [conditionLock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:1]];  //在指定时间前加锁,成功YES,失败NO
//        [conditionLock lockWhenCondition:6 beforeDate:[NSDate dateWithTimeIntervalSinceNow:1]]; //在指定时间前根据指定条件加锁,成功YES,失败NO
}];

6.NSDistributedLock分布锁

在多个主机上的多个应用程序中,可以使用NSDistributedLock类来限制对某些共享资源(文件)的访问。锁实际上是使用文件系统项(文件或目录)来实现锁的。所有使用NSDistributedLock对象的应用程序都必须可以写锁。这就意味着把它放在一个文件系统上,所有运行该应用程序的计算机都可以访问它。

PS:以上翻译自官方文档,如果错误请指正。

上面也说过,NSDistributedLock是不遵循NSLocking协议的,所以没有锁方法。锁方法将阻塞线程的执行,并要求系统以预定的速度轮询锁定。当然这个决定权在开发者手中,可通过tryLock方法来决定是否轮询。
如果持有锁的进程在释放前挂掉,那么锁将一直得不到释放,可通过breakLock强行获取锁。

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

推荐阅读更多精彩内容