iOS開發:深入理解GCD 第二篇(dispatch_group、dispatch_barrier、基於線程安全的多讀單寫)


  1. Dispatch Group
    在追加到Dispatch Queue中的多個任務處理完畢之后想執行結束處理,這種需求會經常出現。如果只是使用一個Serial Dispatch Queue(串行隊列)時,只要將想執行的處理全部追加到該串行隊列中並在最后追加結束處理即可,但是在使用Concurrent Queue 時,可能會同時使用多個Dispatch Queue時,源代碼就會變得很復雜。

    在這種情況下,就可以使用Dispatch Group。

    dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t queue = dispatch_queue_create("com.gcd-group.www", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_group_async(group, queue, ^{
            for (int i = 0; i < 1000; i++) {
                if (i == 999) {
                    NSLog(@"11111111");
                }
            }
            
        });
        
        dispatch_group_async(group, queue, ^{
            NSLog(@"22222222");
        });
        
        dispatch_group_async(group, queue, ^{
            NSLog(@"33333333");
        });
        
        dispatch_group_notify(group, queue, ^{
            NSLog(@"done");
        });
    

    控制台的輸出:


    因為向Concurrent Dispatch Queue 追加處理,多個線程並行執行,所以追加處理的執行順序不定。執行順序會發生變化,但是此執行結果的done一定是最后輸出的。

    無論向什么樣的Dispatch Queue中追加處理,使用Dispatch Group都可以監視這些處理執行的結果。一旦檢測到所有處理執行結束,就可以將結束的處理追加到Dispatch Queue中,這就是使用Dispatch Group的原因。

    下面試一個使用Dispatch Group異步下載兩張圖片,然后合並成一張圖片的medo(注意,我們總是應該在主線程中更新UI):

    #import "ViewController.h"
    
    @interface ViewController ()
    @property (nonatomic, strong) UIImage *imageOne;
    @property (nonatomic, strong) UIImage *imageTwo;
    @property (nonatomic, weak) UILabel *textLabel;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        [self operation1];
    }
    
    - (void)operation1
    {
        UILabel *textLabel = [[UILabel alloc] initWithFrame:CGRectMake(200, 450, 0, 0)];
        textLabel.text = @"正在下載圖片";
        [textLabel sizeToFit];
        [self.view addSubview:textLabel];
        self.textLabel = textLabel;
        [self group];
        NSLog(@"在下載圖片的時候,主線程貌似還可以干點什么");
    }
    
    
    - (void)group
    {
        UIImageView *imageView = [[UIImageView alloc] init];
        [self.view addSubview:imageView];
        
        dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t queue = dispatch_queue_create("cn.gcd-group.www", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_group_async(group, queue, ^{
            NSLog(@"正在下載第一張圖片");
            NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://images2015.cnblogs.com/blog/471463/201509/471463-20150912213125372-589808688.png"]];
            NSLog(@"第一張圖片下載完畢");
            self.imageOne = [UIImage imageWithData:data];
        });
        
        dispatch_group_async(group, queue, ^{
            NSLog(@"正在下載第二張圖片");
            NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://images2015.cnblogs.com/blog/471463/201509/471463-20150912212457684-585830854.png"]];
            NSLog(@"第二張圖片下載完畢");
            self.imageTwo = [UIImage imageWithData:data];
        });
        
        dispatch_group_notify(group, queue, ^{
            UIGraphicsBeginImageContext(CGSizeMake(300, 400));
            
            [self.imageOne drawInRect:CGRectMake(0, 0, 150, 400)];
            [self.imageTwo drawInRect:CGRectMake(150, 0, 150, 400)];
            
            UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
            
            dispatch_async(dispatch_get_main_queue(), ^{
                UIImageView *imageView = [[UIImageView alloc] initWithImage:newImage];
                [self.view addSubview:imageView];
                self.textLabel.text = @"圖片合並完畢";
            });
        });
    }
    @end
    

     

  2. dispatch_barrier_async
    在訪問數據庫或者文件的時候,我們可以使用Serial Dispatch Queue可避免數據競爭問題,代碼如下所示:
    先看看,如果我們在平常編碼中,如果要保證某個屬性可以線程安全的讀寫,如何寫的:
    #import <Foundation/Foundation.h>
    
    @interface ZYPerson : NSObject
    @property (nonatomic, copy) NSString *name;
    @end
    
    
    #import "ZYPerson.h"
    
    static NSString *_name;
    @implementation ZYPerson
    - (void)setName:(NSString *)name
    {
        @synchronized(self) {
            _name = [name copy];
        }
    }
    
    - (NSString *)name
    {
        @synchronized(self) {
            return _name;
        }
    }
    @end
    

    這是我在剛學iOS開發,剛涉及並發中的數據競爭時,書本上提到的一種解決方案。如果有多個線程要執行同一份代碼,那么有時候可能會出現問題,這種情況下,通常要使用鎖來實現某種同步機制。iOS提供了一種加鎖的方式,就是采用內置的synchronization block,也就是上面代碼所寫的。

    這種寫法會根據給定的對象,自動創建一個鎖,並等待塊中的代碼執行完畢。執行到這段代碼結尾處,鎖也就釋放了。在上面的例子中,同步行為所針對的對象是self。這么寫通常沒錯,但是@synchronized(self)會大大降低代碼效率,甚至很多時候,還可以被人感覺到效率明顯下降了,因為共用同一個鎖的那些同步塊,都必須按順序執行。若在self對象上頻繁加鎖,那么程序可能就要等另一段與此無關的代碼執行完畢,才可以繼續執行當前代碼,這樣做是很沒必要的。

    @synchronized(self)會大大降低代碼效率,因為所有的同步塊(  @synchronized(self)  )都會彼此搶奪同一個鎖。要是有多個屬性這么寫,每個屬性的同步塊(  @synchronized(self)  )都要等其他所有的同步塊執行完畢之后才能執行,這並不是我們想要的結果,我們只想要每個屬性各自獨立的同步。

    還有,不得不說,按上面這么做,雖然可以在一定程度上提供“線程安全”,但卻無法保證訪問該對象時是絕對線程安全的。事實上,上面的寫法,就是atomic,也就是原子性屬性xcode自動生成的代碼,這種方法,在訪問屬性時,必定可以從中得到有效值,然而如果在一個線程上多次調用getter方法,每次得到的結果卻未必相同,在兩次讀操作之間,其他線程可能會寫入新的屬性值。

    其實使用GCD可以簡單高效的代替同步塊或者鎖對象,可以使用,串行同步隊列,將讀操作以及寫操作都安排在同一個隊列里,即可保證數據同步,代碼如下:

    #import <Foundation/Foundation.h>
    
    @interface ZYPerson : NSObject
    @property (nonatomic, copy) NSString *name;
    @end
    
    
    #import "ZYPerson.h"
    
    @interface ZYPerson ()
    @end
    
    static NSString *_name;
    static dispatch_queue_t _queue;
    @implementation ZYPerson
    - (instancetype)init
    {
        if (self = [super init]) {
           _queue = dispatch_queue_create("com.person.syncQueue", DISPATCH_QUEUE_SERIAL);
        }
        return self;
    }
    
    - (void)setName:(NSString *)name
    {
        dispatch_sync(_queue, ^{
            _name = [name copy];
        });
    }
    
    - (NSString *)name
    {
        __block NSString *tempName;
        dispatch_sync(_queue, ^{
            tempName = _name;
        });
        return tempName;
    }
    @end
    

    這樣寫的思路是:把寫操作與讀操作都安排在同一個同步串行隊列里面執行,這樣的話,所有針對屬性的訪問操作就都同步了。
    這種方法的確已經足夠好了,但還不是最優的,它只可以實現單讀、單寫。整體來看,我們最終要解決的問題是,在寫的過程中不能被讀,以免數據不對,但是讀與讀之間並沒有任何的沖突!

    多個getter方法(也就是讀取)是可以並發執行的,而getter(讀)與setter(寫)方法是不能並發執行的,利用這個特點,還能寫出更快的代碼來,這次注意,不用串行隊列,而改用並行隊列:

    #import <Foundation/Foundation.h>
    
    @interface ZYPerson : NSObject
    @property (nonatomic, copy) NSString *name;
    @end
    
    
    #import "ZYPerson.h"
    
    @interface ZYPerson ()
    @end
    
    static NSString *_name;
    static dispatch_queue_t _concurrentQueue;
    @implementation ZYPerson
    - (instancetype)init
    {
        if (self = [super init]) {
           _concurrentQueue = dispatch_queue_create("com.person.syncQueue", DISPATCH_QUEUE_CONCURRENT);
        }
        return self;
    }
    - (void)setName:(NSString *)name
    {
        dispatch_barrier_async(_concurrentQueue, ^{
            _name = [name copy];
        });
    }
    - (NSString *)name
    {
        __block NSString *tempName;
        dispatch_sync(_concurrentQueue, ^{
            tempName = _name;
        });
        return tempName;
    }
    @end
    

     這樣優化,測試一下性能,可以發現這種做法肯定比使用串行隊列要快。

    在這個代碼中,我用了點新的東西,dispatch_barrier_async,可以翻譯成柵欄(barrier),它可以往隊列里面發送任務(塊,也就是block),這個任務有柵欄(barrier)的作用。

    在隊列中,barrier塊必須單獨執行,不能與其他block並行。這只對並發隊列有意義,並發隊列如果發現接下來要執行的block是個barrier block,那么就一直要等到當前所有並發的block都執行完畢,才會單獨執行這個barrier block代碼塊,等到這個barrier block執行完畢,再繼續正常處理其他並發block。在上面的代碼中,setter方法中使用了barrier block以后,對象的讀取操作依然是可以並發執行的,但是寫入操作就必須單獨執行了。


  3. 附錄(也算是追加)

    也許會對並行、串行、異步、同步有所理解不深。
    並行:就是隊列里面的任務(代碼塊,block)不是一個個執行,而是並發執行,也就是可以同時執行的意思
    串行:隊列里面的任務一個接着一個執行,要等前一個任務結束,下一個任務才可以執行
    異步:具有新開線程的能力
    同步:不具有新開線程的能力,只能在當前線程執行任務

    那么,如果他們相互串起來,會怎么樣呢?
    並行+異步:就是真正的並發,新開有有多個線程處理任務,任務並發執行(不按順序執行)
    串行+異步:新開一個線程,任務一個接一個執行,上一個任務處理完畢,下一個任務才可以被執行
    並行+同步:不新開線程,任務一個接一個執行
    串行+同步:不新開線程,任務一個接一個執行

    如此,並行+異步,串行+異步倒是很好辨別,他們有各自的特點,那么並行+同步,串行+同步,貌似效果是一樣的,好像用哪種都一樣??!!
    不,這其中區別很大(不得不說一句,我所看多線程書籍,基本沒講到這點的,不知道是不是他們都認為這是基礎,沒必要說明)。

    這其中的區別,我覺得代碼體現更直觀,直接上代碼吧:
    這一份是並行+同步,我在外面並發添加代碼塊:
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_sync(dispatch_get_main_queue(), ^{
                NSLog(@"11  %@",[NSThread currentThread]);
            });
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_sync(dispatch_get_main_queue(), ^{
                NSLog(@"22  %@",[NSThread currentThread]);
            });
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_sync(dispatch_get_main_queue(), ^{
                NSLog(@"33  %@",[NSThread currentThread]);
            });
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_sync(dispatch_get_main_queue(), ^{
                NSLog(@"44  %@",[NSThread currentThread]);
            });
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_sync(dispatch_get_main_queue(), ^{
                NSLog(@"55  %@",[NSThread currentThread]);
            });
        });
    

    看看打印:


    上面這個代碼可能描述的並不直觀,畢竟我采用的是主隊列,新增代碼:
        dispatch_queue_t queue = dispatch_queue_create("com.hao123.www", DISPATCH_QUEUE_SERIAL);
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_sync(queue, ^{
                NSLog(@"11  %@",[NSThread currentThread]);
            });
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_sync(queue, ^{
                NSLog(@"22  %@",[NSThread currentThread]);
            });
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_sync(queue, ^{
                NSLog(@"33  %@",[NSThread currentThread]);
            });
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_sync(queue, ^{
                NSLog(@"44  %@",[NSThread currentThread]);
            });
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_sync(queue, ^{
                NSLog(@"55  %@",[NSThread currentThread]);
            });
        });
    

      可以發現,結論是一樣的,里面的任務依次執行:





    在一個線程里面,任務一個接一個的執行。

    再看看,並行+同步,並發調用:
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                NSLog(@"11  %@",[NSThread currentThread]);
            });
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                NSLog(@"22  %@",[NSThread currentThread]);
            });
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                NSLog(@"33  %@",[NSThread currentThread]);
            });
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                NSLog(@"44  %@",[NSThread currentThread]);
            });
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                NSLog(@"55  %@",[NSThread currentThread]);
            });
        });
    

    打印:

    新開了多個線程,任務並發執行。

    至此,難道可以說,我之前的所說的
    並行+同步:不新開線程,任務一個接一個執行
    串行+同步:不新開線程,任務一個接一個執行
    是錯的?不,並沒有錯,只是這種說法,是將任務添加到這種模式的內部。





免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM