iOS线程安全——锁(一)

简介: iOS线程安全——锁(一)

线程安全是iOS开发中避免不了的话题,随着多线程的使用,对于资源的竞争以及数据的操作都可能存在风险,所以有必要在操作时保证线程安全。线程安全是多线程技术的保障,而iOS中实现线程安全主要是依靠各种锁,锁的种类有很多,有各自的优缺点,需要开发者在使用中权衡利弊,选择最合适的锁来搭配多线程技术。


随着项目越来越庞大且越来越复杂,对项目中事务的处理、多线程的使用也变得尤为必要。多线程利用了CPU多核的性质,能并行执行任务,提高效率,但是随之而来也会出现一些由于多线程使用而造成的问题。锁主要可以分为几种:互斥锁,递归锁,信号量,条件锁等。锁的功能就是为了防止不同的线程同时访问同一段代码。下面简单举个例子。

现在有一个对象Person类,其中有一个NSUInteger(年龄大于等于0,为无符整型)类型的属性age。

//  Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatiomic, assign) NSUInteger age;
@end


当然在未赋值的情况下,age默认是0。我们在外部模拟一种多线程访问该实例方法的情况。

- (void)withoutLock {
    __block Person *p = [Person new];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < 1000; i++) {
            p.age++;
        }
        NSLog(@"%zd \n",p.age);
    }];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < 1000; i++) {
            p.age++;
        }
        NSLog(@"%zd \n",p.age);
    }];
}


不要在意数值的大小,这里只是为了达到模拟的效果。可以看出,有两处代码,在不同的线程中调用了p.age++,按理想情况来说,结果应该是p的age是2000,但是分别打印两个线程代码执行完后的结果却并非如此(每次执行结果都基本不相同,所以只是以某次为例,并不是确定值)。

1170
1906


因为是不同的线程,所以不确定哪一个会先执行结束,所以分别打印了一次。可以看到,最大值是1906,表示最后p的age是1906,并没有到达2000。相信有一定基础的读者都会明白其原因,因为在该处方法中没有加锁,导致不同线程竞争资源,当A线程和B线程同时拿到age时,例如此时age的值是100,执行自增代码后,A线程和B线程都将101赋给了age,但总得有个先来后到,结果就是某一次被覆盖了,按理说两次p.age++后结果应该是102,但获取到的结果是101,这就出现了误差,所以多次这样的误差就导致最后的结果值是比2000要小的。

所以这个时候,在多线程访问同一资源时要通过锁来保证同一时刻仅有一个线程对该资源的访问,这样就可以避免上述出现的问题。iOS系统提供了多种锁来解决这样的问题,下面分别介绍一下各种锁。(在下面的例子中我们设置一个统一的次数变量totalCount,设置一个比较大的数值100000。)


NSLock


NSLock是一种最简单的锁,使用起来也比较简单方便,下面通过实际代码来看一下。


- (void)nslockTest {
    __block Person *p = [Person new];
    NSLock *lock = [[NSLock alloc]init];
    NSLog(@"begin");
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totcalCount; i++) {
            [lock lock];
            p.age++;
            [lock unlock];
        }
        NSLog(@"%zd \n",p.age);
    }];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totcalCount; i++) {
            [lock lock];
            p.age++;
            [lock unlock];
        }
        NSLog(@"%zd \n",p.age);
    }];
}


NSLock使用起来也比较简单,用创建的实例对象调用lock和unlock方法来加锁解锁。通过打印可以看到,结果是正确的,最后的age是2000。


synchronized


这种锁是比较常用的,因为其使用方法是所有锁中最简单的,但性能却是最差的,所以对性能要求不太高的使用情景下synchronized不失为一种比较方便的锁。代码如下。


- (void)synchronizedTest {
    __block Person *p = [Person new];
    NSLog(@"begin");
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totcalCount; i++) {
            @synchronized (p) {
                p.age++;
            }
        }
        NSLog(@"%zd \n",p.age);
    }];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totcalCount; i++) {
            @synchronized (p) {
                p.age++;
            }
        }
        NSLog(@"%zd \n",p.age);
    }];
}


可以看出不需要创建锁,一种类似于Swift中调用一个含有尾随闭包的函数,就能实现功能。

synchronized内部实现是通过传入的对象,为其分配一个递归锁,存储在哈希表中。使用synchronized还需要有一些注意的地方,除了有性能方面的劣势,还有两个问题,一个是小括号里面需要传一个对象类型,基本数据类型不能作为参数,另一个是小括号内的这个对象参数不可为空,如果为nil,就不能保证其锁的功能。我们创建另外一个值为nil的对象,传进去:

- (void)synchronizedTest {
    __block Person *p = [Person new];
    __block Person *p1 = nil;
    NSLog(@"begin");
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totcalCount; i++) {
            @synchronized (p1) {
                p.age++;
            }
        }
        NSLog(@"%zd \n",p.age);
    }];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totcalCount; i++) {
            @synchronized (p1) {
                p.age++;
            }
        }
        NSLog(@"%zd \n",p.age);
    }];
}


打印结果如下。

begin
113750
124617


从打印结果可以看到,两次打印并没有一次能够达到两次循环次数的总和。也就是说明如果传值nil的话,就失去了synchronized提供的锁功能。


pthread


pthread的全称是POSIX thread,是一套跨平台的多线程API,各个平台对其都有实现。pthread是一套非常强大的多线程锁,可以创建互斥锁(普通锁)、递归锁、信号量、条件锁、读写锁、once锁等,基本上所有涉及的锁,都可以用pthread来实现,下面分别对其进行举例。


1.互斥锁(普通锁)

- (void)pthreadNormalTest {
    __block Person *p = [Person new];
    NSLog(@"begin");
    __block pthread_mutex_t t;
    pthread_mutex_init(&t, NULL);
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totcalCount; i++) {
            pthread_mutex_lock(&t);
            p.age++;
            pthread_mutex_unlock(&t);
        }
        NSLog(@"%zd \n",p.age);
    }];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totcalCount; i++) {
            pthread_mutex_lock(&t);
            p.age++;
            pthread_mutex_unlock(&t);
        }
        NSLog(@"%zd \n",p.age);
    }];
}


可以看到普通的互斥锁的创建和使用也是比较简单的,但是需要注意在合适的地方对其调用方法进行销毁。

pthread_mutex_destroy(&t);


注意:在本节关于锁的实例代码中,都将锁的创建放在了方法中,但在实际开发中,多线程都是直接调用方法的,所以也就应使用同一个锁对象。为了保证锁的正常使用,一般将其设置为方法所属对象的一个属性,才能在调用该对象的方法时保证其线程安全,而不是像例子中那样在方法中创建,示例代码仅为演示效果,希望读者能够理解。


2.递归锁


递归锁的创建方法跟普通锁是同一个方法,不过需要传递一个attr参数。

- (void)pthreadRecursiveTest {
    __block Person *p = [Person new];
    NSLog(@"begin");
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, &attr);
    pthread_mutexattr_destroy(&attr);
    __block pthread_mutex_t t = mutex;
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totalCount; i++) {
            pthread_mutex_lock(&t);
            p.age++;
            pthread_mutex_unlock(&t);
        }
        NSLog(@"% zd \n",p.age);
    }];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totalCount; i++) {
            pthread_mutex_lock(&t);
            p.age++;
            pthread_mutex_unlock(&t);
        }
        NSLog(@"% zd \n",p.age);
    }];
}


关于普通锁和递归锁的区别,后面再做陈述,这里先简单介绍锁的用法。

同样,关于pthread递归锁需要注意的是,首先对其属性需要在创建完递归锁之后释放:

pthread_mutexattr_destroy(&attr);


另外,同样也要注意在该锁所对应的对象释放的时候也要对该锁调用释放方法。

pthread_mutex_destroy(&t);


3.pthread信号量


pthread的信号量不同于GCD自带的信号量,如前面所说,pthread是跨平台多线程处理的API,对信号量处理也提供了相应的使用。其大概原理和使用方法与GCD提供的信号量机制类似,使用起来也比较方便。关于GCD的信号量在下面会单独讲到,这里是pthread信号量的使用代码。


- (void)pthreadCondTest {
    __block Person *p = [Person new];
    __block pthread_mutex_t t = PTHREAD_MUTEX_INITIALIZER;
    __block pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    NSLog(@"begin");
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totalCount; i++) {
            pthread_mutex_lock(&t);
            pthread_cond_wait(&cond,&t);
            p.age++;
            pthread_mutex_unlock(&t);
        }
        NSLog(@"% zd \n",p.age);
    }];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totalCount; i++) {
            pthread_mutex_lock(&t);
            p.age++;
            pthread_cond_signal(&cond);
            pthread_mutex_unlock(&t);
        }
        NSLog(@"% zd \n",p.age);
    }];
}


通过使用方法可以看到,pthread_cond_t是需要搭配pthread普通锁共同使用的,是通过pthread_cond_wait和pthread_cond_signal来实现信号量的生产和消费。但与GCD的信号量略有不同,首先pthread_cond_t需要搭配pthread普通锁一起使用,其次pthread_cond_t不能设置信号量的个数,纯粹是一个信号量锁。


pthread_cond_t也可以称为pthread状态锁,如果是第一个线程先获得调度,在第一个线程内调用pthread_mutex_lock(&t)之后,需要等一个信号量才能继续执行,此时内部会将其unlock,然后等第二个线程调度,当第二个线程完成后释放了一个信号并解锁后,线程1重新得到调度,此时在pthread_cond_wait内部重新上锁,然后继续执行线程1的代码。当消耗了这个信号量,下次线程1再获得调度时仍然会阻塞,然后周而复始。

如果读者执行上面这段代码,可以发现,控制台并没有打印两次p.age,综合刚刚的解释,可以明白,控制台打印的是线程2的NSLog,然后线程1消耗了信号量之后并没有其他信号量可以使用,所以一直处于阻塞状态。虽然在此例中并没有完全执行p.age到200 000,但这种状态锁是一种自定义的任务调度方式,可以将指定的事务交给指定的线程来处理。

可以看出,pthread使用信号量来实现线程安全也是比较方便的,通过一个宏来初始化pthread_mutex_t,在涉及锁功能时,pthread_cond_t需要注意与锁的使用搭配。

pthread_mutex_lock(&t);
pthread_cond_wait(&cond,&t);
//CODE
pthread_mutex_unlock(&t);


在调用pthread_cond_wait之前需要先上锁,因为在没有信号量可以消费的时候pthread_cond_wait会解锁,并在获得新的信号量时再次对其加锁。

pthread_mutex_lock(&t);
//CODE
pthread_cond_signal(&cond);
pthread_mutex_unlock(&t);


这段代码需要在执行完代码后先释放信号量,再对其解锁,这样线程1才能获取到锁,并且pthread_cond_wait才能对其重新上锁往下执行。如果是先释放锁,可能线程1获取到锁仍然不能执行,等再释放信号,线程1又得重新获取一遍,更有甚者,此时锁可能又被线程2抢去了。


4.读写锁


读写锁是一种特殊的自旋锁,将对资源的访问者分为读者和写者,顾名思义,读者对资源只进行读访问,而写者对资源只有写访问。相对于自旋锁来说,这种锁能提高并发性。在多核处理器操作系统中,允许多个读者访问同一资源,却只能有一个写者执行写操作,并且读写操作不能同时执行。

- (void)readWriteLockTest {
    __block Person *p = [Person new];
    __block pthread_rwlock_t rwl = PTHREAD_RWLOCK_INITIALIZER;
    NSLog(@"begin");
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totalCount; i++) {
            pthread_rwlock_rdlock(&rwl);
            p.age++;
            pthread_rwlock_unlock(&rwl);
        }
        NSLog(@"% zd \n",p.age);
    }];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totalCount; i++) {
            pthread_rwlock_wrlock(&rwl);
            p.age++;
            pthread_rwlock_unlock(&rwl);
        }
        NSLog(@"% zd \n",p.age);
    }];
}


pthread的读写锁的初始化方法也是通过一个宏返回的,这是pthread为我们提供好的静态初始化宏。读写锁在具体使用的时候有三个方法,一个是给读操作上锁pthread_rwlock_rdlock,一个是给写操作上锁pthread_rwlock_wrlock,两个方法参数都是传一个读写锁的指针;最后一个方法是给读操作和写操作解锁pthread_rwlock_unlock,参数也是读写锁的指针。


这里很有意思,虽然通过打印,发现也实现了我们所要的功能,但是使用情景却是不对的,可以说是误打误撞的使用范例。原因是这样的,刚刚上面提到,读写锁是不能共存的,而且读操作是可以多个同时存在并执行的,但是写操作却只能存在一个,并且读与写也不能同时存在,读操作和其他的写操作会在当前写操作执行时,所以导致了上面的在“错误”的逻辑下却产生了“正确”的结果,因为读和写是互斥的,所以实现了锁的功能。然而这并不是正确的使用场景,正确的场景还是应该用在例如读写文件中,在上读锁跟解锁之间执行读操作,在上写锁跟解锁之间执行写操作。

所以读写锁保障了读写的安全性和有效性,并且更多的是读操作,由于这种逻辑处理,导致读写锁性能比普通锁要稍微低一点儿,但也算有比较方便的实用性。

目录
相关文章
|
1月前
|
安全 编译器 C#
C#学习相关系列之多线程---lock线程锁的用法
C#学习相关系列之多线程---lock线程锁的用法
|
3月前
多线程并发锁的方案—原子操作
多线程并发锁的方案—原子操作
|
2月前
|
安全 Java C++
解释Python中的全局解释器锁(GIL)和线程安全的概念。
解释Python中的全局解释器锁(GIL)和线程安全的概念。
27 0
|
3月前
|
数据处理
多线程与并发编程【线程对象锁、死锁及解决方案、线程并发协作、生产者与消费者模式】(四)-全面详解(学习总结---从入门到深化)
多线程与并发编程【线程对象锁、死锁及解决方案、线程并发协作、生产者与消费者模式】(四)-全面详解(学习总结---从入门到深化)
44 1
|
1月前
|
存储 安全 Java
并发编程知识点(volatile、JMM、锁、CAS、阻塞队列、线程池、死锁)
并发编程知识点(volatile、JMM、锁、CAS、阻塞队列、线程池、死锁)
71 3
|
12天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
15天前
|
存储 缓存 Java
线程同步的艺术:探索 JAVA 主流锁的奥秘
本文介绍了 Java 中的锁机制,包括悲观锁与乐观锁的并发策略。悲观锁假设多线程环境下数据冲突频繁,访问前先加锁,如 `synchronized` 和 `ReentrantLock`。乐观锁则在访问资源前不加锁,通过版本号或 CAS 机制保证数据一致性,适用于冲突少的场景。锁的获取失败时,线程可以选择阻塞(如自旋锁、适应性自旋锁)或不阻塞(如无锁、偏向锁、轻量级锁、重量级锁)。此外,还讨论了公平锁与非公平锁,以及可重入锁与非可重入锁的特性。最后,提到了共享锁(读锁)和排他锁(写锁)的概念,适用于不同类型的并发访问需求。
246 2
|
16天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
24天前
|
安全 Java 调度
深入理解Java中的线程安全与锁机制
【4月更文挑战第6天】 在并发编程领域,Java语言提供了强大的线程支持和同步机制来确保多线程环境下的数据一致性和线程安全性。本文将深入探讨Java中线程安全的概念、常见的线程安全问题以及如何使用不同的锁机制来解决这些问题。我们将从基本的synchronized关键字开始,到显式锁(如ReentrantLock),再到读写锁(ReadWriteLock)的讨论,并结合实例代码来展示它们在实际开发中的应用。通过本文,读者不仅能够理解线程安全的重要性,还能掌握如何有效地在Java中应用各种锁机制以保障程序的稳定运行。
|
1月前
|
Linux API C++
【Linux C/C++ 线程同步 】Linux API 读写锁的编程使用
【Linux C/C++ 线程同步 】Linux API 读写锁的编程使用
21 1