wm.p1us
wm.p1us

Reputation: 2059

DispatchQueue barrier issue

trying to make thread safe array but it works not as I expected

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

public class SafeArray<Element> {
    private var array = [Element]()
    private let queue = DispatchQueue(label: "queueBarrier", attributes: .concurrent)

    public func append(element: Element) {
        queue.async(flags: .barrier) {
            self.array.append(element)
        }
    }

    public var elements: [Element] {
        var result = [Element]()
        queue.sync {
            result = self.array
        }
        return result
    }

    public var last: Element? {
        var result: Element?
        queue.sync {
            result = self.array.last
        }
        return result
    }
}




var safeArray = SafeArray<Int>()
var array = Array<Int>()

DispatchQueue.concurrentPerform(iterations: 10) { (int) in
    let last = array.last ?? 0
    array.append(last + 1)
    print("array = [..\(last)]")
}

print(array)

DispatchQueue.concurrentPerform(iterations: 10) { (int) in
    let last = safeArray.last ?? 0
    safeArray.append(element: last + 1)
       print("safeArray = [..\(last)]")
}

print(safeArray.elements)

output of my code

I expected that array should have some mess but safeArray should have numbers from 0 to 9.

I understand that array have 3 values but safeArray has 10 values as expected. But why this values not from 0 to 9?

Thank you!

Upvotes: 3

Views: 4629

Answers (4)

timbre timbre
timbre timbre

Reputation: 13980

Remember that while barrier makes operation itself thread-safe, the order between 2 operations is undefined. So if you want to depend on value of self.array.last, you have to get its value only after you were able to get through the barrier. I.e. you can have a function like:

public func appendAfterOperationOnLast(_ operation: @escaping (Element?) -> Element) {
        queue.async(flags: .barrier) {
            let last = self.array.last // <- get the value here
            let element = operation(last)
            self.array.append(element)
        }
    }

And then

DispatchQueue.concurrentPerform(iterations: 10) { i in
    safeArray.appendAfterOperationOnLast { last in
        return (last ?? 0) + 1
    }
}

print(safeArray.elements)

prints the expected result:

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Upvotes: 0

vikas kumar jangir
vikas kumar jangir

Reputation: 1

I have created a thread-safe NSMutableArray and its working as expected.

    //
//  GCDTSNSMutableArray.m
//  GCD
//
//  Created by Vikas Kumar Jangir on 07/05/19.
//  
//

#import "GCDTSNSMutableArray.h"

@interface GCDTSNSMutableArray()
@property (nonatomic,strong) NSMutableArray *internalArray;
@property (nonatomic) dispatch_queue_t queue;
@property (nonatomic, strong) NSString *queueName;
@end


@implementation GCDTSNSMutableArray

- (instancetype)init {
    self = [super init];
    if (self) {
        self.internalArray = [NSMutableArray new];
        //Make unique queue for every new insatance.
        self.queueName = [NSString stringWithFormat:@"GCDTSNSMutableArray_%@",[GCDCommonUtil generateUUID]];
        self.queue = dispatch_queue_create([self.queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

#pragma mark -  Add operations

- (void)addObject:(id)object {
    // Check for valid input object
    if (object == nil) {
        NSLog(@"Object must be nonnull");
        return;
    }

    // Check for valid input index
    dispatch_sync(self.queue, ^{
        [self.internalArray addObject:object];
    });
}

- (void)insertObject:(id)object atIndex:(NSUInteger)index {

    // Check for valid input object
    if (object == nil) {
        NSLog(@"Object must be nonnull");
        return;
    }

    // Check for valid input index
    NSUInteger numberOfElements = [self count];
    if (index > numberOfElements) {
        NSLog(@"Index %lu is out of range [0..%lu]",(unsigned long)index,(unsigned long)numberOfElements);
        return;
    }

    dispatch_sync(self.queue, ^{
        [self.internalArray insertObject:object atIndex:index];
    });
}

- (void)addObjectsFromArray:(NSArray *)array {
    // Valid input array
    if (array == nil) {
        NSLog(@"Array must be nonnull");
        return;
    }

    if ([array count] == 0) {
        NSLog(@"Array must be not empty");
        return;
    }

    // Add objects from array
    dispatch_sync(self.queue, ^{
        [self.internalArray addObjectsFromArray:array];
    });
}



#pragma mark - Remove Operation

- (void)removeObject:(NSObject *)object {
    // Valid input object
    if (object == nil) {
        NSLog(@"Object must be nonnull");
        return;
    }

    // Remove object from array
    dispatch_sync(self.queue, ^{
        [self.internalArray removeObject:object];
    });
}

- (void)removeObjectAtIndex:(NSUInteger)index {
    // Valid input index
    NSUInteger numberOfElements = [self count];
    if (index >= numberOfElements) {
        NSLog(@"Index is out of range");
        return;
    }

    // Remove object at index from array
    dispatch_sync(self.queue, ^{
        [self.internalArray removeObjectAtIndex:index];
    });
}

- (void)removeLastObject {
    dispatch_sync(self.queue, ^{
        [self.internalArray removeLastObject];
    });
}


- (void)removeAllObjects {
    // Check nonempty array
    NSUInteger numberOfElements = [self count];
    if (numberOfElements == 0) {
        NSLog(@"Array is empty");
        return;
    }

    // Remove all objects from array
    dispatch_sync(self.queue, ^{
        [self.internalArray removeAllObjects];
    });
}


#pragma mark - Count,Search,Copy

-(NSUInteger)count {
    __block NSUInteger count = 0;
    dispatch_sync(self.queue, ^{
        count = [self.internalArray count];
    });
    return count;
}

- (id)copy {
    __block id returnArray;
    dispatch_sync(self.queue, ^{
        returnArray = [self.internalArray copy];
    });

    return returnArray;
}

- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject {
    dispatch_sync(self.queue, ^{
        [self.internalArray replaceObjectAtIndex:index
                                      withObject:anObject];
    });
}

- (id)objectAtIndex:(NSUInteger)index {
    // Valid input index
    NSUInteger numberOfElements = [self count];
    if (index >= numberOfElements) {
        NSLog(@"Index %lu is out of range [0..%lu]",(unsigned long)index,(unsigned long)numberOfElements);
        return nil;
    }

    // Return object at index in array
    id __block object;
    dispatch_sync(self.queue, ^{
        object = [self.internalArray objectAtIndex:index];
    });
    return object;
}

- (NSUInteger)indexOfObject: (NSObject *)object {
    NSUInteger __block result;
    dispatch_sync(self.queue, ^{
        result = [self.internalArray indexOfObject:object];
    });
    return result;
}

- (BOOL)containsObject: (id)object {
    BOOL __block result;
    dispatch_sync(self.queue, ^{
        result = [self.internalArray containsObject:object];
    });
    return result;
}

- (NSArray *)toNSArray {
    NSArray __block *array;
    dispatch_sync(self.queue, ^{
        array = [[NSArray alloc] initWithArray:self.internalArray];
    });
    return array;
}

- (void)enumerateObjectsUsingBlockInSync:(BOOL)sync withBlock:(__attribute__((noescape)) void (^)(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop))block {

    if (!sync) {
        [self.internalArray enumerateObjectsUsingBlock:block];
    } else {
        dispatch_sync(self.queue, ^{
            [self.internalArray enumerateObjectsUsingBlock:block];
        });
    }
}


- (void)executeOnSynchWithCompletionBlock:(GCDThreadSafeNSMutableArrayCompletionBlock)compBlock {
    dispatch_sync(self.queue, compBlock);
}

@end

Upvotes: -2

Seivan
Seivan

Reputation: 689

The way barrier works is by making sure the DispatchWorkItem (the block for append(:_)) is going wait until all other DispatchWorkItems are done before executing its perform (the code inside the block). Hence, a barrier.

If you look closely, you got one (DispatchWorkItem) inside your last call. Since you're calling last the first thing you do concurrently in DispatchQueue.concurrentPerform, you'll have a stack of DispatchWorkItems waiting in the queue.

This means all your append(_:) calls will be waiting since they're flagged as barrier and your last calls will all execute first, thus getting a lot of zeroes until all the DispatchWorkItems for last are done before squeezing in two appends(_:)

The way barrier works within a concurrent queue, is that it will actually wait until all pending DispatchWorkItems are done, before starting, and nothing else will start concurrently alongside it until it's done. It sorta "disables" the concurrent nature of the queue temporarily, unless I'm mistaken.

I'm generally hesitant to introduce locks or semaphore as suggested by others here and they can cause more issues unless you first understand how GCD works.

It looks like you're trying to solve two things, first having an append(_:) that works concurrently and mutating an array in an parallel operation that depends on its current state. Try to break down what you're trying to solve first so someone can give you a better answer.

Upvotes: 8

MjZac
MjZac

Reputation: 3526

Why not use DispatchSemaphore?

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

public class SafeArray<Element> {
    private var array = [Element]()
    private let semaphore = DispatchSemaphore(value: 1)
    private var lastEl: Element?

    public func append(element: Element) {

            self.array.append(element)
    }

    public var elements: [Element] {
        var result = [Element]()
        result = self.array
        return result
    }

    public var last: Element? {
        self.semaphore.wait()
        lastEl = self.array.last
        self.semaphore.signal()
        return lastEl
    }
}




var safeArray = SafeArray<Int>()
var array = Array<Int>()

DispatchQueue.concurrentPerform(iterations: 10) { (int) in
    let last = array.last ?? 0
    array.append(last + 1)
    print("array = [..\(last)]")
}

print(array)

DispatchQueue.concurrentPerform(iterations: 10) { (int) in
    let last = safeArray.last ?? 0
    safeArray.append(element: last + 1)
    print("safeArray = [..\(last)]")
}

print(safeArray.elements)

Upvotes: 0

Related Questions