在程序开发过程中,经常使用到线程,多线程操作数据时。难免发生一些不可控的情况,造成数据不安全。这个时候我们就用了锁,常用的锁有哪些?有os_unfair_lock、NSLock、NSCondition、NSRecursiveLock等。

一. 线程锁

1、原子属性和锁

通过一个小例子看下无锁情况下的demo:

@property (nonatomic ,assign) int count;

- (void)test {

    for (int i = 0; i < 10; i ++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            self.count --;

            NSLog(@"%d",self.count);

        });

    }

}

复制代码

无锁的情况下打印是无序的。哪我们把 (atomic ,assign) int count;改成原子属性呢? 看到打印结果也是无序的。atomic 原子属性也无法保证数据安全。

哪我们进入objc源码看看:

set方法里没有锁的操作 get方法里有锁的操作看到源码有加锁的操作,哪为什么数据还是不安全呢。self.count --;这个操作即有get操作,也有set操作。但是锁只在get中,所以我们打印的结果并不是有序的。 上图是各种锁的加锁10000次的性能。

锁分:互斥锁(闲等),自旋锁(忙等),读写锁3大类。

ios中有:OSSpinLock、dispatch_semaphore、pthread_mutex、NSCondition、NSLock、pthread_mutex(recursive)、NSRecursiveLock、NSConditionLock、@synchronized9把锁。

2、OSSpinLock,os_unfair_lock锁

OSSpinLock:(它有一个bug,线程优先级反转的问题:优先级低的线程资先执行,然后其他优先级高的在执行会一直占用CPU。优先级底底那个线程会资源无法释放。)

iOS10之后被移除。OS_SPINLOCK_INIT 初始化锁 。OSSpinLockLock(&spinlock)  加锁,参数为OSSPINLOCK地址。OSSpinLockUnlock(&spinlock) 解锁,参数是OSSpinLock地址。OSSpinLockTry(&spinlock) 尝试上锁,参数是OSSpinLock地址。如果返回false,表示上锁失败,锁正在被其他线程持有。如果返回true,表示上锁成功。

os_unfair_lock:

iOS10之后开始支持,用于取代OSSpinLock。OS_UNFAIR_LOCK_INIT 初始化锁。os_unfair_lock_lock 加锁。参数为os_unfair_lock地址。os_unfair_lock_unlock 解锁。参数为os_unfair_lock地址。os_unfair_lock_trylock 尝试加锁。参数为os_unfair_lock地址。如果成功返回true。如果锁已经被锁定则返回false。os_unfair_lock_assert_owner 参数为os_unfair_lock地址。如果当前线程未持有指定的锁或者锁已经被解锁,则触发崩溃。os_unfair_lock_assert_not_owner 参数为os_unfair_lock地址。如果当前线程持有指定的锁,则触发崩溃。

os_unfair_lock例子:

@property (nonatomic ,assign) os_unfair_lock unfairLock;

self.unfairLock = OS_UNFAIR_LOCK_INIT;

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    for (int i = 0; i<10; i++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            [self unfairLock_test];

        });

    }

}

-(void)unfairLock_test {

    os_unfair_lock_lock(&_unfairLock);//加锁

    self.count --;

    NSLog(@"%d",self.count);

    os_unfair_lock_unlock(&_unfairLock);//解锁

}

复制代码

打印结果是有序的。注意:os_unfair_lock_assert_owner如果当前线程未持有指定的锁或者锁已经被解锁,则触发崩溃。和os_unfair_lock_assert_not_owner如果当前线程持有指定的锁,则触发崩溃。

3、NSCondition锁

NSLock:

(void)lock 加锁。(void)unlock 解锁。(BOOL)tryLock 尝试加锁。成功返回YES,失败返回NO。(BOOL)lockBeforeDete:(NSDate *)limit 在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO。@property (nullable ,copy) NSString *name 锁名称。

先通过一个例子:

