五、DICT对象

1、散列表概述

2、PyDictObject

3、PyDictObject的创建与维护

4、PyDictObject 对象缓冲池

5、Hack PyDictObject

这篇篇幅较长,难点在字典搜索。


1、散列表概述

python中的dict并没有采用map中的红黑树结构做关联,而是使用效率更高的散列表。

散列表通过一个函数将键值映射为一个整数,再将整数作为索引值访问内存。用于映射的函数称为散列函数,映射后的值为散列值。散列会发生冲突,解决散列冲突的方法有很多,python使用的是开放定址法,当发生冲突再次探测可用位置,形成探测链,探测链如果要删掉中间一个元素,会使用伪删除处理,防止链断开搜索失败。


2、PyDictObject

后面将把关联容器中的一个(key, value)元素对称为一个entry或slot。一个entry定义:

[dictobject.h] 

typedef struct { 

    long me_hash;      /* cached hash code of me_key */ 

    PyObject *me_key; 

    PyObject *me_value; 

} PyDictEntry; 

me_hash域 存储me_key的散列值,entry分为三种状态:Unused态、Active态、Dummy态,切换如下:

PyDictObject实际是一堆entry的集合:

[dictobject.h] 

#define PyDict_MINSIZE 8 

typedef struct _dictobject PyDictObject; 

struct _dictobject { 

    PyObject_HEAD 

    int ma_fill;  /* # Active + # Dummy */ 

    int ma_used;  /* # Active */ 

    int ma_mask; 

    PyDictEntry *ma_table; 

    PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash); 

    PyDictEntry ma_smalltable[PyDict_MINSIZE]; 

}; 

ma_fill 维护处于Active态和Dummy态的entry数;

ma_used维护处于Active态的entry数;

ma_mask指PyDictObject中所有entry数;

ma_table域 指向PyDictObject中的entry,当其数量小于等于PyDict_MINSIZE(8)时,指向ma_smalltable,否者申请内存指向该内存;

ma_lookup后面说;


3、PyDictObject的创建与维护

 3.1.1、PyDictObject对象创建

[dictobject.c] 

typedef PyDictEntry dictentry; 

typedef PyDictObject dictobject; 
#define INIT_NONZERO_DICT_SLOTS(mp) do {                \ 

    (mp)->ma_table = (mp)->ma_smalltable;               \ 

    (mp)->ma_mask = PyDict_MINSIZE - ;             \ 

    } while() 
    memset((mp)->ma_smalltable, , sizeof((mp)->ma_smalltable));    \ 

    (mp)->ma_used = (mp)->ma_fill = ;              \ 

    INIT_NONZERO_DICT_SLOTS(mp);                    \ 

    } while() 
PyObject* PyDict_New(void) 

{ 

    register dictobject *mp; 

    if (dummy == NULL) { /* Auto-initialize dummy */ 

        dummy = PyString_FromString("<dummy key>"); 

        if (dummy == NULL) 

            return NULL; 

    } 

if (num_free_dicts) 

{ 

        …… //使用缓冲池 

} 

else 

{ 

        mp = PyObject_GC_New(dictobject, &PyDict_Type); 

        if (mp == NULL) 

            return NULL; 

        EMPTY_TO_MINSIZE(mp); 

    } 

    mp->ma_lookup = lookdict_string; 

    _PyObject_GC_TRACK(mp); 

    return (PyObject *)mp; 

} 

创建PyDictObject时,会先创建一个字符串对象dummy,用作指示标志,表面entry曾被使用,也用于探测序列;

num_free_dicts是dict的缓冲池,后面讲;

然后开始创建,将ma_smalltable、ma_used、ma_fill清0,然后ma_table指向ma_smalltable,设置ma_mash,最后将lookdict_string 赋予 ma_lookup。

 3.1.2、元素搜索

PyDictObject有两种搜索策略,lookdict和lookdict_string,lookdict_string是lookdict对PyStringObject的特化。其中lookdict_string:

[dictobject.c] 

static dictentry* lookdict_string(dictobject *mp, PyObject *key, register long hash) 

{ 

    register int i; 

    register unsigned int perturb; 

    register dictentry *freeslot; 

    register unsigned int mask = mp->ma_mask; 

    dictentry *ep0 = mp->ma_table; 

    register dictentry *ep; 

    if (!PyString_CheckExact(key)) { 

        mp->ma_lookup = lookdict; 

        return lookdict(mp, key, hash); 

} 

//[1] 

    i = hash & mask; 

    ep = &ep0[i]; 

//[2] 

//if NULL or interned 

    if (ep->me_key == NULL || ep->me_key == key) 

        return ep; 

//[3] 

    if (ep->me_key == dummy) 

        freeslot = ep; 

    else 

    { 

    //[4] 

        if (ep->me_hash == hash && _PyString_Eq(ep->me_key, key)) 

        { 

            return ep; 

        } 

        freeslot = NULL; 

    } 

    /* In the loop, me_key == dummy is by far (factor of 100s) the 

       least likely outcome, so test for that last. */ 

    for (perturb = hash; ; perturb >>= PERTURB_SHIFT) 

    { 

        i = (i << ) + i + perturb + ; 

        ep = &ep0[i & mask]; 

        if (ep->me_key == NULL) 

            return freeslot == NULL ? ep : freeslot; 

        if (ep->me_key == key 

            || (ep->me_hash == hash 

                && ep->me_key != dummy 

            && _PyString_Eq(ep->me_key, key))) 

            return ep; 

        if (ep->me_key == dummy && freeslot == NULL) 

            freeslot = ep; 

    } 

} 

其中关键步骤标注[1][2][3][4],后面讲。

lookdict_string是在key为PyStringObject的情况下使用,否则使用lookdict:

[dictobject.c] 

static dictentry* lookdict(dictobject *mp, PyObject *key, register long hash) 

{ 

    register int i; 

    register unsigned int perturb; 

    register dictentry *freeslot; 

    register unsigned int mask = mp->ma_mask; 

    dictentry *ep0 = mp->ma_table; 

    register dictentry *ep; 

    register int restore_error; 

    register int checked_error; 

    register int cmp; 

    PyObject *err_type, *err_value, *err_tb; 

    PyObject *startkey; 

    //[1] 

    i = hash & mask; 

    ep = &ep0[i]; 

    //[2] 

    if (ep->me_key == NULL || ep->me_key == key) 

        return ep; 

    //[3] 

    if (ep->me_key == dummy) 

        freeslot = ep; 

    else 

    { 

        //[4] 

        if (ep->me_hash == hash) 

        { 

            startkey = ep->me_key; 

            cmp = PyObject_RichCompareBool(startkey, key, Py_EQ); 

            if (cmp < ) 

                PyErr_Clear(); 

            if (ep0 == mp->ma_table && ep->me_key == startkey) 

            {
         //只有key相等才会返回已有位置,否者会寻找下一个位置
if (cmp > ) goto Done; } else {

        /* The compare did major nasty stuff to the
        * dict: start over.
        * XXX A clever adversary could prevent this
        * XXX from terminating.
        */

                ep = lookdict(mp, key, hash); 

                goto Done; 

            } 

        } 

        freeslot = NULL; 

    } 

    。。。。。。 

Done: 

    return ep; 

} 

由于PyDictObject中维护dict数量是有限的(ma_table的长度),而计算出的hash值可能超过此范围,故需要与ma_mask进行与操作获得下标,因此ma_mask 名字 不是 ma_size。

其中freeslot用来指向第一次搜索序列中的Dummy态entry,如果搜索失败返回freeslot指向的Dummy态entry,如果没有Dummy态entry,返回Unused态entry(都可指示搜索失败)。

下面是lookdict中进行第一次检查时需要注意的动作:

[1]:根据hash值获得entry的序号。

[2]:如果ep->me_key为NULL,且与key相同,搜索失败。

[3]:若当前entry处于Dummy态,设置freeslot。

[4]:检查当前Active的entry中的key与待查找的key是否相同,如果相同,则立即返回,搜索成功。

在[4]中,需要注意那个PyObject_RichCompareBool,它的函数原形为:

int PyObject_RichCompareBool(PyObject *v, PyObject *w, int op)

当(v op w)成立时,返回1;当(v op w)不成立时,返回0;如果在比较中发生错误,则返回-1。

在lookdict中,当第一次hash值获得的entry与待查找元素比较发现不一样时,会继续在探测序列上查找:

[dictobject.c] 

static dictentry* lookdict(dictobject *mp, PyObject *key, register long hash) 

{ 

    register int i; 

    register unsigned int perturb; 

    register dictentry *freeslot; 

    register unsigned int mask = mp->ma_mask; 

    dictentry *ep0 = mp->ma_table; 

    register dictentry *ep; 

    register int restore_error; 

    register int checked_error; 

    register int cmp; 

    PyObject *err_type, *err_value, *err_tb; 

    PyObject *startkey; 

    。。。。。。 

    for (perturb = hash; ; perturb >>= PERTURB_SHIFT) 

    { 

        //[5] 

        i = (i << ) + i + perturb + ; 

        ep = &ep0[i & mask]; 

        //[6] 

        if (ep->me_key == NULL) 

        { 

            if (freeslot != NULL) 

                ep = freeslot; 

            break; 

        } 

        if (ep->me_key == key)//[7] 

            break; 

        if (ep->me_hash == hash && ep->me_key != dummy) 

        { 

            startkey = ep->me_key; 

            cmp = PyObject_RichCompareBool(startkey, key, Py_EQ); 

            if (cmp < ) 

                PyErr_Clear(); 

            if (ep0 == mp->ma_table && ep->me_key == startkey) { 

                if (cmp > ) 

                    break; 

            } 

            else { 

                ep = lookdict(mp, key, hash); 

                break; 

            } 

        } 

        //[8] 

        else if (ep->me_key == dummy && freeslot == NULL) 

            freeslot = ep; 

    } 

Done: 

    return ep; 

} 

[5]:获得探测序列中的下一个待探测的entry。

[6]:ep到达一个Unused态entry,表明搜索结束。这是如果freeslot不为空,则返回freeslot所指entry。

[7]:entry与待查找的key匹配,搜索成功。

[8]:在探测序列中发现Dummy态entry,设置freeslot。

比较lookdict_string与lookdict可发现,lookdict_string是lookdict针对PyStringObject的简化版,而且效率要高很多。Python自身也大量使用PyDictObject对象,大都使用PyStringObject作为key,故lookdict_string对Python整理运行效率都有重要影响。

搜索部分内容比较多,代码比较长,有兴趣好好琢磨。lookdict_string相当于在hash值相同的探索链上找,调用一次可以找到;lookdict差不多,不过里面key对象不一定是PyStringObject,所以多了一些检查、判断函数,还多了一个递归找的逻辑(判断逻辑:只有key相等才返回已有位置,否者会寻找下一个位置)。

***标记一下有一点不太理解:

 if (ep0 == mp->ma_table && ep->me_key == startkey)

啥意思。。

***

 3.1.3、插入与删除

PyDictObject插入建立在搜索上:

[dictobject.c] 

static void 

insertdict(register dictobject *mp, PyObject *key, long hash, PyObject *value) 

{ 

    PyObject *old_value; 

    register dictentry *ep; 

ep = mp->ma_lookup(mp, key, hash); 

//[1] 

    if (ep->me_value != NULL) { 

        old_value = ep->me_value; 

        ep->me_value = value; 

        Py_DECREF(old_value); /* which **CAN** re-enter */ 

        Py_DECREF(key); 

} 

//[2] 

    else { 

        if (ep->me_key == NULL) 

            mp->ma_fill++; 

        else 

            Py_DECREF(ep->me_key); 

        ep->me_key = key; 

        ep->me_hash = hash; 

        ep->me_value = value; 

        mp->ma_used++; 

    } 

} 

搜索结果可能是Active态的entry,也可能是Dummy或Unused态的entry;对于前者只需替换me_value,对于后者要设置其他值:

[1] :搜索成功,返回处于Active的entry,直接替换me_value。

[2] :搜索失败,返回Unused或Dummy的entry,完整设置me_key,me_hash和me_value。

在调用insertdict前会调用PyDict_SetItem:

[dictobject.c] 

int PyDict_SetItem(register PyObject *op, PyObject *key, PyObject *value) 

{ 

    register dictobject *mp; 

    register long hash; 

    register int n_used; 

    mp = (dictobject *)op; 

    //计算hash值 

    if (PyString_CheckExact(key)) { 

        hash = ((PyStringObject *)key)->ob_shash; 

        if (hash == -) 

            hash = PyObject_Hash(key); 

    } 

    else { 

        hash = PyObject_Hash(key); 

        if (hash == -) 

            return -; 

    } 

    n_used = mp->ma_used; 

    Py_INCREF(value); 

    Py_INCREF(key); 

    insertdict(mp, key, hash, value); 

    if (!(mp->ma_used > n_used && mp->ma_fill* >= (mp->ma_mask+)*)) 

        return ; 

    return dictresize(mp, mp->ma_used*(mp->ma_used> ?  : )); 

} 

首先会获得key的hash值,在插入元素后会判断是否需要改变ma_table大小。判断条件为装载率大于2/3((mp->ma_fill)/(mp->ma_mask+1) >= 2/3)而且使用了Unused态的entry(mp->ma_used > n_used)。在改变table时可能是增加也可能是减少,新增大小为table中Active态的entry数的2或4倍(看数量是否超过50000)。

改变table大小则由dictresize负责:

[dictobject.c] 

static int dictresize(dictobject *mp, int minused) 

{ 

    int newsize; 

    dictentry *oldtable, *newtable, *ep; 

    int i; 

    int is_oldtable_malloced; 

    dictentry small_copy[PyDict_MINSIZE]; 

    //[1] 

    for(newsize = PyDict_MINSIZE; newsize <= minused && newsize > ; newsize <<= ) 

        ; 

    oldtable = mp->ma_table; 

    assert(oldtable != NULL); 

    is_oldtable_malloced = oldtable != mp->ma_smalltable; 

    //[2] 

    if (newsize == PyDict_MINSIZE) { 

        newtable = mp->ma_smalltable; 

        if (newtable == oldtable) { 

            if (mp->ma_fill == mp->ma_used) { 

                //没有任何Dummy态entry,直接返回 

                return ; 

            } 

            //将oldtable拷贝,进行备份 

            assert(mp->ma_fill > mp->ma_used); 

            memcpy(small_copy, oldtable, sizeof(small_copy)); 

            oldtable = small_copy; 

        } 

    } 

    else { 

        newtable = PyMem_NEW(dictentry, newsize); 

    } 

    //[3] 

    assert(newtable != oldtable); 

    mp->ma_table = newtable; 

    mp->ma_mask = newsize - ; 

    memset(newtable, , sizeof(dictentry) * newsize); 

    mp->ma_used = ; 

    i = mp->ma_fill; 

    mp->ma_fill = ; 

    //[4] 

    for (ep = oldtable; i > ; ep++) { 

        if (ep->me_value != NULL) { /* active entry */ 

            --i; 

            insertdict(mp, ep->me_key, ep->me_hash, ep->me_value); 

        } 

        else if (ep->me_key != NULL) {  /* dummy entry */ 

            --i; 

            assert(ep->me_key == dummy); 

            Py_DECREF(ep->me_key); 

        } 

    } 

    if (is_oldtable_malloced) 

        PyMem_DEL(oldtable); 

    return ; 

} 

[1] :dictresize首先会确定新的table的大小,很显然,这个大小一定要大于传入的参数minused,这也是在原来的table中处于Active态的entry的数量。dictresize从8开始,以指数方式增加大小,直到超过了minused为止。所以实际上新的table的大小在大多数情况下至少是原来table中Active态entry数量的4倍。

[2] :如果在[1]中获得的新的table大小为8,则不需要在堆上分配空间,直接使用ma_smalltable就可以了;否则,则需要在堆上分配空间。

[3] :对新的table进行初始化,并调整原来PyDictObject对象中用于维护table使用情况的变量。

[4] :对原来table中的非Unused态entry进行处理。对于Active态entry,显然需要将其插入到新的table中,这个动作由前面考察过的insertdict完成;而对于Dummy态的entry,则略过,不做任何处理,因为我们知道Dummy态entry存在的唯一理由就是为了不使搜索时的探测序列中断。现在所有Active态的entry都重新依次插入新的table中,它们会形成一条新的探测序列,不再需要这些Dummy态的entry了。

从PyDictObject中删除一个元素:

[dictobject.c] 

int PyDict_DelItem(PyObject *op, PyObject *key) 

{ 

    register dictobject *mp; 

    register long hash; 

    register dictentry *ep; 

    PyObject *old_value, *old_key; 

    //获得hash值 

    if (!PyString_CheckExact(key) || 

        (hash = ((PyStringObject *) key)->ob_shash) == -) { 

        hash = PyObject_Hash(key); 

        if (hash == -) 

            return -; 

    } 

    //搜索entry 

    mp = (dictobject *)op; 

    ep = (mp->ma_lookup)(mp, key, hash); 

    //删除entry所维护的元素 

    old_key = ep->me_key; 

    Py_INCREF(dummy); 

    ep->me_key = dummy; 

    old_value = ep->me_value; 

    ep->me_value = NULL; 

    mp->ma_used--; 

    Py_DECREF(old_value); 

    Py_DECREF(old_key); 

    return ; 

} 

先获取hash值,取到entry后将entry从Active态转为Dummy态,再调整相关变量。


4、PyDictObject 对象缓冲池

PyDictObject和PyListObject一样也使用缓冲池技术:

[dictobject.c] 

#define MAXFREEDICTS 80 

static PyDictObject *free_dicts[MAXFREEDICTS]; 

static int num_free_dicts = ; 

而且和PyListObject的缓冲池类似,在PyDictObject对象被销毁时才把内存加入缓冲池:

[dictobject.c] 

static void dict_dealloc(register dictobject *mp) 

{ 

    register dictentry *ep; 

    int fill = mp->ma_fill; 

    PyObject_GC_UnTrack(mp); 

Py_TRASHCAN_SAFE_BEGIN(mp) 

//调整dict中对象的引用计数 

    for (ep = mp->ma_table; fill > ; ep++) { 

        if (ep->me_key) { 

            --fill; 

            Py_DECREF(ep->me_key); 

            Py_XDECREF(ep->me_value); 

        } 

} 

//向系统归还从堆上申请的空间 

    if (mp->ma_table != mp->ma_smalltable) 

        PyMem_DEL(mp->ma_table); 

//将被销毁的PyDictObject对象放入缓冲池 

    if (num_free_dicts < MAXFREEDICTS && mp->ob_type == &PyDict_Type) 

        free_dicts[num_free_dicts++] = mp; 

    else 

        mp->ob_type->tp_free((PyObject *)mp); 

    Py_TRASHCAN_SAFE_END(mp) 

} 

缓冲池中只保留了PyDictObject对象,里面从堆上申请的table则会被销毁,归还系统。如果被销毁的PyDictObject对象只是用了固有的ma_smalltable,那只需调整ma_smalltable中对象的引用计数。

在创建PyDictObject对象时,缓冲池有则直接从缓冲池取:

[dictobject.c] 

PyObject* PyDict_New(void) 

{ 

register dictobject *mp; 

………… 

    if (num_free_dicts) { 

        mp = free_dicts[--num_free_dicts]; 

        _Py_NewReference((PyObject *)mp); 

        if (mp->ma_fill) { 

            EMPTY_TO_MINSIZE(mp); 

        } 

} 

………… 

} 

5、Hack PyDictObject

python内部大量使用PyDictObject,每个小小调用都会对insertdict频繁调用,故打印的话可用特征串,打印:

static void ShowDictObject(dictobject* dictObject) 

{ 

   dictentry* entry = dictObject->ma_table; 

   int count = dictObject->ma_mask+; 

   int i; 

   for(i = ; i < count; ++i) 

   { 

      PyObject* key = entry->me_key; 

      PyObject* value = entry->me_value; 

      if(key == NULL) 

      { 

         printf("NULL"); 

      } 

      else 

      { 

         (key->ob_type)->tp_print(key, stdout, ); 

      } 

      printf("\t"); 

      if(value == NULL) 

      { 

         printf("NULL"); 

      } 

      else 

      { 

         (key->ob_type)->tp_print(value, stdout, ); 

      } 

      printf("\n"); 

      ++entry; 

   } 

}

static void 

insertdict(register dictobject *mp, PyObject *key, long hash, PyObject *value) 

{ 

    …… 

   { 

      dictentry *p; 

      long strHash; 

      PyObject* str = PyString_FromString("Python_Robert"); 

      strHash = PyObject_Hash(str); 

      p = mp->ma_lookup(mp, str, strHash); 

      if(p->me_value != NULL && (key->ob_type)->tp_name[] == 'i') 

      { 

         PyIntObject* intObject = (PyIntObject*)key; 

         printf("insert %d\n", intObject->ob_ival); 

         ShowDictObject(mp); 

      } 

   } 

}                                          

调用print的时候也会调用到dealloc,所以num_free_dicts的值变化可能和想象的不一样。

