你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 正確使用@synchronized()

正確使用@synchronized()

編輯:IOS開發基礎

在上篇多線程安全的文章中,我曾推薦過大家使用@synchronized來使得代碼獲得原子性,從而保證多線程安全。這篇文章向大家介紹一些@synchronized的知識點和應該避免的坑。

@synchronized原理

@synchronized是幾種iOS多線程同步機制中最慢的一個,同時也是最方便的一個。

蘋果建立@synchronized的初衷就是方便開發者快速的實現代碼同步,語法如下:

@synchronized(obj) {
  //code
}

為了加深理解,我們刨一刨代碼看看@synchronized到底做了什麼事。我在一個測試工程的main.m中寫了一段代碼:

void testSync()
{
    NSObject* obj = [NSObject new];
    @synchronized (obj) {
    }
}

然後在Xcode中選擇菜單Product->Perform Action->Assemble “main.m”,就得到了如下的匯編代碼:

1.jpg

上圖中我將關鍵代碼用紅線標出了,很容易就定位到了我們的目標代碼。

ARC幫我們插入的retain,release也在其中:),我們感興趣的部分是下面兩個函數:

bl    _objc_sync_enter
bl    _objc_sync_exit

這兩個函數應該就是synchronized進入和退出的調用,下面去Objective C的源碼裡找找 :)

在源碼中一搜,很快就發現了這兩個函數:

// Begin synchronizing on 'obj'. // Allocates recursive mutex associated with 'obj' if needed.// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
int objc_sync_enter(id obj)
{    
  int result = OBJC_SYNC_SUCCESS;  
  if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        assert(data);
        data->mutex.lock();
    } 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();
    }    
    return result;
}// End synchronizing on 'obj'. // Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{    
  int result = OBJC_SYNC_SUCCESS;   
   if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } 
        else {            
            bool okay = data->mutex.tryUnlock();          
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    }
    else {        // @synchronized(nil) does nothing
    }    
    return result;
}

從上述源碼中,我們至少可以確立兩個信息:

synchronized是使用的遞歸mutex來做同步。

@synchronized(nil)不起任何作用

遞歸mutex的意思是,我們可以寫如下代碼:

@synchronized (obj) {
    NSLog(@"1st sync");
    @synchronized (obj) {
        NSLog(@"2nd sync");
    }
}

而不會導致死鎖。我順道扒了下java當中的synchronized關鍵字,發現也是使用的遞歸鎖,看來這是個common trick。recursive mutex其實裡面還是使用了pthread_mutex_t,只不過多了一層ownership的判斷,性能上比非遞歸鎖要稍微慢一些。

@synchronized(nil)不起任何作用,表明我們需要適當關注傳入的object的聲明周期,一旦置為nil之後就無法做代碼同步了。

我們再看看傳入的obj參數有什麼作用。

繼續看代碼發現傳入的obj被用作參數來獲取SyncData對象,裡面有一大段關於SyncData的cache邏輯,有興趣的同學可以自己看下代碼,這是一個兩層的cache設計,第一層是tls cache,第二層是自己維護的一個hash map。這裡將流程簡化,來看下obj是如何在hash map中緩存的。

先看下SyncData獲取的方式:

SyncData **listp = &LIST_FOR_OBJ(object);

而LIST_FOR_OBJ又指向:

#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap sDataLists;

再看下StripedMap的實現就很清楚了:

static unsigned int indexForPointer(const void *p) {
    uintptr_t addr = reinterpret_cast(p); 
    return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
public:
T& operator[] (const void *p) { 
    return array[indexForPointer(p)].value; 
}

indexForPointer中使用了obj的內存地址,做了個簡單的map,映射到另一個內存空間來存放SyncList。

通過上述分析,我們可以得出結論了:

synchronized中傳入的object的內存地址,被用作key,通過hash map對應的一個系統維護的遞歸鎖。

以上就是object的用處,所以不管是傳入什麼類型的object,只要是有內存地址,就能啟動同步代碼塊的效果。

消化完synchronized的內部實現,我們再來看看平常使用中常見的一些坑。

慎用@synchronized(self)

我其實更想說:不要使用@synchronized(self)。

我看過不少代碼都是直接將self傳入@synchronized當中,這是種很粗糙的使用方式,容易導致死鎖的出現。比如:

//class A
@synchronized (self) {
    [_sharedLock lock];
    NSLog(@"code in class A");
    [_sharedLock unlock];
}
//class B
[_sharedLock lock];
@synchronized (objectA) {
    NSLog(@"code in class B");
}
[_sharedLock unlock];

原因是因為self很可能會被外部對象訪問,被用作key來生成一鎖,類似上述代碼中的@synchronized (objectA)。兩個公共鎖交替使用的場景就容易出現死鎖。

所以正確的做法是傳入一個類內部維護的NSObject對象,而且這個對象是對外不可見的。

精准的粒度控制

有些人說@synchronized慢,但@synchronized和其他同步鎖的性能相比並沒有很誇張,對於使用者來說幾乎忽略不計。

之所以慢是更多的因為沒有做好粒度控制。鎖本質上是為了讓我們的一段代碼獲得原子性,不同的critical section要使用不同的鎖。我見過很多類似的寫法:

@synchronized (sharedToken) {
    [arrA addObject:obj];
}
@synchronized (sharedToken) {
    [arrB addObject:obj];
}

使用同一個token來同步arrA和arrB的訪問,雖然arrA和arrB之間沒有任何聯系。傳入self的就更不對了。

應該是不同的數據使用不同的鎖,盡量將粒度控制在最細的程度。上述代碼應該是:

@synchronized (tokenA) {
    [arrA addObject:obj];
}
@synchronized (tokenB) {
    [arrB addObject:obj];
}

注意內部的函數調用

@synchronized還有個很容易變慢的場景,就是{}內部有其他隱蔽的函數調用。比如:

@synchronized (tokenA) {
    [arrA addObject:obj];
    [self doSomethingWithA:arrA];
}

doSomethingWithA內部可能又調用了其他函數,維護doSomethingWithA的工程師可能並沒有意識到自己是被鎖同步的,由此層層疊疊可能引入更多的函數調用,代碼就莫名其妙的越來越慢了,感覺鎖的性能差,其實是我們沒用好。

所以在書寫@synchronized內部代碼的時候,要十分小心內部隱蔽的函數調用。

總結

看似簡單的API調用,背後其實包含了不少知識,知其所以然才能運用得當。關於@synchronized(xxx)就介紹到這裡,希望有將synchronized解釋清楚:)

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved