主頁 > .NET開發 > [一起讀原始碼]走進C#并發佇列ConcurrentQueue的內部世界 — .NET Core篇

[一起讀原始碼]走進C#并發佇列ConcurrentQueue的內部世界 — .NET Core篇

2020-09-15 05:39:23 .NET開發

在上一篇《走進C#并發佇列ConcurrentQueue的內部世界》中決議了Framework下的ConcurrentQueue實作原理,經過拋磚引玉,得到了一眾大佬的指點,找到了.NET Core版本下的ConcurrentQueue原始碼,位于以下地址:

  • https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/Collections/Concurrent/ConcurrentQueue.cs
  • https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/Collections/Concurrent/ConcurrentQueueSegment.cs

我大致看了一下,雖然兩者的實作有不少相似的地方,不過在細節上新增了許多有意思的東西,還是覺得要單獨拉出來說一下,畫外音:誰叫我上篇立了flag,現在跪著也要寫完,,??

必須要吐糟的是,代碼中ConcurrentQueue類明明是包含在System.Collections.Concurrent命名空間下,但是原始碼結構中的檔案卻放在System.Private.CoreLib目錄中,這是鬧哪出~


存盤結構

從上面給出的原始碼地址可以猜測出整個結構依然是Segment+Queue的組合,通過一個Segment鏈表實作了Queue結構,但實際上內部又加了新的設計,拋去Queue先不看的話,Segment本身就是一個實作了多生產者多消費者的執行緒安全集合,甚至可以直接拿它當一個固定容量的執行緒安全佇列使用,這點與之前Framework中差別很大,如果結合Queue整體來看,Segment不再是固定容量,而是可以由Queue來控制每個Segment的容量大小(最小是32,上限是1024 * 1024),

在Framework中,佇列會給每個Segment分配一個索引,雖然這個索引是long型別的,但理論上說佇列容量還是存在上限,在Core中就不一樣了,它取消了這個索引,真正實作了一個無邊界(unbounded)佇列,

我猜測的原因是,在Framework中由于每個Segment是固定大小的,維護一個索引可以很方便的計算佇列里的元素數量,但是Core中的Segment大小不是固定的,使用索引并不能加快計算速度,使得這個索引不再有意義,這也意味著計算元素數量變得非常復雜,

一張圖看清它的真實面目,這里繼續沿用上一篇的結構圖稍作修改:

從圖中可以看到,整體結構上基本一致,核心改動就是Segment中增加了Slot(槽)的概念,這是真正存盤資料的地方,同時有一個序列號與之對應,

從代碼來看一下Segment的核心定義:

internal sealed class ConcurrentQueueSegment<T>
{
    //存放資料的容器
	internal readonly Slot[] _slots;

	//這個mask用來計算槽點,可以防止查找越界
	internal readonly int _slotsMask;

	//首尾位置指標
	internal PaddedHeadAndTail _headAndTail;

	//觀察保留標記,表示當前段在出隊時能否洗掉資料
	internal bool _preservedForObservation;

	//標記當前段是否被鎖住
	internal bool _frozenForEnqueues;

	//下一段的指標
	internal ConcurrentQueueSegment<T>? _nextSegment;
}

其中_preservedForObservation_frozenForEnqueues會比較難理解,后面再詳細介紹,

再看一下佇列的核心定義:

public class ConcurrentQueue<T> : IProducerConsumerCollection<T>, IReadOnlyCollection<T>
{
    //每一段的初始化長度,也是最小長度
	private const int InitialSegmentLength = 32;

    //每一段的最大長度
	private const int MaxSegmentLength = 1024 * 1024;

    //操作多個段時的鎖物件
	private readonly object _crossSegmentLock;

    //尾段指標
	private volatile ConcurrentQueueSegment<T> _tail;

    //首段指標
	private volatile ConcurrentQueueSegment<T> _head;
}

常規操作

還是按上一篇的套路為主線循序漸進,

創建實體

ConcurrentQueue依然提供了2個建構式,分別可以創建一個空佇列和指定資料集的佇列,

