iOS 多线程(1)

1.线程和进程的定义

1.线程
  • 线程是进城的基本执行单元,一个进程的所有任务都在线程中执行。
  • 1个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程,称为主线程)。
  • 程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程。
2.进程
  • 进程是指在系统中正在运行的一个应用程序。
  • 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。
3. 进程和线程的关系
  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
  • 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
  • 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
    执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  • 线程是处理器调度的基本单位,但是进程不是。
4.多线程
  • 定义:1个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务。
  • 原理:同一时间,CPU只能处理1条线程,只有1条线程在工作(执行),多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。
    那么如果线程非常非常多,会发生什么情况?
    CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源,同时每条线程被调度执行的频次也会会降低(线程的执行效率降低)。因此我们一般只开3-5条线程。
  • 优缺点:
    优点:1. 能适当提高程序的执行效率。2. 能适当提高资源利用率(CPU、内存利用率)。
    缺点:创建线程是有开销的,iOS下主要成本包括:内核数据结构(大约1KB)、栈空间(子线程512KB、主线程1MB,也可以使用-setStackSize:设置,但必须是4K的倍数,而且最小是16K),创建线程大约需要90毫秒的创建时间,如果开启大量的线程,会降低程序的性能,线程越多,CPU在调度线程上的开销就越大。程序设计更加复杂:比如线程之间的通信、多线程的数据共享等问题。

2.多线程的实现方案

image.png
1.pthread的简单使用
/**
     pthread_create 创建线程
     参数:
     1. pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀 `_t / Ref` 结尾
     同时不需要 `*`
     2. 线程的属性,nil(空对象 - OC 使用的) / NULL(空地址,0 C 使用的)
     3. 线程要执行的`函数地址`
     void *: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似
     (*): 函数名
     (void *): 参数类型,void *
     4. 传递给第三个参数(函数)的`参数`
     
     返回值:C 语言框架中非常常见
     int
     0          创建线程成功!成功只有一种可能
     非 0       创建线程失败的错误码,失败有多种可能!
  */
 // 1: pthread
    pthread_t threadId = NULL;
    //c字符串
    char *cString = "HelloCode";
//    NSString *ocString = @"Gavin";
    //延伸到: OC--C的混编 尤其在智能家居,SDK封装
    //抛出一个问题: 在ARC需要这样操作,在MRC不需要
    // OC prethread -- 跨平台
    // 锁
 int result = pthread_create(&threadId, NULL, pthreadTest, cString);
 if (result == 0) {
    NSLog(@"成功");
 } else {
    NSLog(@"失败");
 }
 void *pthreadTest(void *para){
     // 接 C 语言的字符串
     //    NSLog(@"===> %@ %s", [NSThread currentThread],  para);
     // __bridge 将 C 语言的类型桥接到 OC 的类型
   //    NSString *name = (__bridge NSString *)(para);
     NSLog(@"===>%@", [NSThread currentThread]);
     return NULL;
}

2.NSThread的使用
2.1 NSTread有3种创建方式,分别是
  • init方式 :
/** 方法一,需要start */
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething1:) object:@"NSThread1"];
// 线程加入线程池等待CPU调度,时间很快,几乎是立刻执行
[thread1 start];
- (void)doSomething1:(NSObject *)object {
    // 传递过来的参数
    NSLog(@"%@",object);
    NSLog(@"doSomething1:%@",[NSThread currentThread]);
}
  • detachNewThreadSelector创建好之后自动启动
/** 方法二,创建好之后自动启动 */
[NSThread detachNewThreadSelector:@selector(doSomething2:) toTarget:self withObject:@"NSThread2"];
- (void)doSomething2:(NSObject *)object {
    NSLog(@"%@",object);
    NSLog(@"doSomething2:%@",[NSThread currentThread]);
}
  • performSelectorInBackground创建好之后也是直接启动
/** 方法三,隐式创建,直接启动 */
[self performSelectorInBackground:@selector(doSomething3:) withObject:@"NSThread3"];
- (void)doSomething3:(NSObject *)object {
    NSLog(@"%@",object);
    NSLog(@"doSomething3:%@",[NSThread currentThread]);
}
  • 后面两种方法都不用我们开启线程,相对方便快捷,但是没有办法拿到子线程对象,没有办法对子线程进行更详细的设置,例如线程名字和优先级等。
2.2 NSThread的方法和属性
// 当前线程
[NSThread currentThread];
NSLog(@"%@",[NSThread currentThread]);

// 如果number=1,则表示在主线程,否则是子线程
打印结果:{number = 1, name = main}
//休眠多久
[NSThread sleepForTimeInterval:2];
//休眠到指定时间
[NSThread sleepUntilDate:[NSDate date]];
//退出线程
[NSThread exit];
//判断当前线程是否是多线程
[NSThread isMultiThreaded];
//主线程的对象
NSThread *mainThread = [NSThread mainThread];
//线程是否在执行
thread.isExecuting;
//线程是否被取消
thread.isCancelled;
//线程是否完成
thread.isFinished;
//是否是主线程
thread.isMainThread;
 // 获取当前线程优先级
 + (double)threadPriority;
 // 设置线程优先级 默认为0.5 取值范围为0.0 - 1.0 
 // 1.0优先级最高
 // 设置优先级
 + (BOOL)setThreadPriority:(double)p;
 // 获取指定线程的优先级
 - (double)threadPriority NS_AVAILABLE(10_6, 4_0);
 - (void)setThreadPriority:(double)p NS_AVAILABLE(10_6, 4_0);
// 设置线程的名字
 - (void)setName:(NSString *)n NS_AVAILABLE(10_5, 2_0);
 - (NSString *)name NS_AVAILABLE(10_5, 2_0);
// 判断指定的线程是否是 主线程
 - (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0);
 // 获取主线程
 + (NSThread *)mainThread NS_AVAILABLE(10_5, 2_0);
 - (void)cancel NS_AVAILABLE(10_5, 2_0);
 // 启动线程
 - (void)start NS_AVAILABLE(10_5, 2_0);
 // 线程主函数  在线程中执行的函数 都要在-main函数中调用,自定义线程中重写-main方法
 - (void)main NS_AVAILABLE(10_5, 2_0);
2.3 NSThread线程的生命周期
image.png
启动线程
- (void)start; 
// 进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态

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

强制停止线程
+ (void)exit;
// 进入死亡状态
2.4线程的安全隐患
  • 多线程安全隐患的原因:1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,比如多个线程访问同一个对象、同一个变量、同一个文件。
    那么当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。
  • 安全隐患解决
    我们可以看出,当线程A访问数据并对数据进行操作的时候,数据被加上一把锁,这个时候其他线程都无法访问数据,知道线程A结束返回数据,线程B此时在访问数据并修改,就不会造成数据错乱了。
    下面我们来看一下互斥锁的使用:
    互斥锁使用格式
@synchronized(锁对象) { 
// 需要锁定的代码  
}

互斥锁的使用前提:多条线程抢夺同一块资源时.
作用:

  • 保证锁内的代码,同一时间,只有一条线程能够执行!
  • 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差!
  • 注意:
    1.锁定1份代码只用1把锁,用多把锁是无效的。
    2.能够加锁的任意 NSObject 对象。
    3.锁对象一定要保证所有的线程都能够访问。
    4.如果代码中只有一个地方需要加锁,大多都使用 self,这样 可以避免单独再创建一个锁对象。
2.5 NSThread线程之间的通信

定义:在同一个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信,例如我们在子线程完成下载图片后,回到主线程刷新UI显示图片。
体现:1个线程传递数据给另1个线程,在1个线程中执行完特定任务后,转到另1个线程继续执行任务。
线程间通信常用的方法:

// 返回主线程
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
// 返回指定线程
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;

举个栗子:

#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [NSThread detachNewThreadSelector:@selector(donwLoadImage) toTarget:self withObject:nil];
}
-(void)donwLoadImage
{
    // 获取图片url地址 http://www.itunes123.com/uploadfile/2016/0421/20160421014340186.jpg
    NSURL *url = [NSURL URLWithString:@"http://www.itunes123.com/uploadfile/2016/0421/20160421014340186.jpg"];
    // 下载图片二进制文件
    NSData *data = [NSData dataWithContentsOfURL:url];
    // 将图片二进制文件转化为image;
    UIImage *image = [UIImage imageWithData:data];
    // 参数 waitUntilDone 是否等@selector(showImage:) 执行完毕以后再执行下面的操作  YES :等 NO:不等
    // 返回主线程显示图片
    // [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
    // self.imageView 也可以直接调用这个方法 直接选择 setImage方法,传入参数image即可
    // [self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
    // 返回特定的线程,[NSThread mainThread] 获得主线程
    [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
}
-(void)showImage:(UIImage *)image
{
    self.imageView.image = image;
}
@end
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容