如何使用dispatch_queue_set_specific()和dispatch_get_specific()

7

我很难找到如何使用这些函数的好例子。

static void * kQueue1Key = "key1";
static void * kQueue2Key = "key2";

dispatch_queue_t queue1 = dispatch_queue_create("com.company.queue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.company.queue2", DISPATCH_QUEUE_SERIAL);

dispatch_queue_set_specific(queue1, kQueue1Key, (void *)kQueue1Key, NULL);
dispatch_queue_set_specific(queue2, kQueue2Key, (void *)kQueue2Key, NULL);

dispatch_sync(queue1, ^{
    if(dispatch_get_specific(kQueue1Key))
    {
        NSLog(@"I'm expecting this line to run (A)");

        dispatch_sync(queue2, ^{

            NSLog(@"I'm expecting this line to run (B)");

            if(dispatch_get_specific(kQueue2Key))
            {
                if(dispatch_get_specific(kQueue1Key))
                {
                    NSLog(@"I'm expecting this line to run (C)");
                }
                else
                {
                    [NSException raise:NSInternalInconsistencyException format:@"Should not end up here (C)"];
                }
            }
            else
            {
                [NSException raise:NSInternalInconsistencyException format:@"Should not end up here (B)"];
            }
        });
    }
    else
    {
        [NSException raise:NSInternalInconsistencyException format:@"Should not end up here (A)"];
    }
});

结果

I'm expecting this line to run (A)
I'm expecting this line to run (B)
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Should not end up here (C)'

这是预期的行为吗?如果我将dispatch_sync分派到queue1,因为我不在该队列上,我会死锁。我错过了什么?


1
你是否正在尝试使用dispatch_sync实现递归锁?因为这条路最终会带来悲伤(简而言之:由于可能存在非默认队列的定向性,即使您假设您的队列没有被巧妙地定向,涉及到的辅助问题也会夺走大部分(甚至全部)标准递归锁的性能优势。)另请参见:https://dev59.com/lHjZa4cB1Zd3GeqPbDWy#19495517 - ipmcc
1
@ipmcc 这确实让我受益匪浅! - hfossli
我曾认为dispatch_get_specific是避免互斥锁的“新答案”。如果不是,那它还能提供什么? - hfossli
1
避免使用互斥锁?怎么做呢?dispatch_queue_set/get_specific只是提供了一个悬挂在队列上的任意关联存储。dispatch_get_specific允许您在没有对当前队列的引用的情况下读取该存储。如果您有一个针对Qa的Qb,并在Qa上设置了特定值,然后从在Qb上执行的块中获取特定值,它将遍历目标链并返回来自Qa的值,其中包含一些附加功能。 - ipmcc
哈哈。听起来非常抽象。那怎么可能有用呢?这是GCD中-[NSThread threadDictionary]的优秀替代品吗? - hfossli
1
我不认为它一定是“更优越的”,但两种机制之间确实存在相似之处。最常提到dispatch_queue_set/get_specific的用途是,它是推荐使用的机制,可以替代现在已被弃用的dispatch_get_current_queue函数。 - ipmcc
2个回答

13

哦,我突然想到了你为什么会得到你所得到的东西。注释如下:

dispatch_sync(queue1, ^{

当您到达此时刻,“current queue”为queue1

    if(dispatch_get_specific(kQueue1Key))
你正在向当前队列查询在kQueue1Key上的值,此前你已经设置过该值,因此它会将该值返回给你。
    {
        NSLog(@"I'm expecting this line to run (A)");

        dispatch_sync(queue2, ^{

当您到达此时刻时,"current queue"现在是queue2

            NSLog(@"I'm expecting this line to run (B)");

            if(dispatch_get_specific(kQueue2Key))

您正在查询当前队列对于 kQueue2Key 的值,这个值是之前由您设置的,所以它会将其返回给您。

            {

                if(dispatch_get_specific(kQueue1Key))

您现在正在询问当前队列 kQueue1Key 的值。由于当前队列是 queue2,并且您从未在 queue2 上设置过具有 kQueue1Key 的值,因此您会收到 NULL 返回。

                {
                    NSLog(@"I'm expecting this line to run (C)");
                }
                else
                {
                    [NSException raise:NSInternalInconsistencyException format:@"Should not end up here (C)"];
                }

这里的误解在于dispatch_get_specific不遍历嵌套队列的堆栈,而是遍历目标队列系谱。例如,如果你做了这个

static void * kQueue1Key = (void*)"key1";
static void * kQueue2Key = (void*)"key2";

dispatch_queue_t queue1 = dispatch_queue_create("com.company.queue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.company.queue2", DISPATCH_QUEUE_SERIAL);

dispatch_queue_set_specific(queue1, kQueue1Key, (void *)kQueue1Key, NULL);
dispatch_queue_set_specific(queue2, kQueue2Key, (void *)kQueue2Key, NULL);

// Set Queue2 to target Queue1
dispatch_set_target_queue(queue2, queue1);

dispatch_sync(queue2, ^{

    if(dispatch_get_specific(kQueue1Key))
    {
        NSLog(@"I'm expecting this line to run (A)");
    }
    else
    {
        [NSException raise:NSInternalInconsistencyException format:@"Should not end up here (C)"];
    }

    if(dispatch_get_specific(kQueue2Key))
    {
        NSLog(@"I'm expecting this line to run (B)");
    }
    else
    {
        [NSException raise:NSInternalInconsistencyException format:@"Should not end up here (C)"];
    }
});

目标关系是被遍历的关系,而不是堆栈关系。如果有一些东西能够遍历堆栈关系,那就太好了,但我不知道有什么(除非你自己去实现)。


1
这真的很有帮助!现在我更好地理解了API。而且没错:你对我的假设/误解是正确的。 - hfossli

3

如我在评论中提到的,使用dispatch_sync进行递归锁定在一般情况下是不可能的,因为存在非默认队列目标的可能性。就默认队列目标而言,以下是一种可能的方法:

#import <unordered_set>
#import <pthread.h>

static dispatch_once_t recursiveLockWithDispatchQueueTLSKeyOnceToken;
static pthread_key_t recursiveLockWithDispatchQueueTLSKey;
typedef std::unordered_multiset<const void*> RecursiveLockQueueBag;

static void freeRecursiveLockWithDispatchQueueTLSValue(void* tlsValue)
{
    RecursiveLockQueueBag* ms = reinterpret_cast<RecursiveLockQueueBag*>(tlsValue);
    if (ms) delete ms;
}

static inline BOOL queueStackCheck(dispatch_queue_t q, BOOL checkAndPushNotPop) // If yes, check and push if not on. If no, pop.
{
    dispatch_once(&recursiveLockWithDispatchQueueTLSKeyOnceToken, ^{
        pthread_key_create(&recursiveLockWithDispatchQueueTLSKey, freeRecursiveLockWithDispatchQueueTLSValue);
    });

    RecursiveLockQueueBag* ms = reinterpret_cast<RecursiveLockQueueBag*>(pthread_getspecific(recursiveLockWithDispatchQueueTLSKey));
    if (!ms)
    {
        ms = new RecursiveLockQueueBag();
        pthread_setspecific(recursiveLockWithDispatchQueueTLSKey, reinterpret_cast<const void*>(ms));
    }

    const void* const vpq = reinterpret_cast<const void*>((__bridge const void*)q);

    BOOL alreadyOn = NO;

    if (checkAndPushNotPop)
    {
        alreadyOn = (ms->count(vpq) > 0);
        if (!alreadyOn)
        {
            ms->insert(vpq);
        }
    }
    else
    {
        ms->erase(vpq);
    }
    return alreadyOn;
}

void dispatch_recursive_sync(dispatch_queue_t queue, dispatch_block_t block)
{
    if (queueStackCheck(queue, YES))
    {
        block();
    }
    else
    {
        @try
        {
            dispatch_sync(queue, block);
        }
        @finally
        {
            queueStackCheck(queue, NO);
        }
    }
}

@implementation MyAppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    dispatch_queue_t a = dispatch_queue_create("a", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t b = dispatch_queue_create("b", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t c = dispatch_queue_create("c", DISPATCH_QUEUE_SERIAL);
    //dispatch_set_target_queue(a, c);

    dispatch_recursive_sync(a, ^{
        dispatch_recursive_sync(b, ^{
            dispatch_recursive_sync(c, ^{
                dispatch_recursive_sync(a, ^{
                    dispatch_recursive_sync(b, ^{
                        dispatch_recursive_sync(c, ^{
                            dispatch_recursive_sync(a, ^{
                                NSLog(@"got there");
                            });
                        });
                    });
                });
            });
        });
    });


}

@end

这是我在几分钟内能想到的最低开销实现方式。我使用C++以避免消息发送开销。它要求队列的所有使用都使用此函数。当存在保护对象内部状态的私有队列(即队列是私有的,因此保证不会被重新定向,并且您可以轻松地确保队列的所有消费者使用dispatch_recursive_sync时,这可能很有用。


感谢您提供答案,即使这不是我想要的。 - hfossli
谢谢您。我将其打包在一个 SPM 和 Carthage 兼容的项目中,链接在这里:https://github.com/happn-tech/RecursiveSyncDispatch - Frizlab
这里还有一个直接的ObjectiveC++实现(未打包;仅为测试):https://gitlab.com/frizlab-demo-projects/test_recursive_dispatch - Frizlab

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接