在iOS开发里面,锁是为了保护共享资源的访问确保线程安全性和避免竞争条件。iOS的应用通常在多线程的环境下运行,之前学习的多线程GCD
和Operation Queue
都是执行并发任务的,多个线程可能同时访问某个对象,所以锁可以确保每次只有一个线程能够修改或访问共享资源,保护了数据的安全,避免了资源冲突。
在过去几十年并发研究领域的出版物中,锁总是扮演着坏人的角色,锁背负的指控包括引起死锁、锁封护(luyang注:lock convoying,多个同优先级的线程重复竞争同一把锁,此时大量虽然被唤醒而得不到锁的线程被迫进行调度切换,这种频繁的调度切换相当影响系统性能)、饥饿、不公平、data races以及其他许多并发带来的罪孽。有趣的是,在共享内存并行软件中真正承担重担的是——锁。
在计算机科学中,锁是一种同步机制,用于多线程环境中对资源访问的限制。你可以理解成它用于排除并发的一种策略。
if (lock == 0) {
lock = myPID;
}
上面这段代码并不能保证这个任务有锁,因此它可以在同一时间被多个任务执行。这个时候就有可能多个任务都检测到lock是空闲的,因此两个或者多个任务都将尝试设置lock,而不知道其他的任务也在尝试设置lock。这个时候就会出问题了。再看看下面这段代码:
class Account {
private(set) var val: Int = 0
public func add(x: Int) {
objc_sync_enter(self)
defer {
objc_sync_exit(self)
}
val += x
}
public func minus(x: Int) {
objc_sync_enter(self)
defer {
objc_sync_exit(self)
}
val -= x;
}
}
这样就能防止多个任务去修改val了。
锁根据不同的性质可以分成不同的类。
在WiKiPedia介绍中,一般的锁都是建议锁,也就四每个任务去访问公共资源的时候,都需要取得锁的资讯,再根据锁资讯来确定是否可以存取。若存取对应资讯,锁的状态会改变为锁定,因此其他线程不会访问该资源,当结束访问时,锁会释放,允许其他任务访问。有些系统有强制锁,若未经授权的锁访问锁定的资料,在访问时就会产生异常。
在iOS中,锁分为互斥锁、递归锁、信号量、条件锁、自旋锁、读写锁(一种特所的自旋锁)、分布式锁
。
对于数据库
的锁分类:
分类方式 | 分类 |
---|---|
按锁的粒度划分 | 表级锁、行级锁、页级锁 |
按锁的级别划分 | 共享锁、排他锁 |
按加锁的方式划分 | 自动锁、显示锁 |
按锁的使用方式划分 | 乐观锁、悲观锁 |
按操作划分 | DML锁、DDL锁 |
在编程中,引入对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问对象。
// 用在防止多线程访问属性上比较多
- (void)setTestInt:(NSInteger)testInt {
@synchronized (self) {
_testInt = testInt;
}
}
// 定义block类型
typedef void(^MMBlock)(void);
// 定义获取全局队列方法
#define MM_GLOBAL_QUEUE(block) \
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ \
while (1) { \
block();\
}\
})
NSLock *lock = [[NSLock alloc] init];
MMBlock block = ^{
[lock lock];
NSLog(@"执行操作");
sleep(1);
[lock unlock];
};
MM_GLOBAL_QUEUE(block);
pthread除了创建互斥锁,还可以创建递归锁、读写锁、once等锁。稍后会介绍一下如何使用。如果想要深入学习pthread请查阅相关文档、资料单独学习。
静态初始化: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
动态初始化: pthread_mutex_init()
函数是以动态方式创建互斥锁的,参数 attr 指定了新建互斥锁的属性。如果参数 attr 为 NULL ,使用默认的属性,返回0代表初始化成功。这种方式可以初始化普通锁、递归锁(同 ** NSRecursiveLock** ), 初始化方式有些复杂。
此类初始化方法可设置锁的类型,PTHREAD_MUTEX_ERRORCHECK
互斥锁不会检测死锁, PTHREAD_MUTEX_ERRORCHECK
互斥锁可提供错误检查, PTHREAD_MUTEX_RECURSIVE
递归锁, PTHREAD_PROCESS_DEFAULT
映射到 PTHREAD_PROCESS_NORMAL
.
下面源自YYKitcopy:
#import <pthread.h>
//YYKit
static inline void pthread_mutex_init_recursive(pthread_mutex_t *mutex, bool recursive) {
#define YYMUTEX_ASSERT_ON_ERROR(x_) do { \
__unused volatile int res = (x_); \
assert(res == 0); \
} while (0)
assert(mutex != NULL);
if (!recursive) {
//普通锁
YYMUTEX_ASSERT_ON_ERROR(pthread_mutex_init(mutex, NULL));
} else {
//递归锁
pthread_mutexattr_t attr;
YYMUTEX_ASSERT_ON_ERROR(pthread_mutexattr_init (&attr));
YYMUTEX_ASSERT_ON_ERROR(pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE));
YYMUTEX_ASSERT_ON_ERROR(pthread_mutex_init (mutex, &attr));
YYMUTEX_ASSERT_ON_ERROR(pthread_mutexattr_destroy (&attr));
}
#undef YYMUTEX_ASSERT_ON_ERROR
}
__block pthread_mutex_t lock;
pthread_mutex_init_recursive(&lock,false);
MMBlock block0=^{
NSLog(@"线程 0:加锁");
pthread_mutex_lock(&lock);
NSLog(@"线程 0:睡眠 1 秒");
sleep(1);
pthread_mutex_unlock(&lock);
NSLog(@"线程 0:解锁");
};
MM_GLOBAL_QUEUE(block0);
MMBlock block1=^(){
NSLog(@"线程 1:加锁");
pthread_mutex_lock(&lock);
NSLog(@"线程 1:睡眠 2 秒");
sleep(2);
pthread_mutex_unlock(&lock);
NSLog(@"线程 1:解锁");
};
MM_GLOBAL_QUEUE(block1);
MMBlock block2=^{
NSLog(@"线程 2:加锁");
pthread_mutex_lock(&lock);
NSLog(@"线程 2:睡眠 3 秒");
sleep(3);
pthread_mutex_unlock(&lock);
NSLog(@"线程 2:解锁");
};
MM_GLOBAL_QUEUE(block2);
线程 2:加锁
线程 0:加锁
线程 1:加锁
线程 2:睡眠 3 秒
线程 2:解锁
线程 0:睡眠 1 秒
线程 2:加锁
线程 0:解锁
线程 1:睡眠 2 秒
线程 0:加锁
同一个线程可以多次加锁,不会造成死锁
举例:
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value = %d", value);
sleep(2);
RecursiveMethod(value - 1);
}
[lock unlock];
};
RecursiveMethod(5);
});
这段代码是一个典型的死锁情况。在我们的线程中,RecursiveMethod
是递归调用的。所有每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所有它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。控制台会输出如下信息:
value = 5
*** -[NSLock lock]: deadlock ( ‘(null)’) *** Break on _NSLockError() to debug.
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
MM_GLOBAL_QUEUE(^{
static void (^RecursiveBlock)(int);
RecursiveBlock = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"加锁层数 %d", value);
sleep(1);
RecursiveBlock(--value);
}
[lock unlock];
};
RecursiveBlock(3);
});
加锁层数 3
加锁层数 2
加锁层数 1
加锁层数 3
加锁层数 2
加锁层数 1
加锁层数 3
加锁层数 2
__block pthread_mutex_t lock;
//第二个参数为true生成递归锁
pthread_mutex_init_recursive(&lock,true);
MM_GLOBAL_QUEUE(^{
static void (^RecursiveBlock)(int);
RecursiveBlock = ^(int value) {
pthread_mutex_lock(&lock);
if (value > 0) {
NSLog(@"加锁层数 %d", value);
sleep(1);
RecursiveBlock(--value);
}
pthread_mutex_unlock(&lock);
};
RecursiveBlock(3);
});
加锁层数 3
加锁层数 2
加锁层数 1
加锁层数 3
加锁层数 2
加锁层数 1
加锁层数 3
加锁层数 2
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量
同步实现
// 参数可以理解为信号的总量,传入的值必须大于或等于0,否则,返回NULL
// dispatch_semaphore_signal + 1
// dispatch_semaphore_wait等待信号,当 <= 0会进入等待状态
__block dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
MM_GLOBAL_QUEUE(^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"这里简单写一下用法,可自行实现生产者、消费者");
sleep(1);
dispatch_semaphore_signal(semaphore);
});
__block pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
__block pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
MM_GLOBAL_QUEUE(^{
//NSLog(@"线程 0:加锁");
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
NSLog(@"线程 0:wait");
pthread_mutex_unlock(&mutex);
//NSLog(@"线程 0:解锁");
});
MM_GLOBAL_QUEUE(^{
//NSLog(@"线程 1:加锁");
sleep(3);//3秒发一次信号
pthread_mutex_lock(&mutex);
NSLog(@"线程 1:signal");
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
//NSLog(@"线程 1:加锁");
});
NSCondition 的对象实际上作为一个锁和一个线程检查器:锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。
- (void)getIamgeName:(NSMutableArray *)imageNames{
NSCondition *lock = [[NSCondition alloc] init];
NSString *imageName;
[lock lock];
if (imageNames.count>0) {
imageName = [imageNames lastObject];
[imageNames removeObject:imageName];
}
[lock unlock];
}
NSCondition
提供更高级的用法。wait和signal
,和条件信号量类似。比如我们要监听imageNames数组
的个数,当imageNames
的个数大于0的时候就执行清空操作。思路是这样的,当imageNames
个数大于0时执行清空操作,否则,wait
等待执行清空操作。当imageNames
个数增加的时候发生signal
信号,让等待的线程唤醒继续执行。NSCondition
和NSLock
、@synchronized
等是不同的是,NSCondition
可以给每个线程分别加锁,加锁后不影响其他线程进入临界区。这是非常强大。 但是正是因为这种分别加锁的方式,NSCondition
使用wait
并使用加锁后并不能真正的解决资源的竞争。比如我们有个需求:不能让m<0。假设当前m=0,线程A要判断到m>0为假,执行等待;线程B执行了m=1操作,并唤醒线程A执行m-1操作的同时线程C判断到m>0,因为他们在不同的线程锁里面,同样判断为真也执行了m-1,这个时候线程A和线程C都会执行m-1,但是m=1,结果就会造成m=-1.lock
、unlock
是没有问题的。- (void)executeNSCondition {
NSCondition* lock = [[NSCondition alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (NSUInteger i=0; i<3; i++) {
sleep(2);
if (i == 2) {
[lock lock];
[lock broadcast];
[lock unlock];
}
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
[self threadMethodOfNSCodition:lock];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
[self threadMethodOfNSCodition:lock];
});
}
-(void)threadMethodOfNSCodition:(NSCondition*)lock{
[lock lock];
[lock wait];
[lock unlock];
}
lock
不分条件,如果锁没被申请,直接执行代码unlock
不会清空条件,之后满足条件的锁还会执行unlockWithCondition
我的理解就是设置解锁条件(同一时刻只有一个条件,如果已经设置条件,相当于修改条件) lockWhenCondition
满足特定条件,执行相应代码NSConditionLock
同样实现了NSLocking
协议,试验过程中发现性能很低。NSConditionLock
也可以像NSCondition
一样做多线程之间的任务等待调用,而且是线程安全的。- (void)executeNSConditionLock {
NSConditionLock* lock = [[NSConditionLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (NSUInteger i=0; i<3; i++) {
sleep(2);
if (i == 2) {
[lock lock];
[lock unlockWithCondition:i];
}
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
[self threadMethodOfNSCoditionLock:lock];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
[self threadMethodOfNSCoditionLock:lock];
});
}
-(void)threadMethodOfNSCoditionLock:(NSConditionLock*)lock{
[lock lockWhenCondition:2];
[lock unlock];
}
ready_to_go
为false
的时候,进入循环,然后线程将会被挂起,直到另一个线程将ready_to_go
设置为true
的时候,并且发送信号的时候,该线程才会被唤醒。pthread_mutex_t mutex;
pthread_cond_t condition;
Boolean ready_to_go = true;
void MyCondInitFunction()
{
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&condition, NULL);
}
void MyWaitOnConditionFunction()
{
// Lock the mutex.
pthread_mutex_lock(&mutex);
// If the predicate is already set, then the while loop is bypassed;
// otherwise, the thread sleeps until the predicate is set.
while(ready_to_go == false)
{
pthread_cond_wait(&condition, &mutex);
}
// Do work. (The mutex should stay locked.)
// Reset the predicate and release the mutex.
ready_to_go = false;
pthread_mutex_unlock(&mutex);
}
void SignalThreadUsingCondition()
{
// At this point, there should be work for the other thread to do.
pthread_mutex_lock(&mutex);
ready_to_go = true;
// Signal the other thread to begin work.
pthread_cond_signal(&condition);
pthread_mutex_unlock(&mutex);
}
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。
breakLock
方式解锁。读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。
先来一个需求:假设我们原先有6个任务要执行,我们现在要插入一个任务0,这个任务0要在1、2、4都并发执行完之后才能执行,而4、5、6号任务要在这几个任务0结束后才允许并发。
- (void)rwLockOfBarrier {
dispatch_queue_t queue = dispatch_queue_create("thread", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"test1");
});
dispatch_async(queue, ^{
NSLog(@"test2");
});
dispatch_async(queue, ^{
NSLog(@"test3");
});
dispatch_barrier_sync(queue, ^{
for (int i = 0; i <= 500000000; i++) {
if (5000 == i) {
NSLog(@"point1");
}else if (6000 == i) {
NSLog(@"point2");
}else if (7000 == i) {
NSLog(@"point3");
}
}
NSLog(@"barrier");
});
NSLog(@"aaa");
dispatch_async(queue, ^{
NSLog(@"test4");
});
dispatch_async(queue, ^{
NSLog(@"test5");
});
dispatch_async(queue, ^{
NSLog(@"test6");
});
}
dispatch_barrier_sync
将自己的任务插入到队列的时候,需要等待自己的任务结束之后才会继续插入被写在它后面的任务,然后执行它们;dispatch_barrier_async
将自己的任务插入到队列之后,不会等待自己的任务结束,它会继续把后面的任务插入队列,然后等待自己的任务结束后才执行后面的任务。与上述初始化方式类似,静态THREAD_RWLOCK_INITIALIZER
、动态pthread_rwlock_init()
、pthread_rwlock_destroy
用来销毁该锁
#import <pthread.h>
__block pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock,NULL);
//读
MM_GLOBAL_QUEUE(^{
//NSLog(@"线程0:随眠 1 秒");//还是不打印能直观些
sleep(1);
NSLog(@"线程0:加锁");
pthread_rwlock_rdlock(&rwlock);
NSLog(@"线程0:读");
pthread_rwlock_unlock(&rwlock);
NSLog(@"线程0:解锁");
});
//写
MM_GLOBAL_QUEUE(^{
//NSLog(@"线程1:随眠 3 秒");
sleep(3);
NSLog(@"线程1:加锁");
pthread_rwlock_wrlock(&rwlock);
NSLog(@"线程1:写");
pthread_rwlock_unlock(&rwlock);
NSLog(@"线程1:解锁");
});
何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
// 初始化
spinLock = OS_SPINKLOCK_INIT;
// 加锁
OSSpinLockLock(&spinLock);
// 解锁
OSSpinLockUnlock(&spinLock);
不过,自旋锁存在优先级反转的问题。
自旋锁已经不再安全,然后苹果推出了 os_unfair_lock_t ,这个锁解决了优先级反转的问题。
os_unfair_lock_t unfairLock;
unfairLock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(unfairLock);
os_unfair_lock_unlock(unfairLock);
利用setter / getter 接口的属性实现原子操作,进而确保“被共享”的变量在多线程中读写安全,这已经是不能满足部分多线程同步要求。
atomic
nonatomic
注意,atomic一定线程安全吗?答案是否定的
因为atomic只是对属性的setter/getter方法加锁,所以说只能保证在调用setter/getter方法时线程安全。
假设有一个 atomic 的属性 “name”,如果线程 A 调[self setName:@"A"],线程 B 调[self setName:@"B"],线程 C 调[self name]
,那么所有这些不同线程上的操作都将依次顺序执行——也就是说,如果一个线程正在执行 getter/setter,其他线程就得等待。因此,属性 name 是读/写安全的。
但是,如果有另一个线程 D 同时在调[name release],那可能就会crash,因为 release 不受 getter/setter 操作的限制。也就是说,这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。
多用于创建单例。
+ (instancetype) sharedInstance {
static id __instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
__instance = [[self alloc] init];
});
return __instance;
}
// 定义方法
void fun() {
NSLog(@"%@", [NSThread currentThread]);
}
- (void)onceOfPthread {
__block pthread_once_t once = PTHREAD_ONCE_INIT;
int i= 0;
while (i > 5) {
pthread_once(&once, fun);
i++;
}
}
死锁是一个典型的并发问题,它在iOS中可能会发生。在iOS中,如果一个线程在等待一个任务完成,而这个任务又在等待该线程释放某种资源,那么就会产生一个死锁。尤其是在使用GCD(Grand Central Dispatch)
或NSOperation
时,死锁问题可能会更频繁地出现。
Objective-C中的一个典型死锁示例是在主线程上使用同步调度块:
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
// Do some work...
});
在上面的例子中,我们要求主队列同步执行一个任务。但是,因为这是在主线程上发生的,并且主线程正在等待该任务完成,所以就产生了一个死锁。由于主线程已经被阻塞,所以它不能执行队列中的任务,而我们又在等待这个任务完成。
为了防止死锁,我们应该尽量避免在已经在执行任务的线程上同步调度任务。相反,我们应该用异步调度替代,或者使用并行队列来进行同步调度。
以下是一个可以避免死锁的改进示例:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{
// Do some work...
});
在这个例子中,我们使用全局并行队列而不是主队列来同步执行任务。因此,主线程可以继续执行其他任务,而不会被阻塞。
或者:
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
// Do some work...
});
使用异步执行主队列,async允许等待当前队列的任务先执行,也不会阻塞。
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"This is a deadlock");
});
这种情况与第四种死锁是一致的,满足sync中向当前未完成的串行队列发送任务
这一条件。
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
[lock lock];
// some code...
[lock lock]; // this will cause deadlock!
// some code...
[lock unlock];
[lock unlock];
- (void)methodA {
// locking resource X
}
- (void)methodB {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// locking resource Y
[self methodA]; // waiting for resource X to be released
});
[self methodB]; // waiting for resource Y to be released, hence causing deadlock
}
由于我们自己创建并使用queue这个串行队列,但是queue中已经有了任务块,在块内添加任务3,会导致任务块不能完成导致死锁。所以最终结果是打印1,5,2,然后崩溃。这种情况是第一种死锁的父集。
dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1"); // 任务1
dispatch_async(queue, ^{
NSLog(@"2"); // 任务2
dispatch_sync(queue, ^{
NSLog(@"3"); // 任务3
});
NSLog(@"4"); // 任务4
});
NSLog(@"5"); // 任务5
这些例子都揭示了产生死锁的可能性,避免死锁的方式最主要的就是避免同步调用,合理的使用锁以保证线程安全,对于资源的使用,注意资源的请求顺序,尽量减少资源的请求数等等。