多线程安全-iOS开发注意咯!!!

简介: 多线程,作为实现软件并发执行的一个重要的方法,也开始具有越来越重要的地位! 正式因为多线程能够在时间片里被CPU快速切换,造就了以下优势 资源利用率更好 程序设计在某些情况下更简单 程序响应更快 但是并不是非常完美,因为多线程常常伴有资源抢夺的问题,作为一个高级开发人员并发编程那是必须要的,同时解决线程安全也成了我们必须要要掌握的基础 原子操作 自旋锁其实就是封装了一个spinlock_t自旋锁 自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。

多线程,作为实现软件并发执行的一个重要的方法,也开始具有越来越重要的地位!

正式因为多线程能够在时间片里被CPU快速切换,造就了以下优势

  • 资源利用率更好
  • 程序设计在某些情况下更简单
  • 程序响应更快

但是并不是非常完美,因为多线程常常伴有资源抢夺的问题,作为一个高级开发人员并发编程那是必须要的,同时解决线程安全也成了我们必须要要掌握的基础

原子操作

自旋锁其实就是封装了一个spinlock_t自旋锁

自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。自旋锁下面还会展开来介绍

互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。

下面是自旋锁的实现原理:

bool lock = false; // 一开始没有锁上,任何线程都可以申请锁
do {
    while(test_and_set(&lock); // test_and_set 是一个原子操作
        Critical section  // 临界区
    lock = false; // 相当于释放锁,这样别的线程可以进入临界区
        Reminder section // 不需要锁保护的代码        
}
复制代码

这里有一篇关于原子性的比较有意思的文章,这里也贴出来,大家可以一起交流讨论 为什么说atomic有时候无法保证线程安全呢? 不再安全的 OSSpinLock

操作在底层会被编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断,去执行别的代码,而我们的原子性的单条指令的执行是不会被打断的,所以保证了安全.

自旋锁的BUG

尽管原子操作非常的简单,但是它只适合于比较简单特定的场合。在复杂的场合下,比如我们要保证一个复杂的数据结构更改的原子性,原子操作指令就力不从心了,

如果临界区的执行时间过长,使用自旋锁不是个好主意。之前我们介绍过时间片轮转算法,线程在多种情况下会退出自己的时间片。其中一种是用完了时间片的时间,被操作系统强制抢占。除此以外,当线程进行 I/O 操作,或进入睡眠状态时,都会主动让出时间片。显然在 while 循环中,线程处于忙等状态,白白浪费 CPU 时间,最终因为超时被操作系统抢占时间片。如果临界区执行时间较长,比如是文件读写,这种忙等是毫无必要的

下面开始我们又爱又恨的

iOS锁

大家也可以参考这篇文章进行拓展:iOS锁

锁并是一种非强制机制,每一个现货出呢个在访问数据或资源之前视图获取(Acquire)锁,并在访问结束之后释放(Release)锁。在锁已经被占用的时候试图获取锁,线程会等待,知道锁重新可用!

信号量

二元信号量(Binary Semaphore) 只有两种状态:占用与非占用。它适合被唯一一个线程独占访问的资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,伺候其他的所有试图获取该二元信号量的线程将会等待,直到该锁被释放

现在我们在这个基础上,我们把学习的思维由二元->多元的时候,我们的信号量由此诞生,多元信号量简称信号量

  • 将信号量的值减1
  • 如果信号量的值小于0,则进入等待状态,否则继续执行。访问玩资源之后,线程释放信号量,进行如下操作
  • 将信号量的值加1
  • 如果信号量的值小于1,唤醒一个等待中的线程
let sem = DispatchSemaphore(value: 1)

for index in 1...5 {
    DispatchQueue.global().async {
        sem.wait()
        print(index,Thread.current)
        sem.signal()
    }
}

输出结果:
1 <NSThread: 0x600003fa8200>{number = 3, name = (null)}
2 <NSThread: 0x600003f90140>{number = 4, name = (null)}
3 <NSThread: 0x600003f94200>{number = 5, name = (null)}
4 <NSThread: 0x600003fa0940>{number = 6, name = (null)}
5 <NSThread: 0x600003f94240>{number = 7, name = (null)}
复制代码

互斥量

互斥量(Mutex)又叫互斥锁和二元信号量很类似,但和信号量不同的是,信号量在整个系统可以被任意线程获取并释放;也就是说哪个线程锁的,要哪个线程释放锁。

具体详细的用法可以参考:常见锁用法

Mutex可以分为递归锁(recursive mutex)非递归锁(non-recursive mutex)。 递归锁也叫可重入锁(reentrant mutex),非递归锁也叫不可重入锁(non-reentrant mutex)。 二者唯一的区别是:

  • 同一个线程可以多次获取同一个递归锁,不会产生死锁。
  • 如果一个线程多次获取同一个非递归锁,则会产生死锁。

NSLock 是最简单额互斥锁!但是是非递归的!直接封装了pthread_mutex 用法非常简单就不做赘述 @synchronized 是我们互斥锁里面用的最频繁的,但是性能最差!

int main(int argc, const char * argv[]) {
    NSString *obj = @"Iceberg";
    @synchronized(obj) {
        NSLog(@"Hello,world! => %@" , obj);
    }
}
复制代码

底层clang

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

    NSString *obj = (NSString *)&__NSConstantStringImpl__var_folders_8l_rsj0hqpj42b9jsw81mc3xv_40000gn_T_block_main_54f70c_mi_0;

    {
        id _rethrow = 0;
        id _sync_obj = (id)obj;
        objc_sync_enter(_sync_obj);
        try {
                struct _SYNC_EXIT {
                    _SYNC_EXIT(id arg) : sync_exit(arg) {}
                    ~_SYNC_EXIT() {
                        objc_sync_exit(sync_exit);
                    }
                    id sync_exit;
                } _sync_exit(_sync_obj);

                NSLog((NSString *)&__NSConstantStringImpl__var_folders_8l_rsj0hqpj42b9jsw81mc3xv_40000gn_T_block_main_54f70c_mi_1 , obj);

            } catch (id e) {
                _rethrow = e;
            }

        {
            struct _FIN {
                _FIN(id reth) : rethrow(reth) {}
                ~_FIN() {
                    if (rethrow)
                        objc_exception_throw(rethrow);
                }
                id rethrow;
            } _fin_force_rethow(_rethrow);
        }
    }

}
复制代码