/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentQueue{T}"/> class.
/// </summary>
public ConcurrentQueue()
{
    _crossSegmentLock = new object();
    _tail = _head = new ConcurrentQueueSegment<T>(InitialSegmentLength);
}

還是熟悉的操作,創建了一個長度是32的Segment并把佇列的首尾指標都指向它,同時創建了鎖物件實體,僅此而已,
進一步看看Segment是怎么創建的:

internal ConcurrentQueueSegment(int boundedLength)
{
    //這里驗證了長度不能小于2并且必須是2的N次冪
    Debug.Assert(boundedLength >= 2, $"Must be >= 2, got {boundedLength}");
    Debug.Assert((boundedLength & (boundedLength - 1)) == 0, $"Must be a power of 2, got {boundedLength}");

    _slots = new Slot[boundedLength];
    //這個mask的作用就是用來計算陣列索引的防止越界,可以用`& _slotsMask`取代`% _slots.Length`
    _slotsMask = boundedLength - 1;

    //設定初始序列號
    for (int i = 0; i < _slots.Length; i++)
    {
        _slots[i].SequenceNumber = i;
    }
}

internal struct Slot
{
    [AllowNull, MaybeNull] public T Item; 
    
    public int SequenceNumber;
}

再看看怎么用集合初始化佇列,這個程序稍微麻煩點,但是很有意思:

public ConcurrentQueue(IEnumerable<T> collection)
{
    if (collection == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
    }

    _crossSegmentLock = new object();

    //計算得到第一段的長度
    int length = InitialSegmentLength;
    if (collection is ICollection<T> c)
    {
        int count = c.Count;
        if (count > length)
        {
            length = Math.Min(ConcurrentQueueSegment<T>.RoundUpToPowerOf2(count), MaxSegmentLength);
        }
    }

    //根據前面計算出來的長度創建一個Segment,再把資料依次入隊
    _tail = _head = new ConcurrentQueueSegment<T>(length);
    foreach (T item in collection)
    {
        Enqueue(item);
    }
}

可以看到,第一段的大小是根據初始集合的大小確定的,如果集合大小count大于32就對count進行向上取2的N次冪(RoundUpToPowerOf2)得到實際大小(但是不能超過最大值),否則就按默認值32來初始化,

向上取2的N次冪到底是啥意思??例如count是5,那得到的結果就是8(2×2×2);如果count是9,那結果就是16(2×2×2×2);如果剛好count是8那結果就是8(2×2×2),具體演算法是通過位運算實作的很有意思,至于為什么一定要是2的N次冪,中間的玄機我也沒搞明白,,

順藤摸瓜,再看看進隊操作如何實作,

元素進隊

/// <summary>在隊尾追加一個元素</summary>
public void Enqueue(T item)
{
    // 先嘗試在尾段插入一個元素
    if (!_tail.TryEnqueue(item))
    {
        // 如果插入失敗,就意味著尾段已經填滿,需要往后擴容
        EnqueueSlow(item);
    }
}

private void EnqueueSlow(T item)
{
    while (true)
    {
        ConcurrentQueueSegment<T> tail = _tail;

        // 先嘗試再隊尾插入元素,如果擴容完成了就會成功
        if (tail.TryEnqueue(item))
        {
            return;
        }
        // 獲得一把鎖,避免多個執行緒同時進行擴容
        lock (_crossSegmentLock)
        {
            //檢查是否擴容過了
            if (tail == _tail)
            {
                // 尾段凍結
                tail.EnsureFrozenForEnqueues();
                // 計算下一段的長度
                int nextSize = tail._preservedForObservation ? InitialSegmentLength : Math.Min(tail.Capacity * 2, MaxSegmentLength);
                var newTail = new ConcurrentQueueSegment<T>(nextSize);

                // 改變隊尾指向
                tail._nextSegment = newTail;
                // 指標交換
                _tail = newTail;
            }
        }
    }
}

從以上流程可以看到,擴容的主動權不再由Segment去控制,而是交給了佇列,正因為如此,所以在跨段操作時要先加鎖,在Framework版本中是在原子操作獲得指標后進行的擴容所以不會有這個問題,后面的出隊操作也是一樣的道理,擴容程序中有兩個細節需要重點關注,那就是SegmentFrozen和下一段的長度計算,
從前面Segment的定義中我們看到它維護了一個_frozenForEnqueues標記欄位,表示當前段是否被凍結鎖定,在被鎖住的情況下會讓其他入隊操作失敗,看一下實作程序:

// must only be called while queue's segment lock is held
internal void EnsureFrozenForEnqueues() 
{
    // flag used to ensure we don't increase the Tail more than once if frozen more than once
    if (!_frozenForEnqueues) 
    {
        _frozenForEnqueues = true;
        Interlocked.Add(ref _headAndTail.Tail, FreezeOffset);
    }
}

首先判斷當前凍結狀態,然后把它設定為true,再使用原子操作把尾指標增加了2倍段長的偏移量,這個尾指標才是真正限制當前段不可新增元素的關鍵點,后面講段的元素追加再關聯起來詳細介紹,而為什么要指定2倍段長這么一個特殊值呢,目的是為了把尾指標和mask做運算后落在同一個slot上,也就是說雖然兩個指標位置不一樣但是都指向的是同一個槽,

再說說下一段長度的計算問題,它主要是受_preservedForObservation這個欄位影響,正常情況下一段的長度是尾段的2倍,但如果尾段正好被標記為觀察保留(類似于上一篇的截取快照),那么下一段的長度依然是初始值32,原作者認為入隊操作不是很頻繁,這樣做主要是為了避免浪費空間,

接著是重頭戲,看一下如何給段追加元素:

public bool TryEnqueue(T item)
{
    Slot[] slots = _slots;

    // 如果發生競爭就自旋等待
    SpinWait spinner = default;
    while (true)
    {
        // 獲取當前段的尾指標
        int currentTail = Volatile.Read(ref _headAndTail.Tail);
        // 計算槽點
        int slotsIndex = currentTail & _slotsMask;
        // 讀取對應槽的序列號
        int sequenceNumber = Volatile.Read(ref slots[slotsIndex].SequenceNumber);

        // 判斷槽點序列號和指標是否匹配
        int diff = sequenceNumber - currentTail;
        if (diff == 0)
        {
            // 通過原子操作比較交換,保證了只有一個入隊者獲得可用空間
            if (Interlocked.CompareExchange(ref _headAndTail.Tail, currentTail + 1, currentTail) == currentTail)
            {
                // 把資料存入對應的槽點,以及更新序列號
                slots[slotsIndex].Item = item;
                Volatile.Write(ref slots[slotsIndex].SequenceNumber, currentTail + 1);
                return true;
            }
        }
        else if (diff < 0)
        {
            // 序列號小于指標就說明該段已經裝滿了,直接回傳false
            return false;
        }

        // 這次競爭失敗了,只好等下去
        spinner.SpinOnce(sleep1Threshold: -1);
    }
}

整個流程的核心就是借助槽點序列號和尾指標的匹配情況判斷是否有可用空間,因為在初始化的時候序列號是從0遞增,正常情況下尾指標和序列號肯定是匹配的,只有在整個段被裝滿時尾指標才會大于序列號,因為前面的凍結操作會給尾指標追加2倍段長的偏移量,要重點提出的是,只有在資料被寫入并且序列號更新完成后才表示整個位置的元素有效,才能有出隊的機會,在Framework是通過維護一個狀態位來實作這個功能,整個設計很有意思,要慢慢品,

這里我們可以總結一下序列號的核心作用:假設一個槽點N,對應序列號是Q,它能允許入隊的必要條件之一就是N==Q,由于入隊操作把位置N的序列號修改成N+1,那么可以猜測出在出隊時的必要條件之一就是滿足Q==N+1

代碼中的CompareExchange在上一篇中有介紹,這里不再重復,另外關于Volatile相關的稍微提一下,它的核心作用是避免記憶體與CPU之間的高速快取帶來的資料不一致問題,告訴編譯器直接讀寫原始資料,有興趣的可以找資料了解,限于篇幅不過多介紹,

元素出隊

可以猜測到,入隊的時候要根據容量大小進行擴容,那么與之對應的,出隊的時候就需要對它進行壓縮,也就是丟棄沒有資料的段,

/// <summary>從隊首移除一個元素</summary>
public bool TryDequeue([MaybeNullWhen(false)] out T result) =>
    _head.TryDequeue(out result) || 
    TryDequeueSlow(out result); 

private bool TryDequeueSlow([MaybeNullWhen(false)] out T item)
{
    // 不斷回圈嘗試出隊,直到成功或失敗為止
    while (true)
    {
        ConcurrentQueueSegment<T> head = _head;

        // 嘗試從隊首移除,如果成功就直接回傳了
        if (head.TryDequeue(out item))
        {
            return true;
        }

        // 如果首段為空并且沒有下一段了,則說明整個佇列都沒有資料了,回傳失敗
        if (head._nextSegment == null)
        {
            item = default!;
            return false;
        }

        // 既然下一段不為空,那就再次確認本段是否還能出隊成功,否則就要把它給移除了,等待下次回圈從下一段出隊
        if (head.TryDequeue(out item))
        {
            return true;
        }

        // 首段指標要往后移動,表示當前首段已丟棄,跨段操作要先加鎖
        lock (_crossSegmentLock)
        {
            if (head == _head)
            {
                _head = head._nextSegment;
            }
        }
    }
}

整體流程基本和入隊一樣,外層通過一個死回圈不斷嘗試操作,直到出隊成功或者佇列為慷訓傳失敗為止,釋放空間的操作也從Segment轉移到佇列上,所以要加鎖保證執行緒安全,這一步我在代碼注釋中寫的很詳細就不多解釋了,再看一下核心操作Segment是如何移除元素的:

public bool TryDequeue([MaybeNullWhen(false)] out T item)
{
    Slot[] slots = _slots;

    // 遇到競爭時自旋等待
    SpinWait spinner = default;
    while (true)
    {
        // 獲取頭指標地址
        int currentHead = Volatile.Read(ref _headAndTail.Head);
        // 計算槽點
        int slotsIndex = currentHead & _slotsMask;

        // 獲取槽點對應的序列號
        int sequenceNumber = Volatile.Read(ref slots[slotsIndex].SequenceNumber);

        // 比較序列號是否和期望值一樣,為什么要加1的原因前面入隊時說過
        int diff = sequenceNumber - (currentHead + 1);
        if (diff == 0)
        {
            // 通過原子操作比較交換得到可以出隊的槽點,并把頭指標往后移動一位
            if (Interlocked.CompareExchange(ref _headAndTail.Head, currentHead + 1, currentHead) == currentHead)
            {
                // 取出資料
                item = slots[slotsIndex].Item!;
                // 此時如果該段沒有被標記觀察保護,要把這個槽點的資料清空
                if (!Volatile.Read(ref _preservedForObservation))
                {
                    slots[slotsIndex].Item = default;
                    Volatile.Write(ref slots[slotsIndex].SequenceNumber, currentHead + slots.Length);
                }
                return true;
            }
        }
        else if (diff < 0)
        {
            // 這種情況說明該段已經沒有有效資料了,直接回傳失敗,
            bool frozen = _frozenForEnqueues;
            int currentTail = Volatile.Read(ref _headAndTail.Tail);
            if (currentTail - currentHead <= 0 || (frozen && (currentTail - FreezeOffset - currentHead <= 0)))
            {
                item = default!;
                return false;
            }
        }

        // 競爭失敗進入下一輪等待
        spinner.SpinOnce(sleep1Threshold: -1);
    }
}

流程和追加元素類似,大部分都寫在備注里面了,這里只額外提一下為空的情況,Segment為空只有一種情況,那就是頭尾指標落在了同一個槽點,但這是會出現兩種可能性:

  • 第一種是都落在了非最后一個槽點,意味著該段沒有被裝滿,拿首尾指標相減即可判斷,
  • 第二種是都落在了最后一個槽點,意味著該段已經被裝滿了,如果此時正在進行擴容(frozen),那么必須要在尾指標的基礎上減去FreezeOffset再去和頭指標判斷,原因前面有說過;

是不是感徑訓環相扣、相輔相成、如膠似漆、balabala.....??

統計元素數量

前面也預告過,因為佇列不再維護段索引,這樣會導致計算元素數量變得非常復雜,復雜到我都不想說這一部分了??,簡單描述一下就跳過了:核心思路就是一段一段來遍歷,然后計算出每段的大小最后把結果累加,如果涉及多個段還得加鎖,具體到段內部就要根據首尾指標計算槽點得出實際數量等等等等,代碼很長就不貼出來了,

這里也嚴重提醒一句,非必要情況下不要呼叫Count不要呼叫Count不要呼叫Count,

接下來重點說一下佇列的IsEmpty,由于Segment不再維護IsEmpty資訊,所以實作方式就有點曲線救國了,通過嘗試能否從隊首位置獲取一個元素來判斷是否佇列為空,也就是常說的TryPeek操作,但細節上稍有不同,

/// <summary>
/// 判斷佇列是否為空,千萬不要使用Count==0來判斷,也不要直接TryPeek
/// </summary>
public bool IsEmpty => !TryPeek(out _, resultUsed: false);

private bool TryPeek([MaybeNullWhen(false)] out T result, bool resultUsed)
{
    ConcurrentQueueSegment<T> s = _head;
    while (true)
    {
        ConcurrentQueueSegment<T>? next = Volatile.Read(ref s._nextSegment);

        // 從首段中獲取頭部元素,成功的話直接回傳true,獲取失敗就意味著首段為空了
        if (s.TryPeek(out result, resultUsed))
        {
            return true;
        }

        // 如果下一段不為空那就再嘗試從下一段重新獲取
        if (next != null)
        {
            s = next;
        }
        //如果下一段為空就說明整個佇列為空,跳出回圈直接回傳false了
        else if (Volatile.Read(ref s._nextSegment) == null)
        {
            break;
        }
    }
    result = default!;
    return false;
}

上面的代碼可以看到有一個特殊的引數resultUsed,它具體會有什么影響呢,那就得看看Segment是如何peek的:

public bool TryPeek([MaybeNullWhen(false)] out T result, bool resultUsed)
{
    // 實際上佇列的TryPeek是一個觀察保護操作,這時resultUsed會標記成true,如果是IsEmpty操作的話就為false,因為并不關心這個元素是否被釋放了
    if (resultUsed)
    {
        _preservedForObservation = true;
        Interlocked.MemoryBarrier();
    }

    Slot[] slots = _slots;

    SpinWait spinner = default;
    while (true)
    {
        int currentHead = Volatile.Read(ref _headAndTail.Head);
        int slotsIndex = currentHead & _slotsMask;

        int sequenceNumber = Volatile.Read(ref slots[slotsIndex].SequenceNumber);

        int diff = sequenceNumber - (currentHead + 1);
        if (diff == 0)
        {
            result = resultUsed ? slots[slotsIndex].Item! : default!;
            return true;
        }
        else if (diff < 0)
        {
            bool frozen = _frozenForEnqueues;
            int currentTail = Volatile.Read(ref _headAndTail.Tail);
            if (currentTail - currentHead <= 0 || (frozen && (currentTail - FreezeOffset - currentHead <= 0)))
            {
                result = default!;
                return false;
            }
        }
        spinner.SpinOnce(sleep1Threshold: -1);
    }
}

除了最開始的resultUsed判斷,其他的基本和出隊的邏輯一致,前面說的很詳細,這里不多介紹了,

列舉轉換資料

前面反復的提到觀察保護,這究竟是個啥意思??為什么要有這個操作??

其實看過上一篇文章的話就比較好理解一點,這里稍微回顧一下方便對比,在Framework中會有截取快照的操作,也就是類似ToArray\ToList\GetEnumerator這種要做資料迭代,它是通過原子操作維護一個m_numSnapshotTakers欄位來實作對資料的保護,目的是為了告訴其他出隊的執行緒我正在遍歷資料,你們執行出隊的時候不要把資料給刪了我要用的,在Core中也是為了實作同樣的功能才引入了觀察保護的概念,換了一種實作方式而已,

那么就以ToArray為例是怎么和其他操作互動的:

public T[] ToArray()
{
    // 這一步可以理解為保護現場
    SnapForObservation(out ConcurrentQueueSegment<T> head, out int headHead, out ConcurrentQueueSegment<T> tail, out int tailTail);

    // 計算佇列長度,這也是要回傳的陣列大小
    long count = GetCount(head, headHead, tail, tailTail);
    T[] arr = new T[count];

    // 開始迭代資料塞到目標陣列中
    using (IEnumerator<T> e = Enumerate(head, headHead, tail, tailTail))
    {
        int i = 0;
        while (e.MoveNext())
        {
            arr[i++] = e.Current;
        }
        Debug.Assert(count == i);
    }
    return arr;
}

上面的代碼中,有一次獲取佇列長度的操作,還有一次獲取迭代資料的操作,這兩步邏輯比較相似都是對整個佇列進行遍歷,所以做一次資料轉換的開銷非常非常大,使用的時候一定要謹慎,別的不多說,重點介紹一下如何實作保護現場的程序:

private void SnapForObservation(out ConcurrentQueueSegment<T> head, out int headHead, out ConcurrentQueueSegment<T> tail, out int tailTail)
{
    // 要保護現場肯定要先來一把鎖
    lock (_crossSegmentLock) 
    {
        head = _head;
        tail = _tail;

        // 一段一段進行遍歷
        for (ConcurrentQueueSegment<T> s = head; ; s = s._nextSegment!)
        {
            // 把每一段的觀察保護標記設定成true
            s._preservedForObservation = true;
            // 遍歷到最后一段了就結束
            if (s == tail) break;
        }
        // 尾段凍結,這樣就不能新增元素
        tail.EnsureFrozenForEnqueues(); 

        // 回傳兩個指標地址用來對每一個元素進行遍歷
        headHead = Volatile.Read(ref head._headAndTail.Head);
        tailTail = Volatile.Read(ref tail._headAndTail.Tail);
    }
}

可以看到上來就是一把鎖,如果此時正在進行擴容或者收容的操作會直接阻塞掉,運氣好沒有阻塞的話你也不能有新元素入隊了,因為尾段已經凍結鎖死只能自旋等待,而出隊也不能釋放空間了,原話是:

At this point, any dequeues from any segment won't overwrite the value, and none of the existing segments can have new items enqueued.

有人就要問,這里把尾段鎖死那等ToArray()完成后豈不是也不能有新元素入隊了?不用擔心,前面入隊邏輯提到過如果該段被鎖住佇列會新創建一個段然后再嘗試入隊,這樣就能成功了,但是問題又來了,假如前面的段還有很多空位,那豈不是有浪費空間的嫌疑?我們知道沒有觀察保護的時候每段會以2倍長度遞增,這樣的話空間浪費率還是挺高的,帶著疑問提了個Issue問一下:
https://github.com/dotnet/runtime/issues/35094

到這里就基本把.NET Core ConcurrentQueue說完了,


總結

對比Framework下的并發佇列,Core里面的改動還是不小的,盡管保留了SpinWaitInterlocked相關操作,但是也加入了lock,邏輯上也復雜了很多,我一步步分析和寫文章搞了好幾天,

至于性能對比,我找到一個官方給出的測驗結果,有興趣的可以看看:

https://github.com/dotnet/runtime/issues/27458#issuecomment-423964046

最后強行打個廣告,基于.NET Core平臺的開源分布式任務調度系統ScheduleMaster有興趣的star支持一下,2.0版本即將上線:

  • https://github.com/hey-hoho/ScheduleMasterCore
  • https://gitee.com/hey-hoho/ScheduleMasterCore(只從github同步)

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/42908.html

標籤:.NET Core

上一篇:使用IDbCommandInterceptor解決EF-CORE-3.x-使用MYSQL時,未正常的生成LIKE查詢陳述句

下一篇:IdentityServer4 QuickStart 授權與自定義Claims

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more