在我之前的理解中,迭代器不就是指scan迭代器吗,但是后来又看到一些地方说字典迭代器,安全迭代器,,,再进一步了解之后,对其更加困惑了,所以进行了深入地学习,看明白了一些地方,但还是有一些疑惑,在此记下,望大家一起探讨。
在进行源码的分析之前,首先明确我们的几个问题:
scan和迭代器是两个不同的概念吗??
- scan迭代器和字典迭代器是两个不同的概念!!
(之所以会把scan遍历操作时字典的rehash 和 字典安全或非安全迭代器对rehash的影响混淆起来,是因为没认识到scan和迭代器是两个不同的概念) - 平时说的scan迭代器是使用dictScan() 函数迭代给定字典中的元素;
那字典迭代器的作用是什么??
安全迭代器和非安全迭代器的区别和用途又分别是啥??
下面结合源码来逐步的探索一下:
一、字典迭代器是啥
1.1 字典迭代器 dictIterator 的定义
首先来看一下字典迭代器的函数定义:
/*
* 字典迭代器(字典遍历器)
*
* 如果 safe 属性的值为 1 ,那么在迭代进行的过程中,
* 程序仍然可以执行 dictAdd 、 dictFind 和其他函数,对字典进行修改。
*
* 如果 safe 不为 1 ,那么程序只会调用 dictNext 对字典进行迭代,
* 而不对字典进行修改。
*/
typedef struct dictIterator {
// 被迭代的字典
dict *d;
// table :正在被迭代的哈希表号码,值可以是 0 或 1 。
// index :迭代器当前所指向的哈希表索引位置。
// safe :标识这个迭代器是否安全
int table, index, safe;
// entry :当前迭代到的节点的指针
// nextEntry :当前迭代节点的下一个节点
// 因为在安全迭代器运作时, entry 所指向的节点可能会被修改,
// 所以需要一个额外的指针来保存下一节点的位置,
// 从而防止指针丢失
dictEntry *entry, *nextEntry;
//指纹,指纹是代表字典在给定时间下的状态的一个64位的数,它通过字典的几个属性的异或可以得到。当我们初始化一个不安全的迭 代器时,就会得到字典的指纹,当迭代器释放时,我们会再次检查指纹。如果两次的指纹不一致,那么意味着不安全迭代器的调用者在迭 代过程中对字典进行了非法的调用,报错。
long long fingerprint; /* unsafe iterator fingerprint for misuse detection */
} dictIterator;
从上面的代码和注释可以看出来,字典迭代器分为安全迭代器和非安全迭代器。
它们的区别通过safe标志来区别:
- 如果 safe 属性的值为 1 ,也就是安全迭代器,那么在迭代进行的过程中,程序仍然可以执行 dictAdd 、 dictFind 和其他函数,对字典进行修改。
- 如果 safe 不为 1 ,也就是非安全迭代器,那么程序只会调用 dictNext 对字典进行迭代,而不对字典进行修改。
看完字典迭代器的定义之后,我们大概对字典迭代器的结构有了一个初步的认识,接下来看一下安全迭代器和非安全迭代器的函数定义,方便之后的分析。
1.2 非安全迭代器的定义
* 创建并返回给定字典的不安全迭代器
* T = O(1)
dictIterator *dictGetIterator(dict *d)
{
dictIterator *iter = zmalloc(sizeof(*iter));
iter->d = d;
iter->table = 0;
iter->index = -1;
iter->safe = 0;
iter->entry = NULL;
iter->nextEntry = NULL;
return iter;
}
注意,这里的 safe 位默认为0,就表示其为非安全迭代器。
1.3 安全迭代器的定义
* 创建并返回给定节点的安全迭代器
* T = O(1)
dictIterator *dictGetSafeIterator(dict *d) {
dictIterator *i = dictGetIterator(d);
// 设置安全迭代器标识
i->safe = 1;
return i;
}
安全迭代器的定义就比较简单,它与非安全迭代器的区别就是一个 safe 位的不同,所以就直接把非安全迭代器的定义拿了过来,然后改了一下 safe 标识位为1。
在知道了字典迭代器、安全迭代器、非安全迭代器的定义之后,我们就要来看一下它们在哪些地方使用,也就是 字典迭代器的作用是啥。
二、字典迭代器用在哪
在Redis源码src文件夹下的dict.c文件中,紧接着的安全迭代器函数定义的是 dictNext 函数,在这个函数里面,它使用到的参数正是我们之前看到的字典迭代器。代码如下:
* 返回迭代器指向的当前节点
* 字典迭代完毕时,返回 NULL
* T = O(1)
*/
dictEntry *dictNext(dictIterator *iter) //前面定义的字典迭代器!!!!
{
while (1) {
// 进入这个循环有两种可能:
// 1) 这是迭代器第一次运行
// 2) 当前索引链表中的节点已经迭代完(NULL 为链表的表尾)
if (iter->entry == NULL) {
// 指向被迭代的哈希表
dictht *ht = &iter->d->ht[iter->table];
// 初次迭代时执行
if (iter->index == -1 && iter->table == 0) {
// 如果是安全迭代器,那么更新安全迭代器计数器
if (iter->safe) !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
iter->d->iterators++; //如果是安全迭代器,就将字典的安全迭代器数量iterators++,导致不能对字典进行单步 rehash
// 如果是不安全迭代器,那么计算指纹
else
iter->fingerprint = dictFingerprint(iter->d);
}
// 更新索引
iter->index++;
// 如果迭代器的当前索引大于当前被迭代的哈希表的大小
// 那么说明这个哈希表已经迭代完毕
if (iter->index >= (signed) ht->size) {
// 如果正在 rehash 的话,那么说明 1 号哈希表也正在使用中
// 那么继续对 1 号哈希表进行迭代
if (dictIsRehashing(iter->d) && iter->table == 0) {
iter->table++;
iter->index = 0;
ht = &iter->d->ht[1];
// 如果没有 rehash ,那么说明迭代已经完成
} else {
break;
}
}
// 如果进行到这里,说明这个哈希表并未迭代完
// 更新节点指针,指向下个索引链表的表头节点
iter->entry = ht->table[iter->index];
} else {
// 执行到这里,说明程序正在迭代某个链表
// 将节点指针指向链表的下个节点
iter->entry = iter->nextEntry;
}
// 如果当前节点不为空,那么也记录下该节点的下个节点
// 因为安全迭代器有可能会将迭代器返回的当前节点删除????????????????????
if (iter->entry) {
/* We need to save the 'next' here, the iterator user
* may delete the entry we are returning. */
iter->nextEntry = iter->entry->next;
return iter->entry;
}
}
// 迭代完毕
return NULL;
}
从上面的函数定义我们了解到,这个dictNext函数的作用就是返回迭代器指向的当前节点!
那这个函数又是在什么地方使用的呢???这个问题我们之后会讨论。
针对这个函数,我们可以看到下面这段比较重要的代码,它就是跟我们正在讨论的安全迭代器有关的代码:
// 初次迭代时执行
if (iter->index == -1 && iter->table == 0) {
// 如果是安全迭代器,那么更新安全迭代器计数器
if (iter->safe) !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
iter->d->iterators++; //如果是安全迭代器,就将字典的安全迭代器数iterators++,导致不能对字典进行单步 rehash
// 如果是不安全迭代器,那么计算指纹
else
iter->fingerprint = dictFingerprint(iter->d);
}
从上面的分析可以看出,在我们调用 dictNext函数返回迭代器指向的当前节点时,如果我们使用的迭代器是安全迭代器,那么会将字典的安全迭代器数iterators++,导致不能对字典进行单步 rehash。
初步总结一下:
其实本质上就是安全的迭代器会给dict设置iterators++(dict里面的变量),这样字典的各种操作就不会执行rehash操作,因为如果在迭代的过程中执行了rehash,迭代索引就会错乱。
这样说来,字典迭代器的作用(dictEntry dictNext(dictIterator iter) )也是遍历字典,只不过遍历的对象是具体到dictEntry节点,而scan是根据槽位来返回槽位里的所有节点的数据。
虽然上面已经初步的总结出安全迭代器的作用:安全的迭代器会给dict设置iterators++(dict里面的变量),这样字典的各种操作就不会执行rehash操作。
那为什么设置 iterators++ 之后就不能对字典进行rehash操作呢??
这里看似只说明了安全迭代器的作用,并没有突出非安全迭代器的作用呀,它们的应用场景又分别是怎样的呢??
在下一篇的博文中,我会尝试探索一下这个问题,欢迎浏览交流~