我们发现objc_sync_enter函数是在try语句之前调用,参数为需要加锁的对象。因为C++中没有try{}catch{}finally{}语句,所以不能在finally{}调用objc_sync_exit函数。因此objc_sync_exit是在_SYNC_EXIT结构体中的析构函数中调用,参数同样是当前加锁的对象。这个设计很巧妙,原因在_SYNC_EXIT结构体类型的_sync_exit是一个局部变量,生命周期为try{}语句块,其中包含了@sychronized{}代码需要执行的代码,在代码完成后,_sync_exit局部变量出栈释放,随即调用其析构函数,进而调用objc_sync_exit函数。即使try{}语句块中的代码执行过程中出现异常,跳转到catch{}语句,局部变量_sync_exit同样会被释放,完美的模拟了finally的功能。

由于篇幅原因,这里分享一篇非常不错的博客:底层分析synchronized

int objc_sync_enter(id obj)
 {
  int result = OBJC_SYNC_SUCCESS;

  if (obj) {
  SyncData* data = id2data(obj, ACQUIRE);
  require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_INITIALIZED, "id2data failed");

  result = recursive_mutex_lock(&data->mutex);
  require_noerr_string(result, done, "mutex_lock failed");
  } else {
  // @synchronized(nil) does nothing
  if (DebugNilSync) {
  _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
  }
  objc_sync_nil();
  }

 done: 
  return result;
 }
复制代码

从上面的源码中我们可以得出你调用sychronized的每个对象,Objective-C runtime都会为其分配一个递归锁并存储在哈希表中。完美

其实如果大家觉得@sychronized性能低的话,完全可以用NSRecursiveLock现成的封装好的递归锁

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    static void (^RecursiveBlock)(int);
    RecursiveBlock = ^(int value) {
        [lock lock];
        if (value > 0) {
            NSLog(@"value:%d", value);
            RecursiveBlock(value - 1);
        }
        [lock unlock];
    };
    RecursiveBlock(2);
});

2016-08-19 14:43:12.327 ThreadLockControlDemo[1878:145003] value:2
2016-08-19 14:43:12.327 ThreadLockControlDemo[1878:145003] value:1
复制代码

条件变量

条件变量(Condition Variable)作为一种同步手段,作用类似一个栅栏。对于条件变量,现成可以有两种操作:

  • 首先线程可以等待条件变量,一个条件变量可以被多个线程等待
  • 其次线程可以唤醒条件变量。此时某个或所有等待此条件变量的线程都会被唤醒并继续支持。

换句话说:使用条件变量可以让许多线程一起等待某个时间的发生,当某个时间发生时,所有的线程可以一起恢复执行!

相信仔细的大家肯定在锁的用法里面见过NSCondition,就是封装了条件变量pthread_cond_t和互斥锁

- (void) signal { 
    pthread_cond_signal(&_condition); 
} 
// 其实这个函数是通过宏来定义的,展开后就是这样 
- (void) lock { 
    int err = pthread_mutex_lock(&_mutex); 
}
复制代码

NSConditionLock借助 NSCondition来实现,它的本质就是一个生产者-消费者模型。“条件被满足”可以理解为生产者提供了新的内容。NSConditionLock 的内部持有一个NSCondition对象,以及 _condition_value属性,在初始化时就会对这个属性进行赋值:

// 简化版代码
- (id) initWithCondition: (NSInteger)value {
    if (nil != (self = [super init])) {
        _condition = [NSCondition new]
        _condition_value = value;
    }
    return self;
}
复制代码

临界区

比互斥量更加严格的同步手段。在术语中,把临界区的获取称为进入临界区,而把锁的释放称为离开临界区。与互斥量和信号量的区别:

  • (1)互斥量和信号量字系统的任何进程都是可见的。
  • (2)临界区的作用范围仅限于本进程,其他进程无法获取该锁。  
// 临界区结构对象
CRITICAL_SECTION g_cs;
// 共享资源
char g_cArray[10];
UINT ThreadProc10(LPVOID pParam)
{
    // 进入临界区
    EnterCriticalSection(&g_cs);
    // 对共享资源进行写入操作
    for (int i = 0; i < 10; i++)
    {
    g_cArray[i]  = a;
    Sleep(1);
    }
    // 离开临界区
    LeaveCriticalSection(&g_cs);
    return 0;
}
UINT ThreadProc11(LPVOID pParam)
{
    // 进入临界区
    EnterCriticalSection(&g_cs);
    // 对共享资源进行写入操作
    for (int i = 0; i < 10; i++)
    {
        g_cArray[10 - i - 1] = b;
        Sleep(1);
    }
    // 离开临界区
    LeaveCriticalSection(&g_cs);
    return 0;
}
……
void CSample08View::OnCriticalSection()
{
    // 初始化临界区
    InitializeCriticalSection(&g_cs);
    // 启动线程
    AfxBeginThread(ThreadProc10, NULL);
    AfxBeginThread(ThreadProc11, NULL);
    // 等待计算完毕
    Sleep(300);
    // 报告计算结果
    CString sResult = CString(g_cArray);
    AfxMessageBox(sResult);
}
复制代码

读写锁

int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);
复制代码

ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。

#include <pthread.h>      //多线程、读写锁所需头文件
pthread_rwlock_t  rwlock = PTHREAD_RWLOCK_INITIALIZER; //定义和初始化读写锁

写模式:
pthread_rwlock_wrlock(&rwlock);     //加写锁
写写写……
pthread_rwlock_unlock(&rwlock);     //解锁  

读模式:
pthread_rwlock_rdlock(&rwlock);      //加读锁
读读读……
pthread_rwlock_unlock(&rwlock);     //解锁 
复制代码
  • 用条件变量实现读写锁

这里用条件变量+互斥锁来实现。注意:条件变量必须和互斥锁一起使用,等待、释放的时候都需要加锁。

#include <pthread.h> //多线程、互斥锁所需头文件

pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER;      //定义和初始化互斥锁
pthread_cond_t  cond = PTHREAD_COND_INITIALIZER;       //定义和初始化条件变量

写模式:
pthread_mutex_lock(&mutex);     //加锁
while(w != 0 || r > 0)
{
     pthread_cond_wait(&cond, &mutex);      //等待条件变量的成立
}
w = 1;

pthread_mutex_unlock(&mutex);
写写写……
pthread_mutex_lock(&mutex);
w = 0;
pthread_cond_broadcast(&cond);       //唤醒其他因条件变量而产生的阻塞
pthread_mutex_unlock(&mutex);    //解锁

读模式:
pthread_mutex_lock(&mutex);     
while(w != 0)
{
     pthread_cond_wait(&cond, &mutex);      //等待条件变量的成立
}
r++;
pthread_mutex_unlock(&mutex);
读读读……
pthread_mutex_lock(&mutex);
r- -;
if(r == 0)
     pthread_cond_broadcast(&cond);       //唤醒其他因条件变量而产生的阻塞
pthread_mutex_unlock(&mutex);    //解锁
复制代码
  • 用互斥锁实现读写锁

这里使用2个互斥锁+1个整型变量来实现

#include <pthread.h> //多线程、互斥锁所需头文件
pthread_mutex_t r_mutex = PTHREAD_MUTEX_INITIALIZER;      //定义和初始化互斥锁
pthread_mutex_t w_mutex = PTHREAD_MUTEX_INITIALIZER; 
int readers = 0;     //记录读者的个数

写模式:
pthread_mutex_lock(&w_mutex);
写写写……
pthread_mutex_unlock(&w_mutex);

读模式:
pthread_mutex_lock(&r_mutex);         

if(readers == 0)
     pthread_mutex_lock(&w_mutex);
readers++;
pthread_mutex_unlock(&r_mutex); 
读读读……
pthread_mutex_lock(&r_mutex);
readers- -;
if(reader == 0)
     pthread_mutex_unlock(&w_mutex);
pthread_mutex_unlock(&r_mutex); 
复制代码
  • 用信号量来实现读写锁

这里使用2个信号量+1个整型变量来实现。令信号量的初始数值为1,那么信号量的作用就和互斥量等价了。

#include <semaphore.h>     //线程信号量所需头文件

sem_t r_sem;     //定义信号量
sem_init(&r_sem, 0, 1);     //初始化信号量 

sem_t w_sem;     //定义信号量
sem_init(&w_sem, 0, 1);     //初始化信号量  
int readers = 0;

写模式:
sem_wait(&w_sem);
写写写……
sem_post(&w_sem);

读模式:
sem_wait(&r_sem);
if(readers == 0)
     sem_wait(&w_sem);
readers++;
sem_post(&r_sem);
读读读……
sem_wait(&r_sem);
readers- -;
if(readers == 0)
     sem_post(&w_sem);
sem_post(&r_sem);
复制代码

线程的安全是现在各个领域在多线程开发必须要掌握的基础!只有对底层有所掌握,才能在真正的实际开发中游刃有余!现在的iOS开发乃至其他开发都是表面基础层开发,真正大牛开发之路还请继续努力,这一篇博客以供大家一起学习!