self.iLock = [[NSLock alloc] init];

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    for (int i = 0; i<10; i++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            [self nslock_test];

        });

    }

}

-(void)nslock_test {

    [self.iLock lock];

    self.count --;

    NSLog(@"%d",self.count);

    [self.iLock unlock];

}

复制代码

打印结果是我们想要的顺序。

如果我们重复调用[self.iLock lock];

NsLock不能循环调用,要不然会死锁。NsLock不支持递归锁。

NSCondition:

(void)lock 加锁。(void)unlock 解锁。(void)wait 阻塞当前线程,使线程进入休眠,等待唤醒信号。调用前必须已加锁。(void)waitUntilDate 阻塞当前线程,使线程进入休眠,等待唤醒信号或者超时。调用前必须已加锁。(void)signal 唤醒一个正在休眠的线程,如果要唤醒多个,需要调用多次。如果没有线程在等待,则什么也不做。调用前必须已加锁。(void)broadcast 唤醒所有在等待的线程。如果没有线程在等待,则什么也不做。调用前必须已加锁。@property (nullable ,copy) NSString *name 锁名称。

我们通过NSCondition 例子进一步了解NSCondition锁:

- (void)nscondition_test {

    for (int i = 0; i < 50; i ++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            [self ny_production];

        });

    }

    for (int i = 0; i < 100; i ++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            [self ny_consumption];

        });

    }

}

- (void)ny_production {

    [self.iCondition lock];

    self.count ++;

    NSLog(@"生产了一个产品,现有产品 : %d个",self.count);

    [self.iCondition signal];//唤醒休眠线程

    [self.iCondition unlock];

}

- (void)ny_consumption {

    [self.iCondition lock];

    while (self.count == 0) {//防止虚假唤醒

        [self.iCondition wait];

    }

    self.count --;

    NSLog(@"消费了一个产品,现有产品: %d个",self.count);

    [self.iCondition unlock];

}

复制代码

一个线程for 50 次生产产品,一个线程 for 100次消费产品。在ny_production 中生产,在 ny_consumption 中消费,如果产品数量count==0 就等待(忙等)。然后消费完解锁。

4、NSRecursiveLock锁

NSConditionLock:

(void)lock 加锁。(void)unlock 解锁。(instancetype)initWithCondition:(NSinteger)初始化一个。NSConditionLock对象。@property(readonly) NSInteger condition 锁的条件。(void)lockWhenCondition:(NSInteger)conditio满足条件时加锁。(BOOL)tryLock尝试加锁。(BOOL)tryLockWhenCondition如果接受对象的condition与给定的condition相等,则尝试获取锁,不足塞线程。(void)unlockWithCondition:(NSInteger)condition解锁,重置锁的条件。(BOOL)lockBeforDate:(NSDate *)limit在指定时间点之前获取锁。(BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit在指定的时间前获取锁。@property (nullable ,copy) NSString *name 锁名称。

先通过一个例子来了解NSConditionLock:

- (void)ny_testConditonLock{

    self.iConditionLock = [[NSConditionLock alloc] initWithCondition:3];

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        [self.iConditionLock lockWhenCondition:3];//只有3==init3时才会打印NSlog

        NSLog(@"线程 1");

        [self.iConditionLock unlockWithCondition:2];

    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        [self.iConditionLock lockWhenCondition:2];//只有2才会打印NSlog

        NSLog(@"线程 2");

        [self.iConditionLock unlockWithCondition:1];

    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        [self.iConditionLock lockWhenCondition:1];//只有1才会打印NSlog

        NSLog(@"线程 3");

        [self.iConditionLock unlockWithCondition:0];

    });

}

复制代码

ConditionLock可以控制线程调用顺序的作用.

NSRecursiveLock:

(void)lock 加锁。(void)unlock 解锁。(BOOL)tryLock 尝试加锁。成功返回YES,失败返回NO。(BOOL)lockBeforeDete:(NSDate *)limit 在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO。@property (nullable ,copy) NSString *name 锁名称。

写一个例子来了解NSRecursiveLock:

- (void)recursiveTest {

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        static void (^recursiveMethod)(int);

        recursiveMethod = ^(int value){

            if (value > 0) {

                [self.iRecursiveLock lock];

                NSLog(@"%d",value);

                recursiveMethod(value - 1);

                [self.iRecursiveLock unlock];

            }

        };

        recursiveMethod(10);

    });

}

复制代码

要注意NSRecursiveLock 只能在同一线程下递归调用。

 

从源码中可以看出,pthread的类型被设置成了RECURSIVE。

5、NSRecursiveLock锁

pthread_mutex:

pthread_mutex_init(pthread_mutex_t mutex,const pthread_mutexattr_t attr)初始化锁,pthread_mutexattr_t可用来设置锁的类型。pthread_mutex_lock(pthread_mutex_t mutex);//加锁pthread_mutex_trylock(*pthread_mutex_t *mutex);//加锁,但是上面方法不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待,成功返回0.失败返回错误信息pthread_mutex_unlock(pthread_mutex_t *mutex);//释放锁pthread_mutex_destroy(pthread_mutex_t* mutex);//使用完锁之后释放锁pthread_mutexattr_setpshared();//设置互斥锁的范围pthread_mutexattr_getpshared() //获取互斥锁的范围

直接上代码:

- (void)ny_pthread_mutex {

    //非递归

    pthread_mutex_t lock0;

    pthread_mutex_init(&lock0, NULL);

    pthread_mutex_lock(&lock0);

    pthread_mutex_unlock(&lock0);

    pthread_mutex_destroy(&lock0);

    

    //递归

    pthread_mutex_t lock;

    pthread_mutexattr_t attr;

    pthread_mutexattr_init(&attr);

    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

    pthread_mutex_init(&lock, &attr);

    pthread_mutexattr_destroy(&attr);

    pthread_mutex_lock(&lock);

    pthread_mutex_unlock(&lock);

    pthread_mutex_destroy(&lock);

}

复制代码

大部分锁都是基于pthread的封装。

dispatch_semaphore_t:

dispatch_semaphore_create(intptr_t value)创建信号量,并且创建的时候需要指定信号量的大小dispatch_semaphore_wait(dispatch_semaphore_t dsema, diapatch_time_t timeout) 等待信号量,如果信号量值为0,那么该函数就会一直等待(相当于阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回。dispatch_semaphore_signal(dispatch_semaphore_t dsema) 发送信号量,该函数会对信号量的值进行加1操作。

写一个小例子:

- (void)lg_dispatch_semaphore_t {

    dispatch_semaphore_t sem = dispatch_semaphore_create(0);

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        NSLog(@"任务1");

        dispatch_semaphore_signal(sem);

    });

    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        NSLog(@"任务2");

        dispatch_semaphore_signal(sem);

    });

    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        NSLog(@"任务3");

    });

}

**2022-06-11 17:38:12.701495+0800 iOSLockDemo[4742:162810] 任务1**

**2022-06-11 17:38:12.702674+0800 iOSLockDemo[4742:162810] 任务2**

**2022-06-11 17:38:12.703386+0800 iOSLockDemo[4742:162810] 任务3**

复制代码

上一章我们已经了解过信号量了,这里不在过多阐述了。

6、读写锁

在开发过程中,我们经常需要达成多读,单写的操作。如何达成这一目的呢?

通过一个例子(来理解):

self.iQueue = dispatch_queue_create("ny", DISPATCH_QUEUE_CONCURRENT);//并发队列

- (NSString *)ny_read {

    // 异步读取

    __block NSString *ret;

    dispatch_sync(self.iQueue, ^{

        // 读取的代码

        ret = self.dataDic[@"name"];

    });

    NSLog(@"%@",ret);

    return ret;

}

- (void)ny_write: (NSString *)name {

    // 写操作

    dispatch_barrier_async(self.iQueue, ^{//栅栏

        [self.dataDic setObject:name forKey:@"name"];

    });

}

复制代码

分析:通过一个并发队列iQueue,写入时用栅栏函数dispatch_barrier_async写入数据,这个时候同队列后面添加的任务就会等待栅栏函数dispatch_barrier_async的任务完成在执行。如果没有写入任务,则读取的任务线程是并发的。 dispatch_sync的ny_read 外部用async来保证多读。(如下图:)

这是打印结果。

总结: 什么是线程安全?

多线程操作共享数据的时候不会出现意想不到的结果就叫线程安全,否则,就是线程不安全。原⼦属性是线程安全的吗?

原⼦属性只能保障set 或者 get的读写安全,但我们在使⽤属性的时候,往往既有set⼜有get,所以说原⼦属性并不是线程安全的。

⾃旋锁和互斥锁的区别

⾃旋锁: 在访问被锁的资源的时候,调⽤者线程不会休眠,⽽是不停循环在那⾥,直到被锁资源释放锁。(忙等)

互斥锁: 在访问被锁资源时,调⽤者线程会休眠,此时cpu可以调度其他线程⼯作。直到被锁的资 源释放锁。然后再唤醒休眠线程。(闲等)

⾃旋锁的优点在于,因为⾃旋锁不会引起调⽤者线程休眠,所以不会进⾏线程调度,cpu时间⽚轮转等⼀些耗时的操作。所以如果能在很短的时间内获得锁,⾃旋锁的效率远⾼于互斥锁。

⾃旋锁缺点在于,⾃旋锁⼀直占⽤CPU,在未获得锁的情况下,⼀直⾃旋,相当于死循环,会⼀直 占⽤着CPU,如果不能在很短的时间内获得锁,这⽆疑会使CPU效率降低。 ⽽且⾃旋锁不能实现递归 调⽤ 。

⾃旋锁优先级反转的bug 当多个线程有优先级的时候,如果⼀个优先级低的线程先去访问某个数据,此时使⽤⾃旋锁进⾏了 加锁,然后⼀个优先级⾼的线程⼜去访问这个数据,那么优先级⾼的线程因为优先级⾼会⼀直占着 CPU资源,此时优先级低的线程⽆法与优先级⾼的线程争夺 CPU 时间,从⽽导致任务迟迟完不成、锁⽆法释放。

由于⾃旋锁本身存在的这个问题,所以苹果在iOS10以后已经废弃了OSSpinLock。

也就是说除⾮⼤家能保证访问锁的线程全部都处于同⼀优先级,否则 iOS 系统中的⾃旋锁就不要去使⽤了。

NSCondition存在的虚假唤醒当线程从等待已发出信号的条件变量中醒来,却发现它等待的条件不满⾜时,就会发⽣虚假唤醒。

之所以称为虚假,是因为该线程似乎⽆缘⽆故地被唤醒了。但是虚假唤醒不会⽆缘⽆故发⽣:它们 通常是因为在发出条件变量信号和等待线程最终运⾏之间,另⼀个线程运⾏并更改了条件。线程之 间存在竞争条件,典型的结果是有时,在条件变量上唤醒的线程⾸先运⾏,赢得竞争,有时它运⾏

第⼆,失去竞争。

在许多系统上,尤其是多处理器系统上,虚假唤醒的问题更加严重,因为如果有多个线程在条件变 量发出信号时等待它,系统可能会决定将它们全部唤醒,将每个signal( )唤醒⼀个线程视为 broadcast( )唤醒所有这些,从⽽打破了信号和唤醒之间任何可能预期的 1:1 关系。如果有 10 个线 程在等待,那么只有⼀个会获胜,另外 9 个会经历虚假唤醒。

读写锁

读写锁的⽬的: 多读单写:在同⼀时刻可以被多条线程进⾏读取数据的操作,但是在同⼀时刻只能有⼀条线程在写⼊数据。

读写互斥:在同以时刻,读和写不能同时进⾏。

文章链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: