Pipe——高性能IO

来源:https://www.cnblogs.com/yswenli/archive/2019/11/07/11810317.html
-Advertisement-
Play Games

System.IO.Pipelines是一個新的庫,旨在簡化在.NET中執行高性能IO的過程。它是一個依賴.NET Standard的庫,適用於所有.NET實現。 Pipelines誕生於.NET Core團隊,為使Kestrel成為業界最快的Web伺服器之一。最初從作為Kestrel內部的實現細節 ...


System.IO.Pipelines是一個新的庫,旨在簡化在.NET中執行高性能IO的過程。它是一個依賴.NET Standard的庫,適用於所有.NET實現

Pipelines誕生於.NET Core團隊,為使Kestrel成為業界最快的Web伺服器之一。最初從作為Kestrel內部的實現細節發展成為可重用的API,它在.Net Core 2.1中作為可用於所有.NET開發人員的最高級BCL API(System.IO.Pipelines)提供。

它解決了什麼問題?

為了正確解析Stream或Socket中的數據,代碼有固定的樣板,並且有許多極端情況,為了處理他們,不得不編寫難以維護的複雜代碼。
實現高性能和正確性,同時也難以處理這種複雜性。Pipelines旨在解決這種複雜性。

有多複雜?

讓我們從一個簡單的問題開始吧。我們想編寫一個TCP伺服器,它接收來自客戶端的用行分隔的消息(由\n分隔)。(譯者註:即一行為一條消息)

使用NetworkStream的TCP伺服器

在Pipelines之前用.NET編寫的典型代碼如下所示:

async Task ProcessLinesAsync(NetworkStream stream)
{
    var buffer = new byte[1024];
    await stream.ReadAsync(buffer, 0, buffer.Length);
    
    // 在buffer中處理一行消息
    ProcessLine(buffer);
}

此代碼可能在本地測試時正確工作,但它有幾個潛在錯誤:

  • 一次ReadAsync調用可能沒有收到整個消息(行尾)。
  • 它忽略了stream.ReadAsync()返回值中實際填充到buffer中的數據量。(譯者註:即不一定將buffer填充滿)
  • 一次ReadAsync調用不能處理多條消息。

這些是讀取流數據時常見的一些缺陷。為瞭解決這個問題,我們需要做一些改變:

  • 我們需要緩衝傳入的數據,直到找到新的行。
  • 我們需要解析緩衝區中返回的所有行
async Task ProcessLinesAsync(NetworkStream stream)
{
    var buffer = new byte[1024];
    var bytesBuffered = 0;
    var bytesConsumed = 0;

    while (true)
    {
        var bytesRead = await stream.ReadAsync(buffer, bytesBuffered, buffer.Length - bytesBuffered);
        if (bytesRead == 0)
        {
            // EOF 已經到末尾
            break;
        }
        // 跟蹤已緩衝的位元組數
        bytesBuffered += bytesRead;
        
        var linePosition = -1;

        do
        {
            // 在緩衝數據中查找找一個行末尾
            linePosition = Array.IndexOf(buffer, (byte)‘\n‘, bytesConsumed, bytesBuffered - bytesConsumed);

            if (linePosition >= 0)
            {
                // 根據偏移量計算一行的長度
                var lineLength = linePosition - bytesConsumed;

                // 處理這一行
                ProcessLine(buffer, bytesConsumed, lineLength);

                // 移動bytesConsumed為了跳過我們已經處理掉的行 (包括\n)
                bytesConsumed += lineLength + 1;
            }
        }
        while (linePosition >= 0);
    }
}

這一次,這可能適用於本地開發,但一行可能大於1KiB(1024位元組)。我們需要調整輸入緩衝區的大小,直到找到新行。

因此,我們可以在堆上分配緩衝區去處理更長的一行。我們從客戶端解析較長的一行時,可以通過使用ArrayPool<byte>避免重覆分配緩衝區來改進這一點。

async Task ProcessLinesAsync(NetworkStream stream)
{
    byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);
    var bytesBuffered = 0;
    var bytesConsumed = 0;

    while (true)
    {
        // 在buffer中計算中剩餘的位元組數
        var bytesRemaining = buffer.Length - bytesBuffered;

        if (bytesRemaining == 0)
        {
            // 將buffer size翻倍 並且將之前緩衝的數據複製到新的緩衝區
            var newBuffer = ArrayPool<byte>.Shared.Rent(buffer.Length * 2);
            Buffer.BlockCopy(buffer, 0, newBuffer, 0, buffer.Length);
            // 將舊的buffer丟回池中
            ArrayPool<byte>.Shared.Return(buffer);
            buffer = newBuffer;
            bytesRemaining = buffer.Length - bytesBuffered;
        }

        var bytesRead = await stream.ReadAsync(buffer, bytesBuffered, bytesRemaining);
        if (bytesRead == 0)
        {
            // EOF 末尾
            break;
        }
        
        // 跟蹤已緩衝的位元組數
        bytesBuffered += bytesRead;
        
        do
        {
            // 在緩衝數據中查找找一個行末尾
            linePosition = Array.IndexOf(buffer, (byte)‘\n‘, bytesConsumed, bytesBuffered - bytesConsumed);

            if (linePosition >= 0)
            {
                // 根據偏移量計算一行的長度
                var lineLength = linePosition - bytesConsumed;

                // 處理這一行
                ProcessLine(buffer, bytesConsumed, lineLength);

                // 移動bytesConsumed為了跳過我們已經處理掉的行 (包括\n)
                bytesConsumed += lineLength + 1;
            }
        }
        while (linePosition >= 0);
    }
}

這段代碼有效,但現在我們正在重新調整緩衝區大小,從而產生更多緩衝區副本。它將使用更多記憶體,因為根據代碼在處理一行行後不會縮緩衝區的大小。為避免這種情況,我們可以存儲緩衝區序列,而不是每次超過1KiB大小時調整大小。

此外,我們不會增長1KiB的 緩衝區,直到它完全為空。這意味著我們最終傳遞給ReadAsync越來越小的緩衝區,這將導致對操作系統的更多調用。

為了緩解這種情況,我們將在現有緩衝區中剩餘少於512個位元組時分配一個新緩衝區:

public class BufferSegment
{
    public byte[] Buffer { get; set; }
    public int Count { get; set; }

    public int Remaining => Buffer.Length - Count;
}

async Task ProcessLinesAsync(NetworkStream stream)
{
    const int minimumBufferSize = 512;

    var segments = new List<BufferSegment>();
    var bytesConsumed = 0;
    var bytesConsumedBufferIndex = 0;
    var segment = new BufferSegment { Buffer = ArrayPool<byte>.Shared.Rent(1024) };

    segments.Add(segment);

    while (true)
    {
        // Calculate the amount of bytes remaining in the buffer
        if (segment.Remaining < minimumBufferSize)
        {
            // Allocate a new segment
            segment = new BufferSegment { Buffer = ArrayPool<byte>.Shared.Rent(1024) };
            segments.Add(segment);
        }

        var bytesRead = await stream.ReadAsync(segment.Buffer, segment.Count, segment.Remaining);
        if (bytesRead == 0)
        {
            break;
        }

        // Keep track of the amount of buffered bytes
        segment.Count += bytesRead;

        while (true)
        {
            // Look for a EOL in the list of segments
            var (segmentIndex, segmentOffset) = IndexOf(segments, (byte)‘\n‘, bytesConsumedBufferIndex, bytesConsumed);

            if (segmentIndex >= 0)
            {
                // Process the line
                ProcessLine(segments, segmentIndex, segmentOffset);

                bytesConsumedBufferIndex = segmentOffset;
                bytesConsumed = segmentOffset + 1;
            }
            else
            {
                break;
            }
        }

        // Drop fully consumed segments from the list so we don‘t look at them again
        for (var i = bytesConsumedBufferIndex; i >= 0; --i)
        {
            var consumedSegment = segments[i];
            // Return all segments unless this is the current segment
            if (consumedSegment != segment)
            {
                ArrayPool<byte>.Shared.Return(consumedSegment.Buffer);
                segments.RemoveAt(i);
            }
        }
    }
}

(int segmentIndex, int segmentOffest) IndexOf(List<BufferSegment> segments, byte value, int startBufferIndex, int startSegmentOffset)
{
    var first = true;
    for (var i = startBufferIndex; i < segments.Count; ++i)
    {
        var segment = segments[i];
        // Start from the correct offset
        var offset = first ? startSegmentOffset : 0;
        var index = Array.IndexOf(segment.Buffer, value, offset, segment.Count - offset);

        if (index >= 0)
        {
            // Return the buffer index and the index within that segment where EOL was found
            return (i, index);
        }

        first = false;
    }
    return (-1, -1);
}

此代碼只是得到很多更加複雜。當我們正在尋找分隔符時,我們同時跟蹤已填充的緩衝區序列。為此,我們此處使用List<BufferSegment>查找新行分隔符時表示緩衝數據。其結果是,ProcessLineIndexOf現在接受List<BufferSegment>作為參數,而不是一個byte[],offset和count。我們的解析邏輯現在需要處理一個或多個緩衝區序列。

我們的伺服器現在處理部分消息,它使用池化記憶體來減少總體記憶體消耗,但我們還需要進行更多更改:

  1. 我們使用的byte[]ArrayPool<byte>的只是普通的托管數組。這意味著無論何時我們執行ReadAsyncWriteAsync,這些緩衝區都會在非同步操作的生命周期內被固定(以便與操作系統上的本機IO API互操作)。這對GC有性能影響,因為無法移動固定記憶體,這可能導致堆碎片。根據非同步操作掛起的時間長短,池的實現可能需要更改。
  2. 可以通過解耦讀取邏輯處理邏輯來優化吞吐量。這會創建一個批處理效果,使解析邏輯可以使用更大的緩衝區塊,而不是僅在解析單個行後才讀取更多數據。這引入了一些額外的複雜性
    • 我們需要兩個彼此獨立運行的迴圈。一個讀取Socket和一個解析緩衝區。
    • 當數據可用時,我們需要一種方法來向解析邏輯發出信號。
    • 我們需要決定如果迴圈讀取Socket“太快”會發生什麼。如果解析邏輯無法跟上,我們需要一種方法來限制讀取迴圈(邏輯)。這通常被稱為“流量控制”或“背壓”。
    • 我們需要確保事情是線程安全的。我們現在在讀取迴圈解析迴圈之間共用多個緩衝區,並且這些緩衝區在不同的線程上獨立運行。
    • 記憶體管理邏輯現在分佈在兩個不同的代碼段中,從填充緩衝區池的代碼是從套接字讀取的,而從緩衝區池取數據的代碼是解析邏輯
    • 我們需要非常小心在解析邏輯完成之後我們如何處理緩衝區序列。如果我們不小心,我們可能會返回一個仍由Socket讀取邏輯寫入的緩衝區序列。

複雜性已經到了極端(我們甚至沒有涵蓋所有案例)。高性能網路應用通常意味著編寫非常複雜的代碼,以便從系統中獲得更高的性能。

System.IO.Pipelines的目標是使這種類型的代碼更容易編寫。

使用System.IO.Pipelines的TCP伺服器

讓我們來看看這個例子的樣子System.IO.Pipelines:

async Task ProcessLinesAsync(Socket socket)
{
    var pipe = new Pipe();
    Task writing = FillPipeAsync(socket, pipe.Writer);
    Task reading = ReadPipeAsync(pipe.Reader);

    return Task.WhenAll(reading, writing);
}

async Task FillPipeAsync(Socket socket, PipeWriter writer)
{
    const int minimumBufferSize = 512;

    while (true)
    {
        // 從PipeWriter至少分配512位元組
        Memory<byte> memory = writer.GetMemory(minimumBufferSize);
        try 
        {
            int bytesRead = await socket.ReceiveAsync(memory, SocketFlags.None);
            if (bytesRead == 0)
            {
                break;
            }
            // 告訴PipeWriter從套接字讀取了多少
            writer.Advance(bytesRead);
        }
        catch (Exception ex)
        {
            LogError(ex);
            break;
        }

        // 標記數據可用,讓PipeReader讀取
        FlushResult result = await writer.FlushAsync();

        if (result.IsCompleted)
        {
            break;
        }
    }

    // 告訴PipeReader沒有更多的數據
    writer.Complete();
}

async Task ReadPipeAsync(PipeReader reader)
{
    while (true)
    {
        ReadResult result = await reader.ReadAsync();

        ReadOnlySequence<byte> buffer = result.Buffer;
        SequencePosition? position = null;

        do 
        {
            // 在緩衝數據中查找找一個行末尾
            position = buffer.PositionOf((byte)‘\n‘);

            if (position != null)
            {
                // 處理這一行
                ProcessLine(buffer.Slice(0, position.Value));
                
                // 跳過 這一行+\n (basically position 主要位置?)
                buffer = buffer.Slice(buffer.GetPosition(1, position.Value));
            }
        }
        while (position != null);

        // 告訴PipeReader我們以及處理多少緩衝
        reader.AdvanceTo(buffer.Start, buffer.End);

        // 如果沒有更多的數據,停止都去
        if (result.IsCompleted)
        {
            break;
        }
    }

    // 將PipeReader標記為完成
    reader.Complete();
}

