iOS線程和鎖實現原理分析

一、線程分享梗概

avatar

二、線程的概念和實現

  1. 線程:是程序執行流的最小單元。一個標準的線程由線程ID,當前指令集合,寄存器集合和棧結構組成。線程是進程中的一個實體,為了解決進程調度的性能損耗問題,被系統獨立調度和分派的基本單位。 線程占用的資源:
  Stack pointer

Registers

Scheduling properties (such as policy or priority)

Set of pending and blocked signals

Thread specific data.

相較于進程,線程有更好的性能,充分利用進程資源(開辟50000個進程和線程的時間對比,單位為秒);


avatar

cpu帶寬(帶寬越高越好):


avatar
  1. 線程實現方式: 如圖所示:
    avatar

    對比如下:
    avatar

三、iOS線程同步方式

Atomic OperationsAtomic Operations是一類針對簡單的數據類型的同步工具,Atomic操作不會阻塞競爭線程。比如自加操作,將會比互斥鎖有更好的性能。 常用的原子操作如下:

  • 原子布爾操作 如OSAtomicOr32,OSAtomicAnd32,OSAtomicXor32等,分別或,與,異或。
  • 原子數學操作 如OSAtomicAdd32,原子相加。
  • 原子比較與交換 OSAtomicCompareAndSwapPtrBarrier,原子性比較并賦值。
  • 原子隊列 原子性地加入和彈出棧。
(一)、Atomic Operations使用舉例
  1. OSAtomicAdd32
int32_t b = 0;

OSAtomicAdd32(a,&b);

b將實現自加操作;因為ios10下不建議使用OSAtomic,建議的api使用舉例如下:

static atomic_int counter = 1;

int oldValue = atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);

同時Atomic還支持or、xor異或、對NSObject變量賦值等操作;

  1. 利用OSAtomicCompareAndSwapPtrBarrier的原子性,來做靜態變量賦值,從而保證單例方法的線程安全。 OSAtomicCompareAndSwapPtrBarrier( void *__oldValue, void *__newValue, void * volatile *__theValue ) 將比較__theValue與__oldValue的值,如果匹配,將執行
  *__theValue=__newValue;

示例代碼如下:

static ViewController * sharedInstance;

+ (ViewController *) sharedInstance {    
  if (!sharedInstance) {       
     id temp = [super allocWithZone:NSDefaultMallocZone()];        //- checks whether       sharedInstance is NULL and only actually sets it to temp to it if it is.        //- This uses hardware support to really, literally only perform the swap once and tell whether it happened.       
     if(OSAtomicCompareAndSwapPtrBarrier(0x0, (__bridge void *)temp, (void *)(&sharedInstance))) {
       ViewController *singleton = (ViewController *) sharedInstance;                  
       [singleton testHash:singleton];        
       return sharedInstance;       
  }       
  else {         
    temp = nil;       
  }    
}  
return nil;}
  1. 原子性入棧、出棧操作; 只適用于結構體類型鏈式結構,不適用于oc對象出棧入棧。
typedef struct elem {        int    data2;        int    data1;        struct elem *link;    } elem_t;

elem_t fred, mary, *p;fred.data1 = 11;mary.data1 = 12;

OSQueueHead q = OS_ATOMIC_QUEUE_INIT;

OSAtomicEnqueue( &q, &fred, offsetof(elem_t,link));OSAtomicEnqueue( &q, &mary, offsetof(elem_t,link));

p = (elem_t *)OSAtomicDequeue( &q, offsetof(elem_t,link));//輸出第一個元素,data1的值為11.
(二)、總結: 針對簡單的賦值操作,如自加、減、異或、取交集并集、給變量賦值等條件下,atomic operations沒有線程上下文切換的開銷,將會有更好的性能。

Memory Barriers and Volatile Variables

(一)、內存屏障--Memory Barriers 1. 有這么一種場景:
1 // thread 1

2 while (!ok);

3 do(x);

4 // thread 2

5 x = 42;

6 ok = 1;

如果第6行的ok=1先于第5行執行,那么do(x),將不會執行,這樣會出現與開發者預先設想的內容不一樣的結果。編譯器為了獲得更好的性能,編譯器會對指令進行重新排序,這將導致cpu訪問內存的順序和我們的想像的不一樣。

  1. Memory Barriers是一種為了讓數據以正確的順序訪問,系統實現的一種非阻塞的同步方式,強制進程強制進程以barrier的順序讀寫內存。

  2. 使用范例:

// thread 1

while (!ok);

do(x);

//    OSMemoryBarrier();

atomic_thread_fence(memory_order_relaxed);

x = 42;

ok = 1;

在需要強制順序執行的代碼塊之間用OSMemoryBarrier或atomic_thread_fence方法分隔開,這將保證指令執行的順序。

(二)、Volatile變量 Volatile變量是提供內存管理的另一種方式。編譯器經常通過從寄存器中讀取變量來優化編譯性能,但在多線程環境下,從寄存器中讀取變量將導致數據不一致問題。Volatile聲明的變量能保證cpu每次從內存中讀取變量。所以系統提供這種方式來解決數據讀取來源的問題。
(三)、總結在Atomic Operations和lock當中,內部會引用Memory Barriers和Volatile來保證數據的訪問一致性。

Locks

(一). Mutex

a. 互斥鎖是使用最廣泛的一種鎖。iOS的NSOperationQueue、GCD、NSThread都是基于pthread_mutex實現的。mutex是一種特殊的信號量,任務總數為1,同一時間內只有一個任務可以進入互斥區。
b. 使用舉例

pthread_mutex_init(&mutt,0);

pthread_mutex_lock(&mutt);

b++;

pthread_mutex_unlock(&mutt);

c.實現原理:mutex底層有實現一個阻塞隊列,如果當前有其他任務正在執行,則加入到隊列中,放棄當前cpu時間片。一旦其他任務執行完,則從隊列中取出等待執行的線程對象,恢復上下文重新執行。

(二). Recursive lock

a. 適合可能會出現遞歸調用的情形,如果用NSLock將導致阻塞。
b. 用法和NSLock一致,只是實例化NSRecursiveLock類而已。
c. 底層采用mutex實現,只設置了pthread的遞歸屬性。

pthread_mutex_t  Mutex;

pthread_mutexattr_t attr;

pthread_mutexattr_init(&attr);

pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

pthread_mutex_init(&Mutex, &attr);

從底層源代碼來看,會檢查當前線程是否已經獲取了鎖,如果是只是增加了引用次數。


avatar
(三). @synchronized 底層采用遞歸鎖實現。 舉例:
  static NSString *test22 = @"test";

    test22 = nullptr;

    static NSMutableArray * ar = [NSMutableArray array];

    NSLog(@"test22 = %@",test22);

    @synchronized(test22) {

        NSLog(@"exe = %lu",(unsigned long)[[NSThread currentThread] hash]);

        if (test22) {

            test22 = nil;

        }

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

            [ar addObject:@(i)];

        }

    }

注意如果@synchronized傳的是一個null對象,將不會有鎖作用。在使用synchronized時,應保證傳入變量的值不會被修改,不會置null,和不會被改成其他值。

avatar

如果傳入的value值變了,這里的SyncData對象是另一個SyncData對象,相應的mutex鎖也發生了變化。不是同一把鎖,也就不能起到線程安全的作用。

typedef struct SyncData {

    struct SyncData* nextData;

    id              object;

    int              threadCount;  // number of THREADS using this block

    recursive_mutex_t        mutex;

} SyncData;
(四). Read-write lock * 針對多讀少寫的情況,有較大的性能優勢。
  • 可以并發讀取資源,但是有寫的請求進來,則需要等讀操作執行完之后才能執行寫。
  • 如果當前正在寫,有讀操作進來,則需要等所有讀操作完成才可以獲得鎖。寫是串行,讀是并行。舉例:
void* readerN(void* arg)

{

    while(1)

    {

        pthread_rwlock_rdlock(&rwlock);

        printf(" N讀者讀出: %d \n",data);

        pthread_rwlock_unlock(&rwlock);

        sleep(1.2);

    }

    return NULL;

}

void* writerA(void* arg)

{

    while(1)

    {

        pthread_rwlock_wrlock(&rwlock);      //寫者加寫鎖

        data++;                              //對共享資源寫數據

        printf("    A寫者寫入: %d\n",data);

        pthread_rwlock_unlock(&rwlock);      //釋放寫鎖

        sleep(2);

    }

    return NULL;

}

void init(){

pthread_rwlock_init(&rwlock, NULL);  //初始化讀寫鎖   

pthread_create(&t1,NULL,readerN,NULL);

pthread_create(&t2,NULL,writerA,NULL);

    pthread_rwlock_destroy(&rwlock);

}
(五). Distributed lock 在多線程訪問共享文件資源時使用,在iOS時用的較少。
(六). Spin lock 自旋鎖通過不斷poll,輪循是否可以訪問共享資源的方式實現線程同步。自旋鎖有造成優先級反轉的風險。 如果低優先級的線程占有了鎖,而此時有很多高優先級的線程在等待,而此時低優先級線程競爭不過高優先級線程,將導致低優先級線程長期占用鎖資源。故蘋果不建議使用。建議采用os_unfair_lock鎖,讓內核接入線程調度的方式避免活鎖現象的發生。舉例:

os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;

os_unfair_lock_lock(&lock);

//do something

os_unfair_lock_unlock(&lock);

(六). Double-checked lock 可能不安全,系統不建議使用。#####(七). Conditions 是一種允許線程發信號通知,在該條件上等待的線程,得到喚醒。很適合生產者消費者模式。舉例:
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];

//線程1

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    [lock lockWhenCondition:1];

    NSLog(@"線程1");

    sleep(2);

    [lock unlock];

});

//線程2

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    sleep(1);//以保證讓線程2的代碼后執行

    if ([lock tryLockWhenCondition:0]) {

        NSLog(@"線程2");

        [lock unlockWithCondition:2];

        NSLog(@"線程2解鎖成功");

    }

    else {

        NSLog(@"線程2嘗試加鎖失敗");

    }

});

//線程4

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    [lock lockWhenCondition:2];

    NSLog(@"線程4");

    [lock unlockWithCondition:1];

    NSLog(@"線程4解鎖成功");

});
(八). atomicatomic只能實現讀寫的線程安全,并不能保證該屬性本身的線程安全。

Perform Selector Routines 采用performSelector: onThread: withObject: waitUntilDone:的方式實現線程同步,但是任務是串行執行的,通過給thread所在的runloop發信號,實現線程通信的。

四、多線程同步造成的問題總結

  1. 一邊枚舉邊寫會crash;
  2. 同時去寫會crash;
  3. 鎖應注意在每個要使用該變量的地方加鎖;
  4. 宜用讀寫鎖解決多讀少寫問題;或者dispatch_barrier;
  5. atomic只能保證set和get的安全,不能保證『改變集合類型變量』的線程安全;
  6. 注意鎖的重入問題,對可能造成遞歸調用的情況用遞歸鎖;
  7. 慎用并發隊列dispatch_get_global_queue;

五、鎖實現原理

  1. 線程的劃分: 從鎖的實現原理上,分為"互斥鎖、自旋鎖、樂觀鎖"。
    a. 互斥鎖基于futex(快速用戶空間互斥體),原子性的訪問各進程共享的外部空間,通過維護一個阻塞隊列,如果當前沒有線程使用該mutex,則直接取用。如果有,則加入隊列,等待喚醒執行。
    b. 自旋鎖,沒有線程上下文的切換,在該線程中不停poll輪循,試探可不可以獲得鎖,這在耗時比較短的線程同步操作的場景下比較適合。
    c. 樂觀鎖。

樂觀鎖( Optimistic Locking ) 相對悲觀鎖而言,樂觀鎖機制采取了更加寬松的加鎖機制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨占性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。而樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基于數據版本( Version )記錄機制實現。何謂數據版本?即為數據增加一個版本標識,在基于數據庫表的版本解決方案中,一般是通過為數據庫表增加一個 “version” 字段來實現。讀取出數據時,將此版本號一同讀出,之后更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號大于數據庫表當前版本號,則予以更新,否則認為是過期數據。

如圖所示:
avatar
  1. 信號量的實現原理:
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
相當于信號量的P操作:
void P(semaphore s){  
s.count--;//信號量s的計數器減1 
 if(s.count < 0){    
//調用p的進程到隊列s.queue上排隊;該進程為阻塞狀態;  
}}
dispatch_semaphore_signal(_semaphore);
相當于信號量的V操作:
void V(semaphore s){
  s.count++; 
 if(s.count <= 0){
     //從隊列s.queue上摘下一個進程,摘下進程狀態為就緒,參與cpu調度 
}}

六、擴展

  1. 悲觀鎖、樂觀鎖、公平鎖、非公平鎖。
  2. 最多可以開辟64個線程

七、各種鎖的性能對比

avatar

執行30萬次i++,并對比加鎖與不加鎖完成所有操作的時間。

八、參考文檔

《操作系統》人民郵電出版社
ios多線程編程指南官方文檔
POSIX Threads Programming——POSIX多線程編程指南(英文版)
@synchronized底層實現,源碼及博客
實現互斥的linux底層Futex接口
apple官方工程師Chris Lattner對自旋鎖不安全的表述:
Memory Barriers
java中的鎖分類
atomic底層實現:
OSAtomicCompareAndSwapPtrBarrier單例模式實現

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容