一、内存管理

1、引用计数(Reference Counting)

引用计数是Objective-C中的内存管理方式。
对象操作与Objective-C中方法的对应关系:

对象操作 Objective-C方法
生成并持有对象 alloc/new/copy/mutableCopy
持有对象 retain
释放对象 release
废弃对象 dealloc

使用以下名称开头的方法名,将自己生成并持有对象

  • allocXXX
  • newXXX
  • copyXXX
  • mutableCopyXXX

    copy方法基于NSCopying协议方法约定,由各类实现的copyWithZone:方法生成并持有对象的副本 mutableCopy与copy类似,基于NSMutableCopying协议 两者的区别: 1.copy生成不可变更的对象 2.mutableCopy生成可变更的对象

autorelease

autorelease顾名思义就是自动释放。调用autorelease会在变量超过自身作用域的时候自动调用release进行释放。autorelease作用域就需要提及NSAutoreleasePool对象池。
使用过程

1.生成并持有NSAutoreleasePool对象 2.调用已经分配对象的autorelease方法 3.废弃NSAutoreleasePool对象

  1. NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; //创建pool
  2. id obj = [[NSObject alloc] init];
  3. [obj autorelease]; //调用autorelease
  4. [pool drain]; // 废弃pool

2、ARC(Automatic Reference Counting)

ARC是指内存管理中对引用采取自动计数的计数。
在Objective-C中采用ARC机制是让编译器来进行内存管理。
满足ARC的条件:

  • 使用xcode 4.2或以上版本
  • 使用LLVM编译器3.0或以上版本
  • 编译器选项中设置ARC为有效(-fobjc-arc)

所有权修饰符

  • __strong
  • __weak
  • __unsafe_unretained
  • __autoreleasing

__strong
strong是id类型和对象类型的默认所有权修饰符。strong修饰符的变量obj在超出其变量作用域时,即在改变量被废弃时,会释放其被赋予的对象。
__weak
weak修饰符提供弱引用,自身不能持有引用对象,引用对象释放之后weak修饰的变量会自动设置为nil。
weak可以避免 strong修饰符中引发的循环引用问题,造成内存泄漏。
__unsafe_unretained
unsafe_unretained是不安全的修饰符,尽管ARC下的内存管理是编译器的工作,但unsafe_unretained修饰符的变量不属于编译器的内存管理对象。同weak一样,**unsafe_unretained不持有引用对象。
weak的区别是unsafe_unretained并不会在引用对象时自动设置为nil,所以在使用过程中需要手动设置为nil,否则会造成野指针问题。
__autoreleasing**
需要@autoreleasepool一起配合使用。

  1. @autoreleasepool {
  2. id __autoreleasing obj = [[NSObject alloc] init];
  3. }

访问附有weak修饰符的变量时,必定要访问注册到autoreleasepool的对象。**这是因为weak只持有对象的弱引用,而在访问引用对象的过程中,该对象有可能被废弃,如果要访问的对象注册到autoreleasepool中,那么在@autoreleasepool块结束之前都能确保该对象存在。
id的指针或对象的指针在没有显示指定会被附加上autoreleasing修饰符。例子:id *obj; `NSObject obj;等价于NSObject * autoreleasing obj; NSError *error;`出现过的例子方法:

  1. - (BOOL)performOperationWithError:(NSError * __autoreleaseing *)error;

ARC下遵循的规则:

  • 不能使用retain/release/retainCount/autorelease
  • 不能使用NSAllocateObject/NSDeallocateObject
  • 须遵守内存管理的方法命名规则
  • 不要显示的调用dealloc
  • 使用@autoreleasepool替代NSAutoreleasePool
  • 不能使用区域NSZone
  • 对象型变量不能作为C语言结构体(struct/union)的成员
  • 显示转换’id’和’void*’

主意的点:

  • dealloc中不能调用[super dealloc];否则会编译报错
  • C语言结构体中出现Objective-C对象,会出现编译错误

Core Foundation对象和Objective-C对象没有任何区别,区别只存在与来源不同的库,所以只需要C语言的转换就能实现相互转化,这种转换不需要使用额外的CPU资源,这种转换叫做Toll-Free Bridge

  1. CFMutableArrayRef cfObj = NULL;
  2. {
  3. id obj = [NSMutableArray array];
  4. cfObj = CFBridgingRetain(obj);
  5. CFShow(cfObj);
  6. printf("retain count = %d \n",CFGetRetainCount(cfObj));
  7. }
  8. printf("retain count after = %d \n",CFGetRetainCount(cfObj));
  9. CFRelease(cfObj);

输出结果:

retain count = 2 retain count after = 1

使用__bridge关键字转化

  1. id obj2 = (__bridge id)cfObj;

属性声明与所有权修饰符的关系

属性声明的属性 所有权修饰符
assin __unsafe_unretained
copy __strong
retain __strong
strong __strong
unsafe_unretained __unsafe_unretained
weak __weak

weak散列表
**
weak修饰的对象销毁过程中,最后会从weak散列表中查找销毁对象的地址为key的记录,并在weak表中删除该记录。并将__weak修饰的变量赋值为nil。**

3、Blocks

3.1、Block基本信息

(1)什么是Block?
Blocks是C语言的扩充功能,即:带有自动变量(局部变量)的匿名函数。
Block本质是Objective-C对象
(2)Block语法

^<返回值类型><参数列表><表达式>;

  1. ^int(int count) {return count + 1;};

Blocks中void可以省略简写

  1. ^void(void){ print('xxxx'); };
  2. //等价于
  3. ^{print('xxxx');};


(3)Block类型变量**

语法: <返回值类型>(^<命名>)(<参数>) 示例:int (^Block)(int) typedef int (^Block)(int);


(4)Block对局部变量的捕获**

  1. int a = 10;
  2. void(^blk)(void) = ^{
  3. printf("a = %d \n",a);
  4. };
  5. a = 20;
  6. blk();

输出结果

a = 10

可以看出a的值并没有因为a=20改变,在执行blk()时而改变,这就是Block对局部变量的捕获
(5)__block修饰符
block可以解决局部变量在block内被赋值,如果不使用block则直接赋值会出现编译错误。
(6)Block的实质探索
将下面代码进行实质转换

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void)=^{ printf("block\n"); };
        block();
    }
}

使用clang转化之后

struct __block_impl {
  void *isa;
  int Flags; //标示
  int Reserved; //版本升级所需区域
  void *FuncPtr; //函数指针
};

// 结构体的声明
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc; 
   //构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock; // 将Block指针赋值给Block结构体成员变量isa
    impl.Flags = flags; 
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
// 参数__cself是__main_block_impl_0结构体的指针
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
 printf("block\n"); }

static struct __main_block_desc_0 {
  size_t reserved; //版本升级所学区域
  size_t Block_size; // Block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        void(*block)(void)=
            ((void (*)())&__main_block_impl_0(
                (void *)__main_block_func_0, 
                &__main_block_desc_0_DATA));
        //方法调用
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        //将转化为(*block->impl.FuncPtr)(blk)

    }
    return 0;
}

从上面转化的代码中可以看出Block最终转化为C语言结构体,出现__main_block_impl_0__main_block_func_0__main_block_desc_0__block_impl
__main_block_impl_0结构体相当于objc_object结构体的OC类对象的结构体。

我们在block中调用局部变量(自动变量)时

int a = 10;
void(^block)(void)=^{ printf("block===%d\n",a);};
block();

通过clang之后,底层源码中__main_block_impl_0 中添加了成员变量a

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a; //新添加的成员变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

从上述代码中,基本可以解释了,局部变量在block中被捕获的原因是block的底层结构体中添加了新的成员变量来接受外部的局部变量,所以在block执行的时候,实际就是赋值时的值。
Block不能直接使用C语言数组类型的自动变量
在Block中使用C语言字符数组时,会发生编译报错。

const char text[] = "hello";
void(^block)(void)=^{ printf("block===%c\n",text[2]);};
block();

报错信息:

Cannot refer to declaration with an array type inside block

__block说明符
我们知道在block中给局部变量进行赋值时,直接赋值则会引发编译错误。如果我们加上block则就能在block中进行赋值,以下我们探究下添加block之后,底层做了些什么?

 __block int a = 10;
void(^block)(void)=^{
    a = a + 10;
    printf("block===%d\n",a);
};
block();

我们使用clang编译之后探究一下:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref

            (a->__forwarding->a) = (a->__forwarding->a) + 10;
            printf("block===%d\n",(a->__forwarding->a));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        // __block int a = 10;
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
        //   void(^block)(void)=^{ a = a + 10; printf("block===%d\n",a);};               
        void(*block)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
         // block();                  
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

从以上的代码中可以看出加上__block修饰符之后,底层源码中出现结构体__Block_byref_a_0

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

__main_block_impl_0中持有__Block_byref_a_0的指针a,而__Block_byref_a_0的结构体实例成员变量__forwarding持有指向该实例自身的指针。
关于Block中的全局变量,局部变量,静态变量的探索可以阅读[*Block常见问题
](https://www.yuque.com/zhangyanneng/angtqk/alhb5e)

3.2、Block存储域

名称 实质
Block 栈上Block的结构体实例
__block 栈上__block变量的结构体实例

Block的类所处的存储域

设置对象的存储域
_NSConcreteStackBlock 栈区
_NSConcreteGlobalBlock 全局区(.data区)
_NSConcreteMallocBlock 堆区

Objective-C中的内存分区
在之前的案例中impl.isa = &_NSConcreteStackBlock;都是指向栈区的。那么全局预期的的在什么场景下出现呢?看下面的代码

// 全局的blk
void(^gblk)(void) = ^{ printf("global block \n");};


int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
}

通过clang这段代码之后,我们发现impl.isa = &_NSConcreteGlobalBlock;

struct __gblk_block_impl_0 {
  struct __block_impl impl;
  struct __gblk_block_desc_0* Desc;
  __gblk_block_impl_0(void *fp, struct __gblk_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

以上说明,全局的block的存储区域是在全局区上的。

将block作为函数返回时,编译器会自动的生成复制到堆上的代码,栈上的block会自动调用copy方法复制到堆上。实际在使用中,我们也常用copy关键字作为block的属性,或者手动复制到堆上。
Block在在使用copy方法后的行为关系

Block的类 副本源的配置存储域 copy复制效果
_NSConcreteStackBlock 从栈复制到堆上
_NSConcreteGlobalBlock 全局区 什么也不做
_NSConcreteMallocBlock 引用计数加1

Block从栈复制到堆上时对__block变量产生的影响

__block的存储域 Block从栈上复制到堆上的影响
从栈上复制打堆上并被Block持有
被Block持有

在Block中使用block变量时,则当该Block从栈上复制到堆上时,使用的所有block变量必定被配置在栈上,同时这些变量也将会全部从栈上复制到堆上。如果block变量已经在堆上则不会影响,只会被Block所持有,并增加引用计数。**栈上的block变量结构体实例在从栈上复制到堆上时,实际是将成员变量forwarding指针的值替换为复制到目标堆上的block变量用结构体实例的地址。**通过这个特性,无论在Block中,还是Block外,还是栈/堆中,都能正确的访问同一个block变量。
当配置到堆上的Block被释放的时候,其所持有的
block变量也将被释放,引用计数减1。

捕获对象时出现的copy和dispose
在__main_block_desc_0结构体中出现的copy和dispose成员变量,如下示例:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

出现copy和dispose两个成员变量的原因是Objective-C为了准确的把握Block从栈复制到堆以及堆上的Block进行释放的时机。以及在Block结构体中使用strong修饰符和weak修饰符的变量,也可以在恰当的时机进行初始化和释放。处了copy和dispose两个成员变量,还有作为指针赋值给这两成员变量的main_block_copy_0函数和main_block_dispose_0函数。

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) 
{
    _Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) 
{
    _Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

_Block_object_assign函数调用相当于retain实例方法的函数,将对象赋值在对象类型的结构体成员变量中。
_Block_object_dispose函数调用相当于release实例方法的函数,释放赋值在对象类型的结构体中的对象。

调用copy函数和dispose函数的时机

函数 调用时机
copy 函数 栈上的Block复制到堆上时
dispose 函数 堆上的Block被废弃时

BLOCK_FIELD_IS_BYREF = 8 对应的是__block变量
BLOCK_FIELD_IS_OBJECT = 3 对应的是对象
**
那么什么时候栈上的Block会复制到堆上呢?

  • 调用Block的copy实例方法时
  • Block作为函数返回值时
  • 将Block赋值给__strong修饰符id类型的类或Block类型成员变量时
  • 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时

Block复制过程归结于都是自动调用了_Block_copy函数

Block常见问题

4、内存泄漏

Block造成内存泄漏的主要问题就是循环引用问题。
如下关系图:
常见避免循环引用的方式有实用weak修饰符,block 变量,__unsafe_unretained修饰符。

在使用__block 变量时优点:

  • 通过__block变量可控制对象的持有期间
  • 在不能使用weak修饰符的环境中不使用unsafe_unretained修饰符。在执行Block时可动态地决定是否将nil或其他对象赋值在__block变量中。

使用__block的缺点:

  • 为避免循环引用必须执行Block

使用__block变量时,如果没有执行Block依然会造成循环引用问题。

二、多线程

1、进程与线程

进程:可以理解成一个运行中的应用程序,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,主要管理资源
线程:是进程的基本执行单元,一个进程对应多个线程

2、GCD

Grand Central Dispatch是异步执行任务的技术
在GCD之前,Cocoa框架提供处理异步执行任务的API

performSelectorInBackground: withObject: //后台处理任务 performSelectorOnMainThread:withObject: waitUntilDone: //回到主线程处理任务

(1)Dispatch Queue

开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中。
Dispatch Queue是执行处理的等待队列,按照FIFO的执行处理。
两种不同的执行队列:

Dispatch Queue种类 说明
Serial Dispatch Queue 等待现在执行中处理结束 (串行队列)
Concurrent Dispatch Queue 不等待现在执行中处理结束(并行队列)

(2)dispatch_queue_create

dispatch_queue_create函数用于生成Dispatch Queue

dispatch_queue_t queue = dispatch_queue_create("com.name", DISPATCH_QUEUE_SERIAL);

两个参数:

  • 第一个参数指定Queue的名称。推荐使用逆序全程域名
  • 第二个参数指定Queue的类型,NULL或DISPATCH_QUEUE_SERIAL都是表示Serial Dispatch Queue,指定为DISPATCH_QUEUE_CONCURRENT表示Concurrent Dispatch Queue

dispatch_release函数释放创建的队列,对应的也就有dispatch_retain函数来持有队列。

(3) Main Dispatch Queue / Global Dispatch Queue

Main Dispatch Queue 是Serial Dispatch Queue,因为主线程只有一个。添加到Main Dispatch Queue的处理在主线程的Runloop中执行。
Global Dispatch Queue是Concurrent Dispatch Queue。Global Dispatch Queue有4个执行优先级,分别是高优先级,默认优先级,低优先级,后台优先级。但是通过XNU内核用于Global Dispatch Queue的线程并不能保证实时性,因此执行的优先级知识大致的判断

名称 种类 说明
Main Dispatch Queue Serial Dispatch Queue 主线程执行
Global Dispatch Queue(High Priority) Concurrent Dispatch Queue 高优先级
Global Dispatch Queue(Default Priority) Concurrent Dispatch Queue 默认
Global Dispatch Queue(Low Priority) Concurrent Dispatch Queue 低优先级
Global Dispatch Queue(Background Priority) Concurrent Dispatch Queue 后台

获取全局队列,第一个参数是优先级,第二个参数作为保留字段,一般为0

// 获取全局队列
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
// 获取主队列
dispatch_get_main_queue()
参数类型 说明
DISPATCH_QUEUE_PRIORITY_HIGH 高优先级
DISPATCH_QUEUE_PRIORITY_DEFAULT 默认
DISPATCH_QUEUE_PRIORITY_LOW 低优先级
DISPATCH_QUEUE_PRIORITY_BACKGROUND 后台执行

注意⚠️:

Main Dispatch Queue和Global Dispatch Queue中执行dispatch_release函数和dispatch_retain函数不会引起任何变化

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       //全局队列
        NSLog(@"currentThread---%@",[NSThread currentThread]);
        dispatch_async(dispatch_get_main_queue(), ^{
            // 主队列
            NSLog(@"currentThread---%@",[NSThread currentThread]);
        });
    });

(4)dispatch_set_target_queue

dispatch_set_target_queue函数主要作用是变更Dispatch Queue的执行优先级。

dispatch_queue_t serialQueue = dispatch_queue_create("com.baidu.xxx", NULL);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);    
// 第一个参数是要变更执行优先级的Queue
// 第二个参数是指定执行优先级的目标
dispatch_set_target_queue(serialQueue, globalQueue);

第一个参数不能是系统指定的Main Dispatch Queue和Global Dispatch Queue,因为不确定最终发生了什么。
如果在多个Serial Dispatch Queue中使用dispatch_set_target_queue函数指定目标为某一个Serial Dispatch Queue,那么原先本应并行执行的多个Serial Dispatch Queue,在目标Serial Dispatch Queue上只能执行一个处理。所以dispatch_set_target_queue函数可以防止多个并行执行的Serial Dispatch Queue按照目标一次执行。

(5)dispatch_after

dispatch_after并不是指定时间后执行处理,而是在指定时间追加处理到队列中。

// 延迟3秒添加到队列中
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue() , ^{
    NSLog(@"3s after");
});

上述代码,是将block处理在3秒后添加到主队列中,因为主队列是在Runloop中执行的,所以每隔1/60执行一次runloop,所以Block最快在3秒后执行,最慢在3+1/60秒之后执行
dispatch_after函数参数说明:

  • 第一个参数是指定时间。这个值可以由dispatch_time和dispatch_walltime函数生成。
  • 第二个参数是需要追加到的队列
  • 第三个参数block是需要执行的处理

dispatch_time函数通常用于计算相对时间。
dispatch_walltime函数用于计算绝对时间,根据系统时钟创建绝对时间。

// 第一个参数是一个时间结构体,如果传入NULL,方法会默认使用当前时间
// 第二个参数是单位是纳秒
dispatch_time_t wtime = dispatch_walltime(NULL, 1000 * 1000 * 1000)

(6)Dispatch Group

Dispatch Group可以监视加入的所有执行队列结束。一旦检测到所有任务执行结束,就可将结束的任务追加到Dispatch Queue中。

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_async(group, queue, ^{
    NSLog(@"执行任务A");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"执行任务B");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"执行任务C");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"执行任务D");
});
dispatch_group_notify(group, queue, ^{
    NSLog(@"最后执行");
});

// 第一个参数是group
// 第二个参数是等待时间(超时),DISPATCH_TIME_FOREVER代表永不超时
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

NSLog(@"group 执行结束");

最后执行结果:

2021-02-23 16:57:19.582588+0800 GCD演示[11718:280292] 执行任务B 2021-02-23 16:57:19.582620+0800 GCD演示[11718:280297] 执行任务C 2021-02-23 16:57:19.582617+0800 GCD演示[11718:280290] 执行任务D 2021-02-23 16:57:19.582584+0800 GCD演示[11718:280291] 执行任务A 2021-02-23 16:57:19.582772+0800 GCD演示[11718:280297] 最后执行 2021-02-23 16:57:19.582781+0800 GCD演示[11718:280220] group 执行结束

dispatch_group_async函数与dispatch_async函数相同。
dispatch_group_notify函数会在group中所有执行队列完成之后追加到最后队列中。不管该函数指定的Dispatch Queue是否是哪个,只要属于Dispatch Group中的所有队列都执行结束在追加该函数的Block到队列时。
dispatch_group_wait函数等待全部处理执行结束。如果返回值不为0,则在指定时间之后,还存在队列正在执行,如果返回值为0,则代表执行队列已经全部执行结束。

(7)dispatch_barrier_async

使用dispatch_barrier_async同新创建的Concurrent Dispatch Queue一起使用,可以实现高效率的数据库访问和文件访问。
dispatch_barrier_async函数能避免数据竞争问题


    dispatch_queue_t queue = dispatch_queue_create("com.queue", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        NSLog(@"执行任务A");
    });
    dispatch_async(queue, ^{
        NSLog(@"执行任务B");
    });

    // 该函数会在任务A和任务B执行完成之后再执行
    // 并且该函数执行完成之后才会执行后面的任务
    dispatch_barrier_async(queue, ^{
        NSLog(@"barrier执行任务");
    });

    dispatch_async(queue, ^{
        NSLog(@"执行任务C");
    });
    dispatch_async(queue, ^{
        NSLog(@"执行任务D");
    });
    dispatch_async(queue, ^{
        NSLog(@"执行任务E");
    });

输出结果:

2021-02-23 17:22:31.147158+0800 GCD演示[12304:298523] 执行任务A 2021-02-23 17:22:31.147169+0800 GCD演示[12304:298521] 执行任务B 2021-02-23 17:22:31.147309+0800 GCD演示[12304:298521] barrier执行任务 2021-02-23 17:22:31.147420+0800 GCD演示[12304:298521] 执行任务C 2021-02-23 17:22:31.147423+0800 GCD演示[12304:298523] 执行任务D 2021-02-23 17:22:31.147434+0800 GCD演示[12304:298529] 执行任务E

(8)dispatch_sync

dispatch_sync同步函数。
dispatch_sync使用主队列会造成死锁问题。同样的,与串行队列一起使用也会造成死锁问题。
下面几种死锁的情况:

// 情况一
dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"AAAA");
});
// 情况二
dispatch_async(dispatch_get_main_queue(), ^{
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"BBB");
    });
});
// 情况三
dispatch_queue_t queue = dispatch_queue_create("com.xxx", NULL);
dispatch_async(queue, ^{
    dispatch_sync(queue, ^{
        NSLog(@"BBB");
    });
});

(9)dispatch_apply

dispatch_apply函数是dispatch_sync函数和Dispatch Group的关联API。该函数按指定的次数将指定的执行任务追加到Dispatch Queue队列中,并等待全部处理执行结束。

dispatch_apply(10, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^(size_t index){
    NSLog(@"%d",index);
});

dispatch_apply参数说明:

  • 参数一:执行重复次数
  • 参数二:追加任务的执行队列
  • 参数三:执行任务处理的Block

(10)dispatch_suspend / dispatch_resume

dispatch_suspend函数:挂起指定的队列。
dispatch_resume函数:恢复挂起的指定队列。
这两个函数对已经执行的队列没有任何的影响。挂起尚未执行的处理将停止,在恢复之后才可以继续执行。

(11)Dispatch Semephore

Dispatch Semephore是GCD中的信号量。是持有计数的信号,Semephore使用计数来实现该功能,计数为0时等待,计数为1或大于1时,减去1而不等待。
核心方法:

  • dispatch_semaphore_create(long value): 创建一个信号量
  • dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout):接收一个信号和等待时间,若信号的信号量为0,则会阻塞当前线程,直到信号量大于0或者超过设定时间值;若信号量大于0,则会使信号量减1并返回,程序继续住下执行。
  • dispatch_semaphore_signal(dispatch_semaphore_t dsema): 使信号量加1并返回

线程同步

利用信号量的特性,先创建信号量为0 在需要保持同步的地方,使用dispatch_semaphore_wait等待线程完成 在子线程完成之后调用dispatch_semaphore_signal去触发等待往下执行

示例代码

// 保持线程同步
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); // 关键,设置信号量为0
// 该函数等待semaphore的计数大于或等于1
__block int a = 10;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    a = 20;
    // 触发信号+1
    dispatch_semaphore_signal(semaphore);
});
long result = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (result == 0) {
    // 成功等待返回

    NSLog(@"%zd",a);

} else {
    // 超过设定时长
}

打印结果是 20,所以到result之后,需要等着子线程执行完成之后才往下走。

线程加锁

  • 创建信号量为1的信号
  • 在需要加锁的地方之前调用dispatch_semaphore_wait
  • 在需要解锁的地方使用dispatch_semaphore_signal

示例代码:

 // 线程加锁
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
__block int a = 100;
for (int i = 0; i < 100; i ++) {
    // for循环用于开辟多个线程
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if (a > 0) {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 相当于加锁
            a -= 1;
            NSLog(@"%zd",a);
            dispatch_semaphore_signal(semaphore); //相当于解锁
        }
    });
}

(12)dispatch_once

dispatch_once函数保证在应用程序执行中只执行一次指定处理。
一般常用于单利模式

+ (instancetype)shared {
    static NSObject *instance;
    static dispatch_once_t once;
    dispatch_once(&once, ^{
        instance = [[NSObject alloc] init];
    });
    return instance;
}

(13)Dispatch I/O

dispatch_io_create 函数生成Dispatch I/O,并指定发生error时用来执行处理的block,以及执行该block的Dispatch Queue。
dispatch_io_set_low_water 函数设置一次读取的大小
dispatch_io_read 函数使用Global Dispatch Queue 开始并发读取。每当各个分割的文件块读取结束时,将含有文件块数据的 Dispatch Data(这里指pipedata) 传递给 “ dispatch_io_read 函数指定的读取结束时回调用的block ”,这个block拿到每一块读取好的 Dispatch Data(这里指pipe data),然后进行合并处理。
如果想提高文件读取速度,可以尝试使用 Dispatch I/O.
以下源码来源:https://opensource.apple.com/source/Libc/Libc-763.11/gen/asl.c


static int
_asl_auxiliary(aslmsg msg, const char *title, const char *uti, const char *url, int *out_fd)
{
    asl_msg_t *merged_msg;
    asl_msg_aux_t aux;
    asl_msg_aux_0_t aux0;
    fileport_t fileport;
    kern_return_t kstatus;
    uint32_t outlen, newurllen, len, where;
    int status, fd, fdpair[2];
    caddr_t out, newurl;
    dispatch_queue_t pipe_q;
    dispatch_io_t pipe_channel;
    dispatch_semaphore_t sem;
    /* ..... 此处省略若干代码.....*/

    // 创建串行队列
    pipe_q = dispatch_queue_create("PipeQ", NULL);
    // 创建 Dispatch I/O
    pipe_channel = dispatch_io_create(DISPATCH_IO_STREAM, fd, pipe_q, ^(int err){
        close(fd);
    });

    *out_fd = fdpair[1];

    // 该函数设定一次读取的大小(分割大小)
    dispatch_io_set_low_water(pipe_channel, SIZE_MAX);
    //
    dispatch_io_read(pipe_channel, 0, SIZE_MAX, pipe_q, ^(bool done, dispatch_data_t pipedata, int err){
        if (err == 0) // err等于0 说明读取无误
        {
            // 读取完“单个文件块”的大小
            size_t len = dispatch_data_get_size(pipedata);
            if (len > 0)
            {
                // 定义一个字节数组bytes
                const char *bytes = NULL;
                char *encoded;

                dispatch_data_t md = dispatch_data_create_map(pipedata, (const void **)&bytes, &len);
                encoded = asl_core_encode_buffer(bytes, len);
                asl_set((aslmsg)merged_msg, ASL_KEY_AUX_DATA, encoded);
                free(encoded);
                _asl_send_message(NULL, merged_msg, -1, NULL);
                asl_msg_release(merged_msg);
                dispatch_release(md);
            }
        }

        if (done)
        {
            dispatch_semaphore_signal(sem);
            dispatch_release(pipe_channel);
            dispatch_release(pipe_q);
        }
    });
}

(14)Dispatch Source

dispatch source是基础数据类型,协调特定底层系统事件的处理
Dispatch source替代了异步回调函数,来处理系统相关的事件。
当你配置一个dispatch source时,你指定要监测的事件、dispatch queue、以及处理事件的代码(block或函数)。
当事件发生时,dispatch source会提交你的block或函数到指定的queue去执行和手工提交到queue的任务不同,dispatch source为应用提供连续的事件源。除非你显式地取消,dispatch source会一直保留与dispatch queue的关联。只要相应的事件发生,就会提交关联的代码到dispatch queue去执行。为了防止事件积压到dispatch queue,dispatch source实现了事件合并机制。 如果新事件在上一个事件处理器出列并执行之前到达,dispatch source会将新旧事件的数据合并。 根据事件类型的不同,合并操作可能会替换旧事件,或者更新旧事件的信息。
Dispatch是BSD系内核惯有功能kqueue的包装。kqueue是在XNU内核中发生各种事件时,在应用程序编程方执行处理的技术。其CPU负荷非常小,尽量不占用资源。kqueue可以说是应用程序处理XUN内核中发生的各种事件的方法中最优秀的一种。
Dispatch Source的种类

名称 内容
DISPATCH_SOURCE_TYPE_DATA_ADD 数据增加
DISPATCH_SOURCE_TYPE_DATA_OR 获取的内容进行OR运算
DISPATCH_SOURCE_TYPE_DATA_REPLACE 获取的内容替换
DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口发送
DISPATCH_SOURCE_TYPE_MACH_RECV MACH端口接受
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE 内存压力
DISPATCH_SOURCE_TYPE_PROC 检测到与进程相关的事件
DISPATCH_SOURCE_TYPE_READ 可读取文件映像
DISPATCH_SOURCE_TYPE_SIGNAL 接受信号
DISPATCH_SOURCE_TYPE_TIMER 定时器
DISPATCH_SOURCE_TYPE_VNODE 文件系统有变更
DISPATCH_SOURCE_TYPE_WRITE 可写入文件映像
DISPATCH_MACH_SEND_DEAD

自定义定时器

__block int a = 0;
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
// 设定 10秒间隔,允许1秒延迟
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC, 1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
    // 定时任务
    NSLog(@"event");
    a = a + 1;
    if (a > 10) { //满足某一条件之后,取消定时器

        // 取消Dispatch Source
        dispatch_source_cancel(timer);
    }
});
// 指定取消定时器的任务处理
dispatch_source_set_cancel_handler(timer, ^{
    NSLog(@"cancel timer");
});

// 启动定时器
dispatch_resume(timer);

3、NSOperation

NSOperation、NSOperationQueue 是苹果提供给我们的一套多线程解决方案。
NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。NSOperation 需要配合 NSOperationQueue 来实现多线程。
NSOperation是一个抽象类,使用前需要子类化(NSInvocationOperation,NSBlockOperation)。NSOperation本身是线程安全的,当我们在子类重写或自定义方法时同样需要保证线程安全。
NSOperation、NSOperationQueue的优势:

  • 可添加完成的代码块,在操作完成后执行。(setCompletionBlock)
  • 添加操作间的依赖关系,方便控制执行顺序。(addDependency)
  • 设定操作执行的优先级。(setQueuePriority)
  • 可以很方便的取消一个操作执行。(- (void)cancel;)
  • 使用KVO观察操作的状态更改。 (isExecuteing,isFinished,isCancelled)

3.1、单独使用NSOperation

(1)使用NSInvocationOperation子类

代码示例:

- (void)testOpreation {
    // 使用子类NSInvocationOperation
    NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doWork) object:nil];
    // 开启操作
    [operation start];
}
- (void)doWork {
    NSLog(@"dowork---%@", [NSThread currentThread]);
}

打印结果:

dowork—-{number = 1, name = main}

从打印结果中可以看出:在没有使用 NSOperationQueue、在主线程中单独使用使用子类 NSInvocationOperation 执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。

(2)使用NSBlockOperation子类
示例代码:

 // 使用子类NSBlockOperation
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"block---%@", [NSThread currentThread]);
}];
// 开启执行操作
[blockOperation start];
addExecutionBlock

打印输出:

block—-{number = 1, name = main}

从结果看出和NSInvocationOperation一样的效果。如果在其他线程中开启,则在当前线程中。
但NSBlockOpreation提供了额外的方法addExecutionBlock来添加更多的操作。

// 使用子类NSBlockOperation
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"block---%@", [NSThread currentThread]);
}];

//添加额外操作
[blockOperation addExecutionBlock:^{
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"block---%@", [NSThread currentThread]);
}];
//添加额外操作
[blockOperation addExecutionBlock:^{
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"block---%@", [NSThread currentThread]);
}];

// 开启执行操作
[blockOperation start];

打印结果:

block—-{number = 1, name = main} block—-{number = 5, name = (null)} block—-{number = 6, name = (null)}

使用子类 NSBlockOperation,并调用方法 addExecutionBlock: 的情况下,blockOperationWithBlock:方法中的操作 和 addExecutionBlock: 中的操作是在不同的线程中并发执行的。而且,这次执行结果中 blockOperationWithBlock:方法中的操作也不是在当前线程(主线程)中执行的。从而印证了blockOperationWithBlock: 中的操作也可能会在其他线程(非当前线程)中执行。

综上:一般情况下,如果一个 NSBlockOperation 对象封装了多个操作。NSBlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。

(3)使用自定义NSOperation子类
自定义继承自 NSOperation 的子类,可以通过重写 main 或者 start 方法 来定义自己的 NSOperation 对象。
示例代码:

@interface SonOperation : NSOperation

@end

@implementation SonOperation

- (void)main {
    // 重写main方法

    if (!self.isCancelled) {   
        [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
        NSLog(@"SonOperation---%@", [NSThread currentThread]);
    }
}

@end

打印结果:

SonOperation—-{number = 1, name = main}

从结果可以看出:在没有使用 NSOperationQueue、在主线程单独使用自定义继承自 NSOperation 的子类的情况下,是在主线程执行操作,并没有开启新线程。

3.2、使用NSOperationQueue

NSOperationQueue包含主队列和自定义队列。
主队列:(添加到主队列的都在主线程中执行,addExecutionBlock添加的操作可能在其他线程执行)

 NSOperationQueue *queue = [NSOperationQueue mainQueue];

自定义队列:

 // 自定义操作队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 控制最大并发数
// queue.maxConcurrentOperationCount = 1; // 串型队列
queue.maxConcurrentOperationCount = 2; // 并发队列

最大并发数:maxConcurrentOperationCount (这里控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。而且一个操作也并非只能在一个线程中运行)

maxConcurrentOperationCount = 1 则是串型队列 maxConcurrentOperationCount > 1 时则是并发队列,操作并发最大不应该超过系统限制,超过最终也会取系统最大值。 maxConcurrentOperationCount 默认情况下是-1,允许并发控制


设置操作依赖:**
通过操作依赖,我们可以很方便的控制操作之间的执行先后顺序。
NSOperation 提供了3个接口供我们管理和查看依赖。

    • (void)addDependency:(NSOperation *)op; //添加操作依赖,使当前操作依赖于op操作的完成
    • (void)removeDependency:(NSOperation *)op; // 移除操作依赖,取消当前操作对op操作的依赖
  1. @property (readonly, copy) NSArray *dependencies; // 在当前操作开始执行之前完成的所有操作对象数组。
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 创建操作A
NSBlockOperation *bOpA = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"NSBlockOperation A");
}];
NSBlockOperation *bOpB = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"NSBlockOperation B");
}];
NSBlockOperation *bOpC = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"NSBlockOperation C");
}];
//设置依赖,先执行C再执行A
[bOpA addDependency:bOpC];

//将操作添加到队列
[queue addOperation:bOpA];
[queue addOperation:bOpB];
[queue addOperation:bOpC];

运行日志:

NSBlockOperation B NSBlockOperation C NSBlockOperation A

可以看出操作A永远在操作C之后执行。
注意⚠️:

设置依赖需要在添加队列之前完成

NSOperation优先级
提供了queuePriority优先级属性,以下是优先级的取值。

typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};
@property NSOperationQueuePriority queuePriority;

默认情况下,所有操作的默认优先级都是NSOperationQueuePriorityNormal 。我们可以通过setQueuePriority:方法来改变当前操作在同一队列中的执行优先级。
操作设置优先级执行顺序:

  • 当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行。
  • queuePriority 属性决定了进入准备就绪状态下的操作之间的开始执行顺序。并且,优先级不能取代依赖关系。
  • 如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么队列先执行高优先级操作。
  • 如果一个队列中既包含了准备就绪状态的操作,又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作优先级高。那么,虽然准备就绪的操作优先级低,也会优先执行。
  • 优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。

线程间的通信

NSBlockOperation *bOpA = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"NSBlockOperation A");

    // 线程间的通信
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // 回到主线程
    }];
}];

4、NSThread

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

// NSThread
NSThread *thread = [[NSThread alloc] initWithBlock:^{

    NSLog(@"%@",[NSThread currentThread]);
}];
// 启动线程
[thread start];

//创建线程后自动启动线程
[NSThread detachNewThreadWithBlock:^{
    NSLog(@"%@",[NSThread currentThread]);
}];

// 隐式创建并启动线程
[self performSelectorInBackground:@selector(run) withObject:nil];

NSThread类的属性和方法

@interface NSThread : NSObject  {
@private
    id _private;
    uint8_t _bytes[44];
}
//获取当前属性
@property (class, readonly, strong) NSThread *currentThread;
// 快速创建新线程的类方法(block和SEL两种方式),并直接启动
+ (void)detachNewThreadWithBlock:(void (^)(void))block;
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

//是否是多线程
+ (BOOL)isMultiThreaded;

//保存线程的信息
@property (readonly, retain) NSMutableDictionary *threadDictionary;

// 阻塞线程的方法,在指定日期或者时间内阻塞
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

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

// 线程优先级
+ (double)threadPriority;
// 设置线程的优先级
+ (BOOL)setThreadPriority:(double)p;

@property double threadPriority ; // To be deprecated; use qualityOfService below

@property NSQualityOfService qualityOfService; // read-only after the thread is started

@property (class, readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses;
@property (class, readonly, copy) NSArray<NSString *> *callStackSymbols;

// 线程名称
@property (nullable, copy) NSString *name;

//栈大小
@property NSUInteger stackSize;

// 是否是主线程
@property (readonly) BOOL isMainThread;
@property (class, readonly) BOOL isMainThread; // reports whether current thread is main
// 获取主线程
@property (class, readonly, strong) NSThread *mainThread;

// 线程初始化
- (instancetype)init ;
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;
- (instancetype)initWithBlock:(void (^)(void))block ;

@property (readonly, getter=isExecuting) BOOL executing ;
@property (readonly, getter=isFinished) BOOL finished ;
@property (readonly, getter=isCancelled) BOOL cancelled ;

// 取消线程
- (void)cancel;
// 线程进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态
- (void)start ;

- (void)main;    // thread body method

@end

5、pthread

pthread 是一套通用的多线程的 API,可以在Unix/Linux/Windows 等系统跨平台使用,使用 C 语言编写,需要程序员自己管理线程的生命周期,使用难度较大,我们在 iOS 开发中几乎不使用 pthread。

// C语言函数
void *run(void *param) {
    NSLog(@"%@",[NSThread currentThread]);
    return NULL;
}

// 定义pthread_t变量
pthread_t pthread;
// 开启线程
pthread_create(&pthread, NULL,run,NULL);
// 设置子线程的状态为detach
pthread_detach(pthread);

pthread_create的参数说明:

  • 第一个参数是指向线程变量的指针
  • 第二个参数是线程属性,可赋值为NULL
  • 第三个参数是指向函数的指针
  • 第四个参数是运行函数的参数


pthread相关API**

  • pthread_create() 创建一个线程
  • pthread_exit() 终止当前线程
  • pthread_cancel() 中断另外一个线程的运行
  • pthread_join() 阻塞当前的线程,直到另外一个线程运行结束
  • pthread_attr_init() 初始化线程的属性
  • pthread_attr_setdetachstate() 设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
  • pthread_attr_getdetachstate() 获取脱离状态的属性
  • pthread_attr_destroy() 删除线程的属性
  • pthread_kill() 向线程发送一个信号

6、线程安全问题

线程安全是指变量或方法( 这些变量或方法是多线程共享的) 可以在多线程的环境下被安全有效的访问。
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

  • 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;
  • 若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程同步即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。
可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。

典型列子:售票系统

场景:总的有100张火车票,分别在售票窗口A和售票窗口B进行同事售票,一直到火车票卖完为止。

转化为代码

// 非线程安全的情况
{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 窗口A
    NSBlockOperation *opA = [NSBlockOperation blockOperationWithBlock:^{
        [self sellTickets];
    }];
    // 窗口B
    NSBlockOperation *opB = [NSBlockOperation blockOperationWithBlock:^{
        [self sellTickets];
    }];
    // 加入队列
    [queue addOperation:opA];
    [queue addOperation:opB];
}

- (void)sellTickets {
    while (self.tickets > 0) {
        self.tickets -= 1;
        NSLog(@"还剩%zdp票,窗口%@",self.tickets,[NSThread currentThread]);
    }
}

打印结果中出现异常条目:
image.png
出现两次都是A,B线程进行减1操作之后,还是97的情况,这里就出现了线程安全问题

使用NSlock加锁处理,保证线程安全。以下是关键代码:

@property(nonatomic, strong) NSLock *lock;

{
    //初始化锁
    self.lock = [[NSLock alloc] init];   
}

- (void)sellTickets {
    while (1) {
        [self.lock lock]; //加锁操作
        if (self.tickets > 0) {
            self.tickets -= 1;
            NSLog(@"还剩%zdp票,窗口%@",self.tickets,[NSThread currentThread]);
        } else {
            NSLog(@"票已经卖完了");
            break;
        }
        [self.lock unlock]; //解锁
    }
}

7、解决线程安全的几种方式

(1) @synchronized

@synchronized指令使用对象作为该锁的唯一标识。只有标识相同时,才满足互斥条件。
@synchronized 指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized 块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。

@synchronized (object) {
    //TODO 需要加锁的内容
}

(2) OSSpinLock 自旋锁

OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。
自旋锁存在的问题
当多个线程有优先级的时候,那么自旋锁就会出现问题。如果一个优先级低的线程先去访问某个数据,此时使用自旋锁进行了加锁,然后一个优先级高的线程又去访问这个数据,那么优先级高的线程会一直占着CPU资源,优先级低的线程就无法释放锁。由于自旋锁本身存在的问题,所以苹果已经废弃了OSSpinLock。
苹果在iOS10之后使用os_unfair_lock代替了OSSpinLock。
os_unfair_lock就是让线程睡眠,所以它避免了自旋锁导致的优先级反转问题

os_unfair_lock_t *lock;
os_unfair_lock_lock(&lock);
//TODO 加锁代码片段
os_unfair_lock_unlock(&lock);

(3) NSConditionLock 条件锁

@interface NSConditionLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name;

@end

(4) NSRecursiveLock 递归锁

NSRecursiveLock实际上定义的是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。pthread_mutex(recursive)的高级封装。

- (void)test8 {

//    self.lock = [[NSLock alloc] init]; //使用互斥锁就会造成死锁
    self.lock = [[NSRecursiveLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self revFunc:5];
    });
}
- (void)revFunc:(int)value {
    if (value > 0) {
        // 递归调用
        [self.lock lock];
        NSLog(@"value = %d", value);
        sleep(1); //休眠1s
        [self revFunc:value - 1];
        [self.lock unlock];
    }
}

从示例中看出如果使用NSLock,则会造成死锁的情况,原因是在递归调用的过程中,第二次去调用的时候,由于锁已经使用了,并且没有解锁。他需要等待锁被解除,所以就进入死锁的状态,线程阻塞。
将锁修改为NSRecursiveLock之后,则线程正常运行,这是因为NSRecursiveLock允许同一个线程多次加锁而不会造成死锁
NSRecursiveLock类相关方法和属性

@interface NSRecursiveLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name ;

@end

(5) dispatch_semaphore GCD信号量

详细的解释看上面[GCD->Dispatch Semephore]
**

(6) pthread_mutex

pthread_mutex表示互斥锁,和信号量的实现原理类似,也是阻塞线程并进入睡眠,需要进行上下文切换。

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);

pthread_mutex_t lock;
pthread_mutex_init(&lock, &attr);    //设置属性

pthread_mutex_lock(&lock);    //上锁
//需要执行的代码
pthread_mutex_unlock(&lock);    //解锁

(7) pthread_mutex(recursive)

pthread_mutex锁的一种,属于递归锁。一般一个线程只能申请一把锁,但是,如果是递归锁,则可以申请很多把锁,只要上锁和解锁的操作数量就不会报错。

(8) NSLock

NSLock是Cocoa提供给我们最基本的锁对象,他属于互斥锁。
在使用的时候需要使用 lock方法和unlock方法一起使用

NSLock *lk = [[NSLock alloc] init];
//加锁部分
[lk lock];
// TODO 需要加锁的内容
[lk unlock];

NSLock相关方法

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name ;

@end

(9) NSCondition

一种最基本的条件锁。手动控制线程wait和signal。
[condition lock];一般用于多线程同时访问、修改同一个数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到unlock ,才可访问。
[condition unlock];与lock 同时使用。
[condition wait];让当前线程处于等待状态。
[condition signal];CPU发信号告诉线程不用在等待,可以继续执行。

NSCondition方法属性

@interface NSCondition : NSObject <NSLocking> {
@private
    void *_priv;
}

- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

@property (nullable, copy) NSString *name;

@end