我們的行讀取器的pipelines版本有2個迴圈:

  • FillPipeAsync從Socket讀取並寫入PipeWriter。
  • ReadPipeAsync從PipeReader中讀取並解析傳入的行。

與原始示例不同,在任何地方都沒有分配顯式緩衝區。這是管道的核心功能之一。所有緩衝區管理都委托給PipeReader/PipeWriter實現。

這使得使用代碼更容易專註於業務邏輯而不是複雜的緩衝區管理。

在第一個迴圈中,我們首先調用PipeWriter.GetMemory(int)從底層編寫器獲取一些記憶體; 然後我們調用PipeWriter.Advance(int)告訴PipeWriter我們實際寫入緩衝區的數據量。然後我們調用PipeWriter.FlushAsync()來提供數據給PipeReader。

在第二個迴圈中,我們正在使用PipeWriter最終來自的緩衝區Socket。當調用PipeReader.ReadAsync()返回時,我們得到一個ReadResult包含2條重要信息,包括以ReadOnlySequence<byte>形式讀取的數據和bool IsCompleted,讓reader知道writer是否寫完(EOF)。在找到行尾(EOL)分隔符並解析該行之後,我們將緩衝區切片以跳過我們已經處理過的內容,然後我們調用PipeReader.AdvanceTo告訴PipeReader我們消耗了多少數據。

在每個迴圈結束時,我們完成了reader和writer。這允許底層Pipe釋放它分配的所有記憶體。

System.IO.Pipelines

除了處理記憶體管理之外,其他核心管道功能還包括能夠在Pipe不實際消耗數據的情況下查看數據。

PipeReader有兩個核心API ReadAsyncAdvanceToReadAsync獲取Pipe數據,AdvanceTo告訴PipeReader不再需要這些緩衝區,以便可以丟棄它們(例如返回到底層緩衝池)。


這是一個http解析器的示例,它在接收Pipe到有效起始行之前讀取部分數據緩衝區數據。

技術分享圖片

ReadOnlySequence<T>

該Pipe實現存儲了在PipeWriter和PipeReader之間傳遞的緩衝區的鏈接列表。PipeReader.ReadAsync暴露一個ReadOnlySequence<T>新的BCL類型,它表示一個或多個ReadOnlyMemory<T>段的視圖,類似於Span<T>和Memory<T>提供數組和字元串的視圖。

技術分享圖片

該Pipe內部維護指向reader和writer可以分配或更新它們的數據集合,。SequencePosition表示緩衝區鏈表中的單個點,可用於有效地對ReadOnlySequence<T>進行切片。

這段實在翻譯困難,給出原文
The Pipe internally maintains pointers to where the reader and writer are in the overall set of allocated data and updates them as data is written or read. The SequencePosition represents a single point in the linked list of buffers and can be used to efficiently slice the ReadOnlySequence

由於ReadOnlySequence<T>可以支持一個或多個段,因此高性能處理邏輯通常基於單個或多個段來分割快速和慢速路徑(fast and slow paths?)。

例如,這是一個將ASCII ReadOnlySequence<byte>轉換為string以下內容的常式:

string GetAsciiString(ReadOnlySequence<byte> buffer)
{
    if (buffer.IsSingleSegment)
    {
        return Encoding.ASCII.GetString(buffer.First.Span);
    }

    return string.Create((int)buffer.Length, buffer, (span, sequence) =>
    {
        foreach (var segment in sequence)
        {
            Encoding.ASCII.GetChars(segment.Span, span);

            span = span.Slice(segment.Length);
        }
    });
}

背壓和流量控制

在一個完美的世界中,讀取和解析工作是一個團隊:讀取線程消耗來自網路的數據並將其放入緩衝區,而解析線程負責構建適當的數據結構。通常,解析將比僅從網路複製數據塊花費更多時間。結果,讀取線程可以輕易地壓倒解析線程。結果是讀取線程必須減慢或分配更多記憶體來存儲解析線程的數據。為獲得最佳性能,在頻繁暫停和分配更多記憶體之間存在平衡。

