.net原始碼分析 - ConcurrentDictionary泛型
繼上篇Dictionary
原始碼分析,上篇講過的在這裡不會再重複
ConcurrentDictionary
原始碼地址:
前言
ConcurrentDictionary
一大特點是執行緒安全,在沒有ConcurrentDictionary
之前在多執行緒下用Dictionary
,不管讀寫都要加個鎖,不但麻煩,效能上也不是很好,因為在上篇分析中我們知道Dictionary
內部是由多個bucket
組成,不同bucket
的操作即使在多執行緒下也可以不互相影響,如果一個鎖把整個Dictionary
都鎖住實在有點浪費。
不過凡事都有兩面性,給每個Bucket
都加一個鎖也不可取,Bucket
的數量和Dictionary
元素數量是一樣的,而Bucket
可能會有一部分是空的,而且訪問Dictionary
的執行緒如果數量不是太多也根本用上不這麼多鎖,想想即使有10個執行緒在不停的操作這個Dictionary
,同時操作的最多也就10個,即使兩兩衝突訪問同一個Bucket
,5個鎖就夠了,當然這是最好的情況,最壞情況是這5個bucket
用同一個鎖。所以,要得到最好的結果需要嘗試取一個最優解,而影響因素則是bucket
數量和執行緒數量。我們想要的結果是鎖夠用但又不浪費。
微軟得出的結果是預設的鎖的數量是CPU核的個數,這個執行緒池預設的執行緒數量一樣。隨著Dictionary
的擴容,鎖的個數也可以跟著增加,這個可以在建構函式中自己指定。
下面看看ConcurrentDictionary
裡元素是做了怎樣的封裝。
private volatile Tables _tables; // 這不同於Dictionary的bucket 陣列,而是整個封裝起來,而且用volatile來保證讀寫時的原子性
private sealed class Tables
{
internal readonly Node[] _buckets; // bucket成了這樣,也就是ConcurrentDictionary可以認為是一個bucket陣列,每個Bucket裡又由next來形成連結串列
internal readonly object[] _locks; // 這個就是鎖的陣列了
internal volatile int[] _countPerLock; // 這個是每個鎖罩的元素個數
internal Tables(Node[] buckets, object[] locks, int[] countPerLock)
{
_buckets = buckets;
_locks = locks;
_countPerLock = countPerLock;
}
}
//由Dictionary裡的Entry改成Node,並且把next放到Node裡
private sealed class Node
{
internal readonly TKey _key;
internal TValue _value;
internal volatile Node _next; //next由volatile修飾,確保不被優化且讀寫原子性
internal readonly int _hashcode;
internal Node(TKey key, TValue value, int hashcode, Node next)
{
_key = key;
_value = value;
_next = next;
_hashcode = hashcode;
}
}
裡面的一些變數:
private readonly bool _growLockArray; // 是否在Dictionary擴容時也增加鎖的數量
private int _budget; // 單個鎖罩的元素的最大個數
private const int DefaultCapacity = 31; //ConcurrentDictionary預設大小,和List,Dictionary不一樣
private const int MaxLockNumber = 1024; //最大鎖的個數,不過也可以在建構函式中弄個更大的,不般沒必要
看看建構函式初始化做了些啥
internal ConcurrentDictionary(int concurrencyLevel, int capacity, bool growLockArray, IEqualityComparer<TKey> comparer)
{
if (concurrencyLevel < 1)
{
throw new ArgumentOutOfRangeException(nameof(concurrencyLevel), SR.ConcurrentDictionary_ConcurrencyLevelMustBePositive);
}
if (capacity < 0)
{
throw new ArgumentOutOfRangeException(nameof(capacity), SR.ConcurrentDictionary_CapacityMustNotBeNegative);
}
if (comparer == null) throw new ArgumentNullException(nameof(comparer));
// The capacity should be at least as large as the concurrency level. Otherwise, we would have locks that don't guard
// any buckets.
if (capacity < concurrencyLevel) //concurrencyLevel就是鎖的個數,容量小於鎖的個數時,一部分鎖就真是完全沒用了
{
capacity = concurrencyLevel; //所以容量至少要和鎖的個數一樣
}
object[] locks = new object[concurrencyLevel]; //初始化鎖陣列
for (int i = 0; i < locks.Length; i++)
{
locks[i] = new object();
}
int[] countPerLock = new int[locks.Length]; //初始化鎖罩元素個數的陣列
Node[] buckets = new Node[capacity]; //初始化Node
_tables = new Tables(buckets, locks, countPerLock); //初始化table
_comparer = comparer;
_growLockArray = growLockArray; //這就是指定鎖是否增長
_budget = buckets.Length / locks.Length; //鎖最大罩【容量/鎖個數】這麼多元素(畢竟不是扛把子,罩不了太多),多了怎麼辦,擴地盤。。
}
通過常用的函式看看是怎麼做到多執行緒安全的
TryGet
public bool TryGetValue(TKey key, out TValue value)
{
if (key == null) ThrowKeyNullException();
return TryGetValueInternal(key, _comparer.GetHashCode(key), out value);
}
private bool TryGetValueInternal(TKey key, int hashcode, out TValue value)
{
Debug.Assert(_comparer.GetHashCode(key) == hashcode);
//先用本地變數存一下,免得在另外一個執行緒擴容時變了
Tables tables = _tables;
//又是hashcode取餘哈,不多說
//int bucketNo = (hashcode & 0x7fffffff) % bucketCount;
int bucketNo = GetBucket(hashcode, tables._buckets.Length);
//這個用Valatile確保讀了最新的Node
Node n = Volatile.Read<Node>(ref tables._buckets[bucketNo]);
//遍歷bucket,真懷疑這些程式碼是幾個人寫的,風格都不一樣
while (n != null)
{
//找到了
if (hashcode == n._hashcode && _comparer.Equals(n._key, key))
{
//返回true和value
value = n._value;
return true;
}
n = n._next;
}
value = default(TValue);
return false;
}
GetOrAdd
public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
{
if (key == null) ThrowKeyNullException();
if (valueFactory == null) throw new ArgumentNullException(nameof(valueFactory));
int hashcode = _comparer.GetHashCode(key);
TValue resultingValue;
//先TryGet,沒有的再TryAdd
if (!TryGetValueInternal(key, hashcode, out resultingValue))
{
TryAddInternal(key, hashcode, valueFactory(key), false, true, out resultingValue);
}
return resultingValue;
}
private bool TryAddInternal(TKey key, int hashcode, TValue value, bool updateIfExists, bool acquireLock, out TValue resultingValue)
{
Debug.Assert(_comparer.GetHashCode(key) == hashcode);
while (true)
{
int bucketNo, lockNo;
Tables tables = _tables;
//GetBucketAndLockNo函式裡面就是下面兩句
//bucketNo = (hashcode & 0x7fffffff) % bucketCount; 取餘得bucket No.,和Dictionary一樣
//lockNo = bucketNo % lockCount; 也是取餘得鎖No. 也就是一個鎖也是可能給多個Bucket用的
GetBucketAndLockNo(hashcode, out bucketNo, out lockNo, tables._buckets.Length, tables._locks.Length);
bool resizeDesired = false;
bool lockTaken = false;
try
{
if (acquireLock) //引數指定需要鎖的話就鎖上這個bucket的鎖,也就在建構函式初始化時不需要鎖
Monitor.Enter(tables._locks[lockNo], ref lockTaken);
//這裡是做個校驗,判斷tables是否在這邊取完鎖後其他執行緒把元素給擴容了,擴容會生成一個新的tables,tables變了的話上面的鎖就沒意義了,需要重來,所以這整個是在while(true)裡面
if (tables != _tables)
{
continue;
}
Node prev = null;
//這裡就遍歷bucket裡的連結串列了,和Dictionary差不多
for (Node node = tables._buckets[bucketNo]; node != null; node = node._next)
{
Debug.Assert((prev == null && node == tables._buckets[bucketNo]) || prev._next == node);
if (hashcode == node._hashcode && _comparer.Equals(node._key, key))//看是否找到
{
//看是否需要更新node
if (updateIfExists)
{
if (s_isValueWriteAtomic) //這個是判斷是否是支援原子操作的值型別,比如32位上byte,int,byte,short都是原子的,而long,double就不是了,支援原子操作的直接賦值就可以了,得注意是值型別,引用型別可不能這麼搞
{
node._value = value;
}
else //不是原子操作的值型別就new一個node
{
Node newNode = new Node(node._key, value, hashcode, node._next);
if (prev == null)
{
tables._buckets[bucketNo] = newNode;
}
else
{
prev._next = newNode;
}
}
resultingValue = value;
}
else//不更新就直接取值
{
resultingValue = node._value;
}
return false; //找到了返回false,表示不用Add就Get了
}
prev = node;
}
// 找了一圈沒找著,就Add吧,new一個node用Volatile的寫操作寫到bucket裡
Volatile.Write<Node>(ref tables._buckets[bucketNo], new Node(key, value, hashcode, tables._buckets[bucketNo]));
checked//這裡如果超出int大小,拋overflow exception, 能進這裡表示一個鎖罩int.MaxValue大小的Node,真成扛把子了,極端情況下只有一個鎖而且Node的大小已經是Int.MaxValue才可能會出現(還要看budget同不同意)
{
tables._countPerLock[lockNo]++;
}
//如果鎖罩的Node個數大於budget就表示差不多需要擴容了,黑社會表示地盤不夠用了
if (tables._countPerLock[lockNo] > _budget)
{
resizeDesired = true;
}
}
finally
{
if (lockTaken) //出現異常要把鎖釋放掉
Monitor.Exit(tables._locks[lockNo]);
}
if (resizeDesired)
{
GrowTable(tables); //擴容
}
resultingValue = value; //result值
return true;
}
}
再來看看是怎麼擴容的
private void GrowTable(Tables tables)
{
const int MaxArrayLength = 0X7FEFFFFF;
int locksAcquired = 0;
try
{
// 先把第一個鎖鎖住,免得其他執行緒也要擴容走進來
AcquireLocks(0, 1, ref locksAcquired);
//如果table已經變了,也就是那些等著上面鎖的執行緒進來發現已經擴容完了直接返回就好了
if (tables != _tables)
{
return;
}
// 計算每個鎖罩的元素的個數總和,也就是當前元素的個數
long approxCount = 0;
for (int i = 0; i < tables._countPerLock.Length; i++)
{
approxCount += tables._countPerLock[i];
}
//如果元素總和不到Bucket大小的1/4,說明擴容擴得不是時候,歸根結底是budget小了
if (approxCount < tables._buckets.Length / 4)
{
_budget = 2 * _budget;//2倍增加budget
if (_budget < 0) //小於0說明overflow了,看看,前面用check,這裡又用小於0。。
{
_budget = int.MaxValue; //直接最大值吧
}
return;
}
int newLength = 0;
bool maximizeTableSize = false;
try
{
checked
{
//2倍+1取得一個奇數作了新的容量
newLength = tables._buckets.Length * 2 + 1;
//看是否能整除3/5/7,能就+2,直到不能整除為止,也挺奇怪這演算法,List是2倍,Dictionary是比2倍大的一個質數,這裡又是另外一種,只能說各人有各人的演算法
while (newLength % 3 == 0 || newLength % 5 == 0 || newLength % 7 == 0)
{
newLength += 2;
}
Debug.Assert(newLength % 2 != 0);
if (newLength > MaxArrayLength)
{
maximizeTableSize = true;
}
}
}
catch (OverflowException)
{
maximizeTableSize = true;
}
if (maximizeTableSize)//進這裡表示溢位了
{
newLength = MaxArrayLength; //直接給最大值
_budget = int.MaxValue; //budget也給最大值,因為沒法再擴容了,給小了進來也沒意義
}
//擴容之後又是熟悉的重新分配元素,和Dictionary基本一致,這裡要先把所有鎖鎖住,前面已經鎖了第一個,這裡鎖其他的
AcquireLocks(1, tables._locks.Length, ref locksAcquired);
object[] newLocks = tables._locks;
//如果允許增加鎖並則鎖的個數還不到1024,就增加鎖
if (_growLockArray && tables._locks.Length < MaxLockNumber)
{
newLocks = new object[tables._locks.Length * 2]; //也是2倍增加
Array.Copy(tables._locks, 0, newLocks, 0, tables._locks.Length); //舊鎖複製到新陣列裡
for (int i = tables._locks.Length; i < newLocks.Length; i++) //再初始化增的鎖
{
newLocks[i] = new object();
}
}
//新的Node陣列
Node[] newBuckets = new Node[newLength];
int[] newCountPerLock = new int[newLocks.Length];
//遍歷bucket
for (int i = 0; i < tables._buckets.Length; i++)
{
Node current = tables._buckets[i];//當前node
while (current != null)
{
Node next = current._next;
int newBucketNo, newLockNo;
//算新的bucket No.和lock No.
GetBucketAndLockNo(current._hashcode, out newBucketNo, out newLockNo, newBuckets.Length, newLocks.Length);
//重建個新的node,注意next指到了上一個node,和Dictionary裡一樣
newBuckets[newBucketNo] = new Node(current._key, current._value, current._hashcode, newBuckets[newBucketNo]);
checked
{
newCountPerLock[newLockNo]++; //這個鎖又罩了一個小弟,加一個
}
current = next;
}
}
//調整下budget
_budget = Math.Max(1, newBuckets.Length / newLocks.Length);
//得到新的table
_tables = new Tables(newBuckets, newLocks, newCountPerLock);
}
finally
{
// 釋放鎖
ReleaseLocks(0, locksAcquired);
}
}
通過這幾個函式差不多也就清楚了ConcurrentDictionary
整個的原理,其他函式有興趣的可以去看看,都差不多這個意思。
總結
說完了,總結下,ConcurrentDictionary
可以說是為了避免一個大鎖鎖住整個Dictionary
帶來的效能損失而出來的,當然也是採用空間換時間,不過這空間換得還是很值得的,一些object
而已。
原理在於Dictionary
本質是是一個連結串列陣列,只有在多執行緒同時操作到陣列裡同一個連結串列時才需要鎖,所以就用到一個鎖陣列,每個鎖罩著幾個小弟(bucket
及bucket
內的連結串列元素),這樣多執行緒讀寫不同鎖罩的區域的時候可以同時進行而不會等待,進而提高多執行緒效能。
不過也凡事無絕對,不同業務場景的需求不一樣,可能Dictionary
配合ReaderWriterLockSlim
在某些場景(比如讀的機會遠大於寫的)可能會有更好的表現。
相關文章
- .net原始碼分析 – Dictionary泛型原始碼泛型
- 泛型型別(.NET 指南)泛型型別
- SOFA 原始碼分析 — 泛化呼叫原始碼
- .net原始碼分析 – List原始碼
- .net core 原始碼分析原始碼
- 【科普】.NET6 泛型泛型
- Java™ 教程(泛型原始型別)Java泛型型別
- ASP.NET Core[原始碼分析篇] - StartupASP.NET原始碼
- 泛型類、泛型方法及泛型應用泛型
- Java中泛型的詳細解析,深入分析泛型的使用方式Java泛型
- 【java】【泛型】泛型geneticJava泛型
- .NET進階篇01-Generic泛型深入泛型
- ASP.NET Core[原始碼分析篇] - 認證ASP.NET原始碼
- Presto原始碼分析之資料型別REST原始碼資料型別
- 泛型類和泛型方法泛型
- 泛型--泛型萬用字元和泛型的上下限泛型字元
- ConcurrentDictionary與ConcurrentQueue
- Net6 Configuration & Options 原始碼分析 Part1原始碼
- TypeScript 泛型介面和泛型類TypeScript泛型
- Go 泛型之泛型約束Go泛型
- Retrofit原始碼分析三 原始碼分析原始碼
- Swift 4 泛型:如何在你的程式碼或App裡應用泛型Swift泛型APP
- 泛型泛型
- 泛型最佳實踐:Go泛型設計者教你如何用泛型泛型Go
- .net core 原始碼分析(9) 依賴注入(DI)-Dependency Injection原始碼依賴注入
- Net6 Configuration & Options 原始碼分析 Part2 Options原始碼
- Net6 DI原始碼分析Part2 Engine,ServiceProvider原始碼IDE
- Net6 DI原始碼分析Part4 CallSiteFactory ServiceCallSite原始碼
- Net6 DI原始碼分析Part3 CallSiteRuntimeResolver,CallSiteVisitor原始碼
- 集合原始碼分析[2]-AbstractList 原始碼分析原始碼
- 集合原始碼分析[3]-ArrayList 原始碼分析原始碼
- Guava 原始碼分析之 EventBus 原始碼分析Guava原始碼
- 【JDK原始碼分析系列】ArrayBlockingQueue原始碼分析JDK原始碼BloC
- 集合原始碼分析[1]-Collection 原始碼分析原始碼
- Android 原始碼分析之 AsyncTask 原始碼分析Android原始碼
- lodash原始碼分析之獲取資料型別原始碼資料型別
- mybaits原始碼分析--型別轉換模組(三)AI原始碼型別
- TypeScript 泛型型別TypeScript泛型型別