Python 源码剖析(五)【DICT对象】的更多相关文章

  1. 《Python 源码剖析》之对象

    py一切皆对象的实现 Python中对象分为两类: 定长(int等), 非定长(list/dict等) 所有对象都有一些相同的东西, 源码中定义为PyObject和PyVarObject, 两个定义都 ...

  2. Python开发【源码剖析】 Dict对象

    static void ShowDictObject(PyDictObject* dictObject) { PyDictEntry* entry = dictObject->ma_table; ...

  3. Python源码剖析——01内建对象

    <Python源码剖析>笔记 第一章:对象初识 对象是Python中的核心概念,面向对象中的"类"和"对象"在Python中的概念都为对象,具体分为 ...

  4. Python 源码剖析(一)【python对象】

    处于研究python内存释放问题,在阅读部分python源码,顺便记录下所得.(基于<python源码剖析>(v2.4.1)与 python源码(v2.7.6)) 先列下总结:      ...

  5. Python开发【源码剖析】 List对象

    前言 本文探讨的Python版本为2.7.16,可从官网上下载,把压缩包Python-2.7.16.tgz解压到本地即可 需要基本C语言的知识(要看的懂) PyListObject对象 PyListO ...

  6. python源码剖析学习记录-01

    学习<Python源码剖析-深度探索动态语言核心技术>教程         Python总体架构,运行流程   File Group: 1.Core Modules 内部模块,例如:imp ...

  7. Python源码剖析|百度网盘免费下载|Python新手入门|Python新手学习资料

    百度网盘免费下载:Python源码剖析|新手免费领取下载 提取码:g78z 目录  · · · · · · 第0章 Python源码剖析——编译Python0.1 Python总体架构0.2 Pyth ...

  8. Python源码剖析——02虚拟机

    <Python源码剖析>笔记 第七章:编译结果 1.大概过程 运行一个Python程序会经历以下几个步骤: 由解释器对源文件(.py)进行编译,得到字节码(.pyc文件) 然后由虚拟机按照 ...

  9. Python 源码剖析 目录

    Python 源码剖析 作者: 陈儒 阅读者:春生 版本:python2.5 版本 本博客园的博客记录我会适当改成Python3版本 阅读 Python 源码剖析 对读者知识储备 1.C语言基础知识, ...

  10. Python 源码剖析(六)【内存管理机制】

    六.内存管理机制 1.内存管理架构 2.小块空间的内存池 3.循环引用的垃圾收集 4.python中的垃圾收集 1.内存管理架构 Python内存管理机制有两套实现,由编译符号PYMALLOC_DEB ...

随机推荐

  1. spring源码-bean之加载-2

    一.前面说了bean的容器初始化,后面当然是说bean的加载.这里还是不讲解ApplicationContext的bean的加载过程,还是通过最基础的XmlBeanFactory来进行讲解,主要是熟悉 ...

  2. HashMap在并发场景下踩过的坑

    本文来自网易云社区 作者:张伟 关于HashMap在并发场景下的问题有很多人,很多公司遇到过!也很多人总结过,我们很多时候都认为这样都坑距离自己很远,自己一定不会掉入这样都坑.可是我们随时都有就遇到了 ...

  3. unity面试题一

    一:什么是协同程序? 在主线程运行的同时开启另一段逻辑处理,来协助当前程序的执行,协程很像多线程,但是不是多线程,Unity的协程实在每帧结束之后去检测yield的条件是否满足. 二:Unity3d中 ...

  4. InnoDB锁冲突案例演示

      Preface       As we know,InnoDB is index organized table.InnoDB engine supports row-level lock bas ...

  5. Selenium(Python) ddt读取Excel文件数据驱动

    首先, 引入xlrd模块: ExcelDDT.py: import unittestfrom time import sleep from ddt import ddt, datafrom selen ...

  6. Objective-C Block数据类型 @protocol关键字

    Block数据类型 Block封装了一段代码 可以在任何时候执行 Block可以作为函数参数或者函数的返回值 而其本身又可以带输入参数或返回值 苹果官方建议尽量多用Block 在多线程 异步任务 集合 ...

  7. Unity - Humanoid设置Bip骨骼导入报错

    报错如下: 解决: 原因是biped骨骼必须按照Unity humanoid的要求设置,在max中设置如下:

  8. [CodeForce455A]Boredom

    题面描述 Alex doesn't like boredom. That's why whenever he gets bored, he comes up with games. One long ...

  9. git push origin master 错误解决办法

    一.错误代码如下: error: failed to push some refs to 'https://github.com/wbingithub/drag.git' 二.在网上搜了一下,如下写就 ...

  10. springMVC使用拦截器检查用户登录

    参考文章 编写拦截器类 package cultivate_web.interceptor; import javax.servlet.http.HttpServletRequest; import ...