為瞭解決這個問題,管道有兩個設置來控制數據的流量,PauseWriterThreshold和ResumeWriterThreshold。PauseWriterThreshold決定有多少數據應該在調用PipeWriter.FlushAsync之前進行緩衝停頓。ResumeWriterThreshold控制reader消耗多少後寫入可以恢復。

技術分享圖片

當Pipe的數據量超過PauseWriterThreshold,PipeWriter.FlushAsync會非同步阻塞。數據量變得低於ResumeWriterThreshold,它會解鎖時。兩個值用於防止在極限附近發生反覆阻塞和解鎖。

IO調度

通常在使用async / await時,會線上程池線程或當前線程上調用continuation SynchronizationContext。

在執行IO時,對執行IO的位置進行細粒度控制非常重要,這樣可以更有效地利用CPU緩存,這對於Web伺服器等高性能應用程式至關重要。Pipelines公開了一個PipeScheduler確定非同步回調運行位置的方法。這使得調用者可以精確控制用於IO的線程。

實踐中的一個示例是在Kestrel Libuv傳輸中,其中IO回調在專用事件迴圈線程上運行。

PipeReader模式的其他好處:

  • 一些底層系統支持“無緩衝等待”,即,在底層系統中實際可用數據之前,永遠不需要分配緩衝區。例如,在帶有epoll的Linux上,可以等到數據準備好之後再實際提供緩衝區來進行讀取。這避免了具有大量線程等待數據的問題不會立即需要保留大量記憶體。
  • 預設情況下Pipe,可以輕鬆地針對網路代碼編寫單元測試,因為解析邏輯與網路代碼分離,因此單元測試僅針對記憶體緩衝區運行解析邏輯,而不是直接從網路中消耗。它還可以輕鬆測試那些難以測試發送部分數據的模式。ASP.NET Core使用它來測試Kestrel的http解析器的各個方面。
  • 允許將底層OS緩衝區(如Windows上的Registered IO API)暴露給用戶代碼的系統非常適合管道,因為緩衝區始終由PipeReader實現提供。

其他相關類型

作為製作System.IO.Pipelines的一部分,我們還添加了許多新的原始BCL類型:

  • MemoryPool<T>IMemoryOwner<T>MemoryManager<T> - .NET Core 1.0添加了ArrayPool<T>,在.NET Core 2.1中,我們現在有一個更通用的抽象,適用於任何工作的池Memory<T>。這提供了一個可擴展點,允許您插入更高級的分配策略以及控制緩衝區的管理方式(例如,提供預先固定的緩衝區而不是純托管的陣列)。
  • IBufferWriter<T> - 表示用於寫入同步緩衝數據的接收器。(PipeWriter實現這個)
  • IValueTaskSource - ValueTask<T>自.NET Core 1.1以來就已存在,但在.NET Core 2.1中獲得了一些超級許可權,允許無分配的等待非同步操作。有關詳細信息,請參閱https://github.com/dotnet/corefx/issues/27445。

如何使用管道?

API存在於System.IO.Pipelines nuget包中。

主要包含一個Pipe對象,它有一個Writer屬性和Reader屬性。

var pipe = new Pipe();
var writer = pipe.Writer;
var reader = pipe.Reader; 

Writer對象

Writer對象用於從數據源讀取數據,將數據寫入管道中;它對應業務中的"讀"操作。

var content = Encoding.Default.GetBytes("hello world");
var data = new Memory<byte>(content);
var result = await writer.WriteAsync(data);

另外,它也有一種使用Pipe申請Memory的方式

var buffer = writer.GetMemory(512);
content.CopyTo(buffer);
writer.Advance(content.Length);
var result = await writer.FlushAsync(); 

Reader對象

Reader對象用於從管道中獲取數據源,它對應業務中的"用"操作。

首先獲取管道的緩衝區:

var result = await reader.ReadAsync();
var buffer = result.Buffer;

這個Buffer是一個ReadOnlySequence<byte>對象,它是一個相當好的動態記憶體對象,並且相當高效。它本身由多段Memory<byte>組成,查看Memory段的方法有:

IsSingleSegment: 判斷是否只有一段Memory<byte>
First: 獲取第一段Memory<byte>
GetEnumerator: 獲取分段的Memory<byte>
它從邏輯上也可以看成一段連續的Memory<byte>,也有類似的方法:

Length: 整個數據緩衝區長度
Slice: 分割緩衝區
CopyTo: 將內容複製到Span中
ToArray: 將內容複製到byte[]中
另外,它還有一個類似游標的位置對象SequencePosition,可以從其Position相關函數中使用,這裡就不多介紹了。

這個緩衝區解決了"數據讀不夠"的問題,一次讀取的不夠下次可以接著讀,不用緩衝區的動態分配,高效的記憶體管理方式帶來了良好的性能,好用的介面是我們能更關註業務。

獲取到緩衝區後,就是使用緩衝區的數據

var data = buffer.ToArray();

使用完後,告訴PIPE當前使用了多少數據,下次接著從結束位置後讀起

reader.AdvanceTo(buffer.GetPosition(4));

這是一個相當實用的設計,它解決了"讀了就得用"的問題,不僅可以將不用的數據下次再使用,還可以實現Peek的操作,只讀但不改變游標。 

交互

除了"讀"和"用"操作外,它們之間還需要一些交互,例如:

讀過程中數據源不可用,需要停止使用
使用過程中業務結束,需要中止數據源。
Reader和Writer都有一個Complete函數,用於通知結束:

reader.Complete();
writer.Complete();

在Writer寫入和Reader讀取時,會獲得一個結果

FlushResult result = await writer.FlushAsync();
ReadResult result = await reader.ReadAsync();

它們都有一個IsComplete屬性,可以根據它是否為true判斷是否已經結束了讀和寫的操作。 

取消

在寫入和讀取的時候,也可以傳入一個CancellationToken,用於取消相應的操作。

writer.FlushAsync(CancellationToken.None);
reader.ReadAsync(CancellationToken.None);

如果取消成功,對應的Result的IsCanceled則為true

 


轉載請標明本文來源:https://www.cnblogs.com/yswenli/p/11810317.html
更多內容歡迎Star、Fork我的的github:https://github.com/yswenli/
如果發現本文有什麼問題和任何建議,也隨時歡迎交流~

 

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一個基於Net Core3.0的WPF框架Hello World實例 [toc] 1.創建WPF解決方案 1.1 創建Net Core版本的WPF工程 1.2 指定項目名稱,路徑,解決方案名稱 2. 依賴庫和4個程式文件介紹 2.1 框架依賴庫 依賴Microsoft.NETCore.App跟Mic ...
  • 什麼是基於角色的授權? 當涉及ASP.NET Core授權時,我們有兩種選擇,基於角色和基於策略(也有基於聲明的,但那隻是基於策略的一種特殊類型)。 基於角色的授權最初是在ASP.NET(ASP.NET Core之前)中引入,這是一種限制對資源訪問的聲明性方法。 開發人員可以指定用戶... ...
  • 前言 在之前鼓搗過一次基礎工程 April.WebApi 後,就考慮把常用的類庫打包做成一個公共類庫,這樣既方便維護也方便後續做快速開發使用,倉庫地址: "April.Util_github" , "April.Util_gitee" ,後續會繼續推出基於Util的基礎工程(包含許可權相關)以及如果代 ...
  • 出處:http://www.hzhcontrols.com/原文:http://www.hzhcontrols.com/blog-149.html本文版權歸www.hzhcontrols.com所有歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利 ...
  • string類型在我們實際項目開發中是一個最使用的類型,sting是一個引用類型,但是在實際使用中又有其特殊性所在,他是一個是:密封類、只讀類。在使用過程需要註意:避免不必要的記憶體開銷、避免不必要的裝箱操作。 ...
  • Aspose.Words for .NET是用於執行各種文檔管理和操作任務,支持生成,修改,轉換,呈現和列印文檔,而無需在跨平臺應用程式中直接使用Microsoft Word。同時支持所有流行的Word處理文件格式,並允許將Word文檔導出或轉換為固定佈局文件格式和最常用的圖像、多媒體格式。 我們很 ...
  • 前言 隨著 .NET Core 3.1 的第二個預覽版本發佈,微軟正式將 C++/CLI 移植到 .NET Core 上,從此可以使用 C++ 編寫 .NET Core 的程式了。 由於目前僅有 MSVC 支持編譯此類混合代碼,並且由於涉及到非托管代碼,因此 C++/CLI 目前不能跨平臺,只支持 ...
  • 關於String為值類型還是引用類型的討論一直沒有平息,最近一直在研究性能方面的問題,今天再次將此問題進行一次明確。希望能給大家帶來點幫助,如果有錯誤請指出。 來看下麵例子: //值類型 int a = 1; int b = a; a = 2; Console.WriteLine("a is {0} ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...