目录
相关文章
|
1月前
|
Java Android开发 Swift
安卓与iOS开发对比:平台选择对项目成功的影响
【10月更文挑战第4天】在移动应用开发的世界中,选择合适的平台是至关重要的。本文将深入探讨安卓和iOS两大主流平台的开发环境、用户基础、市场份额和开发成本等方面的差异,并分析这些差异如何影响项目的最终成果。通过比较这两个平台的优势与挑战,开发者可以更好地决定哪个平台更适合他们的项目需求。
110 1
|
1月前
|
设计模式 安全 Swift
探索iOS开发:打造你的第一个天气应用
【9月更文挑战第36天】在这篇文章中,我们将一起踏上iOS开发的旅程,从零开始构建一个简单的天气应用。文章将通过通俗易懂的语言,引导你理解iOS开发的基本概念,掌握Swift语言的核心语法,并逐步实现一个具有实际功能的天气应用。我们将遵循“学中做,做中学”的原则,让理论知识和实践操作紧密结合,确保学习过程既高效又有趣。无论你是编程新手还是希望拓展技能的开发者,这篇文章都将为你打开一扇通往iOS开发世界的大门。
|
1月前
|
搜索推荐 IDE API
打造个性化天气应用:iOS开发之旅
【9月更文挑战第35天】在这篇文章中,我们将一起踏上iOS开发的旅程,通过创建一个个性化的天气应用来探索Swift编程语言的魅力和iOS平台的强大功能。无论你是编程新手还是希望扩展你的技能集,这个项目都将为你提供实战经验,帮助你理解从构思到实现一个应用的全过程。让我们开始吧,构建你自己的天气应用,探索更多可能!
63 1
|
2月前
|
IDE Android开发 iOS开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
【9月更文挑战第27天】在移动应用开发的世界中,Android和iOS是两个主要的操作系统平台。每个系统都有其独特的开发环境、工具和用户群体。本文将深入探讨这两个平台的关键差异点,并分析这些差异如何影响应用的性能、用户体验和最终的市场表现。通过对比分析,我们将揭示选择正确的开发平台对于确保项目成功的重要作用。
|
6天前
|
安全 数据处理 Swift
深入探索iOS开发中的Swift语言特性
本文旨在为开发者提供对Swift语言在iOS平台开发的深度理解,涵盖从基础语法到高级特性的全面分析。通过具体案例和代码示例,揭示Swift如何简化编程过程、提高代码效率,并促进iOS应用的创新。文章不仅适合初学者作为入门指南,也适合有经验的开发者深化对Swift语言的认识。
24 9
|
6天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
|
4天前
|
iOS开发 开发者
探索iOS开发中的SwiftUI框架
【10月更文挑战第39天】在苹果的生态系统中,SwiftUI框架以其声明式语法和易用性成为开发者的新宠。本文将深入SwiftUI的核心概念,通过实际案例展示如何利用这一框架快速构建用户界面,并探讨其对iOS应用开发流程的影响。
|
7天前
|
JSON 前端开发 API
探索iOS开发之旅:打造你的第一个天气应用
【10月更文挑战第36天】在这篇文章中,我们将踏上一段激动人心的旅程,一起构建属于我们自己的iOS天气应用。通过这个实战项目,你将学习到如何从零开始搭建一个iOS应用,掌握基本的用户界面设计、网络请求处理以及数据解析等核心技能。无论你是编程新手还是希望扩展你的iOS开发技能,这个项目都将为你提供宝贵的实践经验。准备好了吗?让我们开始吧!
|
12天前
|
设计模式 前端开发 Swift
探索iOS开发:从初级到高级的旅程
【10月更文挑战第31天】在这篇文章中,我们将一起踏上iOS开发的旅程。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧。我们将从基础开始,逐步深入到更高级的技术和概念。让我们一起探索iOS开发的世界吧!
|
14天前
|
设计模式 前端开发 Swift
探索iOS开发:从初级到高级的旅程
【10月更文挑战第28天】在这篇技术性文章中,我们将一起踏上一段探索iOS开发的旅程。无论你是刚入门的新手,还是希望提升技能的开发者,这篇文章都将为你提供宝贵的指导和灵感。我们将从基础概念开始,逐步深入到高级主题,如设计模式、性能优化等。通过阅读这篇文章,你将获得一个清晰的学习路径,帮助你在iOS开发领域不断成长。
46 2