iOS锁简单整理
iOS锁
什么是锁
在计算机科学中,锁或互斥锁(互斥)是一种同步机制,用于在存在许多执行线程的环境中强制限制对资源的访问。锁旨在强制执行互斥并发控制策略。
说人话就是避免在多线程中同时访问一个资源(变量,文件等)发生意料之外的事情。
就像所有人都去买东西,收银台只有一个人,但是如果2、3个人同时去结账,收银员可能会处理的过来,但也能会出错(找错钱)等。
在iOS有几种哪些锁
OSSpinLock
自旋锁os_unfair_lock
互斥锁pthread_mutex
递归锁pthread_mutex
条件锁dispatch_semaphore
信号量dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSRecursiveLock
NSCondition
NSConditionLock
@synchronized
dispatch_barrier_async
栅栏dispatch_group
调度组
他们有什么区别
pthread(POSIX Mutex Lock)锁
由c语言实现、跨平台。
Pthreads定义了一套C语言的类型、函数与常量,它以pthread.h
头文件和一个线程库实现。
Pthreads API中大致共有100个函数调用,全都以"pthread_"开头,并可以分为四类:
- 线程管理,例如创建线程,等待(join)线程,查询线程状态等。
- 互斥锁(Mutex):创建、摧毁、锁定、解锁、设置属性等操作
- 条件变量(Condition Variable):创建、摧毁、等待、通知、设置与查询属性等操作
- 使用了互斥锁的线程间的同步管理
在c中使用
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>
static void wait(void)
{
time_t start_time = time(NULL);
while (time(NULL) == start_time)
{
/* do nothing except chew CPU slices for up to one second */
}
}
static void *thread_func(void *vptr_args)
{
int i;
for (i = 0; i < 20; i++)
{
fputs(" b\n", stderr);
wait();
}
return NULL;
}
int main(void)
{
int i;
pthread_t thread;
if (pthread_create(&thread, NULL, thread_func, NULL) != 0)
{
return EXIT_FAILURE;
}
for (i = 0; i < 20; i++)
{
puts("a");
wait();
}
if (pthread_join(thread, NULL) != 0)
{
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
在oc中使用
- (NSString *)debugDescription
{
NSMutableString *s = [NSMutableString stringWithFormat:@"<%@:%p", NSStringFromClass([self class]), self];
// lock
pthread_mutex_lock(&_mutex);
NSMutableArray *infoDescriptions = [NSMutableArray arrayWithCapacity:_infos.count];
for (_FBKVOInfo *info in _infos) {
[infoDescriptions addObject:info.debugDescription];
}
[s appendFormat:@" contexts:%@", infoDescriptions];
// unlock
pthread_mutex_unlock(&_mutex);
[s appendString:@">"];
return s;
}
详细可以了解
https://zh.wikipedia.org/wiki/POSIX%E7%BA%BF%E7%A8%8B
GCD锁
网上资料资料已经有很多,不再赘述,这里有一篇写的比较好的,为了防止链接失效我这里复制一下,还是原链接可读性比较好
http://www.mwpush.com/content/d04bd655.html
一、Dispatch Semaphore
使用信号量可以达到控制并发数和线程锁的目的。它通过持有计数的信号的增减来控制访问。当计数为0时等待,计数大于等于1时,减1继续执行。
dispatch_semaphore_create通过初始值创建一个信号量,有一个参数,为long类型,即初始值,应大于等于0,返回值为dispatch_semaphore_t类型信号量;
dispatch_semaphore_signal发信号,即信号量值增加。有一个参数,为dispatch_semaphore_t类型信号量。
dispatch_semaphore_wait等待信号量,即信号量减少。有两个参数,第一个为dispatch_semaphore_t类型信号量,第二个为dispatch_time_t类型超时时间。返回值为long类型,返回值为0则等待时间未超时,不为0则超时。
dispatch_semaphore_signal与dispatch_semaphore_wait的调用要保持平衡。
dispatch_queue_t c_queue = dispatch_queue_create("com.mwpush", DISPATCH_QUEUE_CONCURRENT);
for(int i = 0; i < 10; i++) {
dispatch_async(c_queue, ^{
NSLog(@"第%d次执行", i);
});
}
上述代码为异步并发队列,此时输出结果如下:
第0次执行
第1次执行
第4次执行
第2次执行
第5次执行
第3次执行
第6次执行
第7次执行
第9次执行
第8次执行
为无序输出,我们可以通过串行队列达到依次输出的目的,也可以使用信号量来控制,如下:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_queue_t c_queue = dispatch_queue_create("com.mwpush", DISPATCH_QUEUE_CONCURRENT);
for(int i = 0; i < 10; i++) {
dispatch_async(c_queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"第%d次执行", i);
dispatch_semaphore_signal(semaphore);
});
}
输出结果如下:
第0次执行
第1次执行
第2次执行
第3次执行
第4次执行
第5次执行
第7次执行
第6次执行
第8次执行
第9次执行
我们创建的信号量初始值为1,当执行dispatch_semaphore_wait方法时信号计数减1为0,所以此时如果有其他线程要执行同样的方法,因为信号计数为0需等待,无法继续执行。当执行dispatch_semaphore_signal方法时,信号计数加1,则dispatch_semaphore_wait方法将信号计数减1并继续执行,依次往复。
可见,信号量可以在多个线程间通过信号计数来协调控制访问处理。我们可以通过信号计数的值的控制来控制最大并发数,只要信号计数大于0就可以不用等待并发执行。
二、Dispatch Source
调度源,是用来监视低级系统事件的对象,在事件发生时,自动在指定的Dispatch Queue中执行指定的处理事件。
1.自定义调度源
dispatch_source_create创建一个调度源来监视低级系统事件。有4个参数,第一个为dispatch_source_type_t类型,是一个结构体常量指针,用来定义调度源的类型,也就是调度源监视的低级系统对象类型的标识符,需要根据这个参数来确定第二个和第三个参数的解释方式。第二个参数为uintptr_t即unsigned long类型的要监视的底层系统句柄,这个值的设定是根据第一个参数来确定的,通过一个参数来确定这个参数是作为文件描述符、mach端口、信号数,还是进程标识符等,第三个参数为unsigned long类型的事件所需的标志掩码,这个值的设定也是根据第一个参数来确定的,系统提供了这个值的固定参数,第四个参数为监听到指定事件时调用的处理事件所要提交到的队列。返回值为dispatch_source_t类型的调度源。调度源是在非活动状态下创建的。上一篇文章中我们介绍到dispatch_resume与dispatch_suspend应该匹配使用,成对出现,否则会引发崩溃,但是有一个例外,就是这里的调度源,在官方对dispatch_resume的介绍中,说到新创建的调度源挂起计数为1,必须要在事件传递前将其恢复。而在官方对dispatch_source_create的说明中说到,出于向后兼容的原因,dispatch_resume在一个非活动的、而不是挂起的源上,与dispatch_activate的作用相同。所以我们也姑且称之为“非活动状态”。官方建议在调度源中使用dispatch_activate来激活调度源。
2.dispatch_source_merge_data将数据合并到DISPATCH_SOURCE_TYPE_DATA_ADD或DISPATCH_SOURCE_TYPE_DATA_OR类型的调度源中,并将其事件处理程序提交到目标队列。两个参数,第一个为调度源,第二个参数为unsigned long类型,是要提交的数据,不能为0,为0不会出发提交事件处理程序块,也不能为负数。
dispatch_source_set_event_handler为给定的调度源设置事件处理程序块。有两个参数,第一个为目标调度源,第二个为要执行的程序块。此方法是设置要提交到目标调度源中指定的队列的程序块。同一时间只能有一个任务块被执行,如果当另一次提交事件发生时,当前程序块还没有执行完毕,则会以指定的方式(创建调度源时指定的ADD或OR)进行积累合并,有效解决了频繁提交带来的处理事件的压力。
dispatch_source_get_data返回调度源的待处理数据,有一个参数为调度源。此方法应在事件处理程序块中调用(也就是dispatch_source_set_event_handler中),在事件处理程序回调之外调用此函数结果是不确定的。通过此方法可以获取调度源中的当前值,并将数据清零。
dispatch_queue_t c_queue = dispatch_queue_create("com.mwpush", DISPATCH_QUEUE_CONCURRENT);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, c_queue);
dispatch_source_set_event_handler(source, ^{
NSLog(@"%ld", dispatch_source_get_data(source));
});
dispatch_activate(source);
dispatch_apply(5, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t index) {
dispatch_source_merge_data(source, 1);
});
输出结果如下:
5
上面的代码,自定义一个并发队列c_queue,作为定义调度源的使用队列,通过dispatch_source_set_event_handler设置处理事件的程序块,然后激活调度源。然后通过5次迭代将数据1提交到调度源,因为事件提交是连续发生的,程序块还没有执行完毕,会以ADD的方式对数据进行合并,最后输出。如果当提交合并时,程序块已执行完毕,则不会以ADD方式进行合并。
以下验证代码:
@interface ViewController ()
@property (nonatomic, strong) dispatch_source_t source;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];dispatch_queue_t c_queue = dispatch_queue_create("com.mwpush", DISPATCH_QUEUE_CONCURRENT);
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, c_queue);dispatch_source_set_event_handler(self.source, ^{
NSLog(@"%ld", dispatch_source_get_data(self.source));
});
dispatch_activate(self.source);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
dispatch_source_merge_data(self.source, 1);
}
@end
此时我们点击屏幕,可以输出结果如下:
1
再次验证,未执行完毕,进行合并,修改代码如下:
dispatch_source_set_event_handler(self.source, ^{
sleep(2); //添加睡眠,模拟耗时
NSLog(@"%ld", dispatch_source_get_data(self.source));
});
此时我们连续点击屏幕5次,输出结果如下:
1
4
因为第一次点击要耗时2秒后执行完毕,而点击是在短时间内完成的,所以第一次事件没有执行完毕,后面点击会被系统优化合并。
2.定时器源
dispatch_source_set_timer设置计时器源的开始时间,间隔和回程值。4个参数,第一个为调度源,第二个为dispatch_time_t类型的开始时间,第三个为时间间隔,单位为纳秒,第四个为设置计时精度,单位为纳秒,如果希望很精确(相对来说,没有绝对的精确),则设置为0,即使设为0,计时器也会有一定的等待时间,如果对精确度要求不高,可以设置为能接受的延迟时间。精确度越低对系统的执行灵活性越高(即系统可以根据延迟时间来配合系统事件执行,延迟时间允许的情况会与其他需要执行的事件一起唤醒执行,而非单单为了计时器单独执行)。
定时器源是有一个掩码值的,为DISPATCH_TIMER_STRICT,但是不建议使用,一般时候使用0就可以,除非对时间的精确度非常高。这个掩码指定dispatch_source_set_timer的延迟参数(leeway)尽最大努力遵守设置的值,设置此标记好,系统会将会应用最小的延迟,也会带来负面影响,比如耗电量大,影响省电技术,应谨慎使用,只有在绝对必要时设置此值。
dispatch_queue_t c_queue = dispatch_queue_create("com.mwpush", DISPATCH_QUEUE_CONCURRENT);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, c_queue);
dispatch_time_t time_t = dispatch_time(DISPATCH_TIME_NOW, 2ullNSEC_PER_SEC);
uint64_t interval = 1ullNSEC_PER_SEC;
dispatch_source_set_timer(source, time_t, interval, 0);
dispatch_source_set_event_handler(source, ^{
NSLog(@"%ld", dispatch_source_get_data(source));
});
dispatch_activate(source); //dispatch_resume(source);
以上代码实现了一个延迟2秒执行,并每隔一秒调用一次程序块的功能,如不需要重复调用,将时间间隔设置为DISPATCH_TIME_FOREVER即可。对于所提交的队列大家可自行选择,这里只是演示的例子。我们可以使用dispatch_suspend来暂停定时器,要对应有启动/继续定时器的dispatch_resume,也可以使用下面的方法取消定时器。
dispatch_source_cancel异步取消调度源,以防止进一步调用其事件处理程序块。一个参数为调度源。注意只能取消未被执行的程序块,已经执行的将继续执行完毕(提交的程序块也有可能未来得及执行)。
dispatch_source_set_cancel_handler设置目标调度源的取消处理程序块,一个参数为目标调度源。此方法在执行dispatch_source_cancel,且系统释放对源基础句柄的所有引用且所有已处理程序块执行完毕后被调用。
3、其他方法
dispatch_source_get_mask 返回调度源监视的事件掩码。
dispatch_source_testcancel测试目标调度源是否已取消。
dispatch_source_get_handle返回与指定调度源相关联的基础系统句柄。
dispatch_source_set_registration_handler设置给定调度源的注册处理程序块。
Dispatch Source官方地址:https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/GCDWorkQueues/GCDWorkQueues.html#//apple_ref/doc/uid/TP40008091-CH103-SW13
三、Dispatch I/O与Dispatch Data介绍
通过使用Dispatch I/O和Dispatch Data可以将大的文件分割成若干小部分进行读取合并。
dispatch_io_create创建一个I/O通道,并将其与指定的文件描述相关联。
dispatch_io_create_with_path创建具有关联路径的调度I/O通道。
dispatch_io_read在指定的通道上调用异步读取操作。
dispatch_io_write在指定的通道上调用异步写操作。
dispatch_io_close关闭指定的通道以进行新的读写操作。
dispatch_io_set_high_water设置在排队处理程序块之前要处理的最大字节数。
dispatch_io_set_low_water设置入队处理程序块之前要处理的最小字节数。
dispatch_io_set_interval设置间隔,单位为纳秒,在该间隔处调用通道的I/O处理程序。
dispatch_read 使用指定的文件描述符调度异步读取操作。
dispatch_write 使用指定的文件描述符调度异步写入操作。
调度数据对象提供了一个用于管理基于内存的数据缓冲区的接口。客户端访问数据缓冲区将其视为连续的内存块,但是在内部缓冲区可能由多个不连续的内存块组成。
dispatch_data_create使用指定的内存缓冲区创建一个新的调度数据对象。
dispatch_data_get_size返回由调度数据对象管理的内存逻辑大小。
dispatch_data_create_map返回一个新的调度数据对象,其中包含指定对象内存的连续表示形式。
dispatch_data_create_concat返回一个新的调度数据对象,该对象由来自其他两个数据对象的串联数据组成。
dispatch_data_create_subrange返回一个新的调度数据对象,其内容由另一个对象的内存区域的一部分组成。
dispatch_data_apply遍历调度数据对象的内存,并在每个区域上执行自定义代码。
dispatch_data_copy_region返回一个数据对象,该数据对象包含另一个数据对象中的一部分数据。
NS类锁
NSLock对象可用于调解对应用程序全局数据的访问或保护关键代码段,从而使其能够自动运行。
NSLock类使用POSIX线程来实现其锁定行为。向NSLock对象发送解锁消息时,必须确保该消息是从发送初始锁定消息的同一线程发送的。从其他线程解锁锁可能导致未定义的行为。
您不应使用此类来实现递归锁。在同一线程上两次调用lock方法将永久锁定您的线程。可以使用NSRecursiveLock类来实现递归锁。解锁未锁定的锁被视为程序员错误,应在代码中修复。NSLock类通过在发生错误时向控制台打印错误消息来报告此类错误。
@protocol NSLocking
- (void)lock;//加锁
- (void)unlock;//解锁
@end
@interface NSLock : NSObject <NSLocking> {
@private
void *_priv;
}
//尝试加锁,不会阻塞线程。true则加锁成功,false则失败,说明其他线程在加锁中这个方法无论如何都会立即返回。
- (BOOL)tryLock;
//尝试在指定NSDate之前加锁,会阻塞线程。true则加锁成功,false则失败,说明其他线程在加锁中这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
- (BOOL)lockBeforeDate:(NSDate *)limit;
//name 是用来标识用的
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
NSRecursiveLock
一个锁可以由同一线程多次获取而不会导致死锁。
NSRecursiveLock定义了一个锁,同一线程可以多次获取该锁而不会导致死锁,这种情况是线程被永久阻塞,等待自己放弃锁。当锁定线程具有一个或多个锁时,将阻止所有其他线程访问受该锁保护的代码。
另外其性能低于NSLock
NSCondition
一个条件变量,其语义遵循用于POSIX样式条件的语义。
条件对象既充当给定线程中的锁又充当检查点。锁在测试条件并执行条件触发的任务时保护您的代码。检查点行为要求条件在线程继续执行其任务之前为真。条件不成立时,线程将阻塞。它保持阻塞状态,直到另一个线程向条件对象发出信号为止。使用NSCondition对象的语义如下:锁定条件对象。测试布尔谓词。(此谓词是代码中的布尔标志或其他变量,指示执行该条件保护的任务是否安全。)如果布尔谓词为false,则调用条件对象的wait或waitUntilDate:方法来阻止线程。从这些方法返回后,请转到步骤2以重新测试您的布尔谓词。(继续等待并重新测试谓词,直到它成立为止。)如果布尔谓词为true,则执行任务。(可选)更新受任务影响的所有谓词(或发出任何条件)。完成任务后,解锁条件对象。
NSConditionLock
可以与特定的用户定义条件相关联的锁。
使用NSConditionLock对象,可以确保线程仅在满足特定条件时才能获取锁。一旦获得了锁并执行了代码的关键部分,线程就可以放弃该锁并将关联条件设置为新的条件。条件本身是任意的:您可以根据应用程序的需要定义它们。
1、初始化 self.condition = [[NSConditionLock alloc]initWithCondition:0];
2、获得锁 [self.condition lockWhenCondition:1];
3、解锁 [self.condition unlockWithCondition:1];
@synchronized
经常用的,不再赘述
dispatch_barrier_async
提交屏障块以异步执行并立即返回。
dispatch_barrier_sync和dispatch_barrier_async的共同点:
1、都会等待在它前面插入队列的任务(1、2、3)先执行完
2、都会等待他自己的任务执行完再执行后面的任务(4、5、6)
dispatch_barrier_sync和dispatch_barrier_async的不共同点:
在将任务插入到queue的时候,dispatch_barrier_sync需要等待自己的任务(0)结束之后才会继续程序,然后插入被写在它后面的任务(4、5、6),然后执行后面的任务
而dispatch_barrier_async将自己的任务(0)插入到queue之后,不会等待自己的任务结束,它会继续把后面的任务(4、5、6)插入到queue
共同点:
1、等待在它前面插入队列的任务先执行完
2、等待他们自己的任务执行完再执行后面的任务
不同点(追加任务的不同):
1、dispatch_barrier_sync将自己的任务插入到队列的时候,需要等待自己的任务结束之后才会继续插入被写在它后面的任务,然后执行它们
2、dispatch_barrier_async将自己的任务插入到队列之后,不会等待自己的任务结束,它会继续把后面的任务插入到队列,然后等待自己的任务结束后才执行后面任务。
所以,dispatch_barrier_async的不等待(异步)特性体现在将任务插入队列的过程,它的等待特性体现在任务真正执行的过程。
转载至:https://blog.csdn.net/ivolcano/article/details/78012385
dispatch_group
这个用的比较多
直接贴一下代码
dispatch_group_t group = dispatch_group_create();
``dispatch_queue_t queue = dispatch_queue_create(``"com.gcd-group.www"``, DISPATCH_QUEUE_CONCURRENT);
``dispatch_group_async(group, queue, ^{
``for` `(``int` `i = 0; i < 1000; i++) {
``if` `(i == 999) {
``NSLog``(@``"11111111"``);
``}
``}
``});
``dispatch_group_async(group, queue, ^{
``NSLog``(@``"22222222"``);
``});
``dispatch_group_async(group, queue, ^{
``NSLog``(@``"33333333"``);
``});
``dispatch_group_notify(group, queue, ^{
``NSLog``(@``"done"``);
``});
等待所有任务完成时再调用dispatch_group_notify
发表回复