C#多線程(一)線程基礎篇

来源:https://www.cnblogs.com/xiaolipro/archive/2022/11/13/16885592.html
-Advertisement-
Play Games

線程基礎 視頻已經發佈到B站 參考文章: 《Threading in C# 》(Joseph Albahari)https://www.albahari.com/threading/ 《Threading in C# 》中文翻譯(GKarch ):https://blog.gkarch.com/to ...


線程基礎

視頻已經發佈到B站

參考文章:

《Threading in C# 》(Joseph Albahari)https://www.albahari.com/threading/

《Threading in C# 》中文翻譯(GKarch ):https://blog.gkarch.com/topic/threading.html

《圖解系統》(小林coding):https://xiaolincoding.com/os/

一、概念

並行(parallel):同一時間,多個線程/進程同時執行。多線程的目的就是為了並行,充分利用cpu多個核心,提高程式性能

線程(threading):線程是操作系統能夠進行 運算調度的最小單位,是進程的實際運作單位。
一條線程指的是進程中一個單一順序的控制流,一個進程中可以並行多個線程,每條線程並行執行不同的任務。

進程(process):進程是操作系統進行資源分配的基本單位。多個進程並行的在電腦上執行,多個線程並行的在進程中執行,
進程之間是隔離的,線程之間共用堆,私有棧空間。

CLR 為每個線程分配各自獨立的 棧(stack) 空間,因此局部變數是線程獨立的。

static void Main()
{
  new Thread(Go).Start();  // 在新線程執行Go()
  Go();  // 在主線程執行Go()
}

static void Go()
{
  // 定義和使用局部變數 - 'cycles'
  for (int cycles = 0; cycles < 5; cycles++) Console.Write ('?');
}

變數cycles的副本是分別線上程各自的棧中創建,因此會輸出 10 個問號

??????????

線程可以通過對同一對象的引用來共用數據。例如:

static bool done = false;

static void Main()
{
  new Thread (tt.Go).Start(); // A
  Go(); // B
}

static void Go()
{
   if (!done) { 
      Console.WriteLine ("Done");
      done = true;
   }
}

這個例子引出了一個關鍵概念 線程安全(thread safety) ,由於併發,” Done “ 有可能會被列印兩次

通過簡單的加鎖操作:在讀寫公共欄位時,獲得一個 排它鎖(互斥鎖,exclusive lock ) ,c#中使用lock即可生成 臨界區(critical section)

static readonly object locker = new object();
...
static void Go()
{
  lock (locker) // B
  {
    if (!done) { 
      Console.WriteLine ("Done");
      done = true;
    }
  }
}

臨界區(critical section):在同一時刻只有一個線程能進入,不允許併發。當有線程進入臨界區段時,其他試圖進入的線程或是進程必須 等待或阻塞(blocking)

線程阻塞(blocking):指一個線程在執行過程中暫停,以等待某個條件的觸發來解除暫停。阻塞狀態的線程不會消耗CPU資源

掛起(Suspend):和阻塞非常相似,在虛擬記憶體管理的操作系統中,通常會把阻塞狀態的進程的物理記憶體空間換出到硬碟,等需要再次運行的時候,再從硬碟換入到物理記憶體。描述進程沒有占用實際的物理記憶體空間的情況,這個狀態就是掛起狀態

可以通過調用Join方法等待線程執行結束,例如:

static void Main()
{
  Thread t = new Thread(Go);
  t.Start();
  t.Join();  // 等待線程 t 執行完畢
  Console.WriteLine ("Thread t has ended!");
}

static void Go()
{
  for (int i = 0; i < 1000; i++) Console.Write ("y");
}

也可以使用Sleep使當前線程阻塞一段時間:

Thread.Sleep (500);  // 阻塞 500 毫秒

Thread.Sleep(0)會立即釋放當前的時間片(time slice),將 CPU 資源出讓給其它線程。Framework 4.0的Thread.Yield()方法與其大致相同,不同的是Yield()只會出讓給運行在相同處理器核心上的其它線程。

Sleep(0)和Yield在調整代碼性能時偶爾有用,它也是一個很好的診斷工具,可以用於找出線程安全(thread safety)的問題。如果在你代碼的任意位置插入Thread.Yield()會影響到程式,
基本可以確定存在 bug。

二、原理

硬體結構

https://xiaolincoding.com/os/1_hardware/how_cpu_run.html#圖靈機的工作方式

運行時

  線程在內部由一個 線程調度器(thread scheduler) 管理,一般 CLR 會把這個任務交給操作系統完成。線程調度器確保所有活動的線程能夠分配到適當的執行時間,並且保證那些處於等待或阻塞狀態(例如,等待排它鎖或者用戶輸入)的線程不消耗CPU時間。

  在單核電腦上,線程調度器會進行 時間切片(time-slicing) ,快速的在活動線程中切換執行。在 Windows 操作系統上,一個時間片通常在十幾毫秒(譯者註:預設 15.625ms),遠大於 CPU 線上程間進行上下文切換的開銷(通常在幾微秒區間)。

  在多核電腦上,多線程的實現是混合了時間切片和 真實的併發(genuine concurrency) ,不同的線程同時運行在不同的 CPU 核心上。仍然會使用到時間切片,因為操作系統除了要調度其它的應用,還需要調度自身的線程。

  線程的執行由於外部因素(比如時間切片)被中斷稱為 被搶占(preempted)。在大多數情況下,線程無法控制其在什麼時間,什麼代碼塊被搶占。

  多線程同樣也會帶來缺點,最大的問題在於它提高了程式的複雜度。使用多個線程本身並不複雜,複雜的是線程間的交互(共用數據)如何保證安全。無論線程間的交互是否有意為之,都會帶來較長的開發周期,以及帶來間歇的、難以重現的 bug。因此,最好保證線程間的交互儘可能少,並堅持簡單和已被證明的多線程交互設計。

  當頻繁地調度和切換線程時(且活動線程數量大於 CPU 核心數),多線程會增加系統資源和 CPU 的開銷,線程的創建和銷毀也會增加開銷。多線程並不總是能提升程式的運行速度,如果使用不當,反而可能降低速度。

三、基礎

創建與啟動

使用Thread類的構造方法來創建線程,支持以下兩種委托

public delegate void ThreadStart();

public delegate void ParameterizedThreadStart (object? obj);

關於Thread構造重載方法參數 maxStackSize,不建議使用

https://stackoverflow.com/questions/5507574/maximum-thread-stack-size-net

public void 創建一個線程()
{
    var t = new Thread(Go);  // 開一個線程t
    t.Start();  // 啟動t線程,執行Go方法
    
    Go();  // 主線程執行Go方法
}

void Go()
{
    _testOutputHelper.WriteLine("hello world!");
}

每一個線程都有一個 Name 屬性,我們可以設置它以便於調試。線程的名字只能設置一次,再次修改會拋出異常。

public void 線程命名()
{
    var t = new Thread(Go);  // 開一個線程t
    t.Name = "worker";
    t.Start();  // 啟動t線程,執行Go方法
    
    Go();  // 主線程執行Go方法
}

void Go()
{
    // Thread.CurrentThread屬性會返回當前執行的線程
    _testOutputHelper.WriteLine(Thread.CurrentThread.Name + " say: hello!");
}

傳遞參數

Thread類的Start方法重載支持向thread實例傳參

public void Start(object? parameter)

參數被lambda表達式捕獲,傳遞給Go方法

public void 創建一個線程()
{
    var t = new Thread(msg => Go(msg));  // 開一個線程t
    t.Start("hello world!");  // 啟動t線程,執行Go方法
    
    Go("main thread say:hello world!");  // 主線程執行Go方法
}

void Go(object? msg)
{
    _testOutputHelper.WriteLine(msg?.ToString());
}

請務必註意,不要在啟動線程之後誤修改被捕獲變數(captured variables)

public void 閉包問題()
{
    for (int i = 0; i < 10; i++)
    {
        new Thread (() => Go(i)).Start();
    }
}

前臺/後臺線程

預設情況下,顯式創建的線程都是前臺線程(foreground threads)。只要有一個前臺線程在運行,程式就可以保持存活不結束。
當一個程式中所有前臺線程停止運行時,仍在運行的所有後臺線程會被強制終止。

這裡說的 顯示創建,指的是通過new Thread()創建的線程

非預設情況,指的是將Thread的IsBackground屬性設置為true

static void Main (string[] args)
{
	Thread worker = new Thread ( () => Console.ReadLine() );
	if (args.Length > 0) worker.IsBackground = true;
	worker.Start();
}

當進程以強制終止這種方式結束時,後臺線程執行棧中所有finally塊就會被避開。如果程式依賴finally(或是using)塊來執行清理工作,例如釋放資料庫/網路連接或是刪除臨時文件,就可能會產生問題。
為了避免這種問題,在退出程式時可以顯式的等待這些後臺線程結束。有兩種方法可以實現:

  • 如果是顯式創建的線程,線上程上調用Join阻塞。
  • 如果是使用線程池線程,使用信號構造,如事件等待句柄。

在任何一種情況下,都應指定一個超時時間,從而可以放棄由於某種原因而無法正常結束的線程。這是後備的退出策略:我們希望程式最後可以關閉,而不是讓用戶去開任務管理器(╯-_-)╯╧══╧

線程的 前臺/後臺狀態 與它的 優先順序/執行時間的分配無關。

異常處理

當線程開始運行後,其內部發生的異常不會拋到外面,更不會被外面的try-catch-finally塊捕獲到。

void 異常捕獲()
{
    try
    {
        new Thread(Go).Start();  // 啟動t線程,執行Go方法
    }
    catch (Exception e)
    {
        _testOutputHelper.WriteLine(e.Message);
    }
}
    
void Go() => throw null!;  // 拋出空指針異常

解決方案是將異常處理移到Go方法中:自己的異常,自己解決

static void Go()
{
  try
  {
    // ...
    throw null;    // 異常會在下麵被捕獲
    // ...
  }
  catch (Exception ex)
  {
    // 一般會記錄異常,或通知其它線程我們遇到問題了
    // ...
  }
}

AppDomain.CurrentDomain.UnhandledException 會對所有未處理的異常觸發,因此它可以用於集中記錄線程發生的異常,但是它不能阻止程式退出。

void UnhandledException()
{
    AppDomain.CurrentDomain.UnhandledException += HandleUnHandledException;
    new Thread(Go).Start();  // 啟動t線程,執行Go方法
}

void HandleUnHandledException(object sender, UnhandledExceptionEventArgs eventArgs)
{
    _testOutputHelper.WriteLine("我發現異常了");
}

並非所有線程上的異常都需要處理,以下情況,.NET Framework 會為你處理:

  • 非同步委托(APM)
  • BackgroundWorker(EAP)
  • 任務並行庫(TPL)

中斷與中止

所有阻塞方法Wait(), Sleep() or Join(),在阻塞條件永遠無法被滿足且沒有指定超時時間的情況下,線程會陷入永久阻塞。

有兩個方式可以實現強行結束:中斷、中止

中斷(Interrupt)

在一個阻塞線程上調用Thread.Interrupt會強制釋放它,並拋出ThreadInterruptedException異常,與上文的一樣,這個異常同樣不會拋出

var t = new Thread(delegate()
{
    try
    {
        Thread.Sleep(Timeout.Infinite);  // 無期限休眠
    }
    catch (ThreadInterruptedException)
    {
        _testOutputHelper.WriteLine("收到中斷信號");
    }

    _testOutputHelper.WriteLine("溜溜球~");
});
t.Start();
Thread.Sleep(3000);  // 睡3s後中斷線程t
t.Interrupt();

如果在非阻塞線程上調用Thread.Interrupt,線程會繼續執行直到下次被阻塞時,拋出ThreadInterruptedException。這避免了以下這樣的代碼:

if ((worker.ThreadState & ThreadState.WaitSleepJoin) > 0)  // 線程不安全的
{
    worker.Interrupt();
}

  隨意中斷一個線程是極度危險的,這可能導致調用棧上的任意方法(框架、第三方包)收到意外的中斷,而不僅僅是你自己的代碼!只要調用棧上發生阻塞(因為使用同步構造),
中斷就會發生在這,如果在設計時沒有考慮中斷(在finally塊中執行適當清理),線程中的對象就可能成為一個奇怪狀態(不可用或未完全釋放)。

  如果是自己設計的阻塞,完全可以用 信號構造(signal structure) 或者 取消令牌(cancellation tokens) 來達到相同效果,且更加安全。如果希望結束他人代碼導致的阻塞,Abort總是更合適

中止(Abort)

通過Thread.Abort方法也可以使阻塞的線程被強制釋放,效果和調用Interrupt類似,不同的是它拋出的是ThreadAbortException的異常。另外,這個異常會在catch塊結束時被重新拋出(試圖更好的結束線程)。

Thread t = new Thread(delegate()
{
    try
    {
        while (true)
        {
        }
    }
    catch (ThreadAbortException)
    {
        _testOutputHelper.WriteLine("收到中止信號");
    }
    // 這裡仍然會繼續拋出ThreadAbortException,以保證此線程真正中止
});

_testOutputHelper.WriteLine(t.ThreadState.ToString()); // Unstarted 狀態

t.Start();
Thread.Sleep(1000);
_testOutputHelper.WriteLine(t.ThreadState.ToString()); // Running 狀態

t.Abort();
_testOutputHelper.WriteLine(t.ThreadState.ToString()); // AbortRequested 狀態

t.Join();
_testOutputHelper.WriteLine(t.ThreadState.ToString()); // Stopped 狀態

除非Thread.ResetAbort在catch塊中被調用,在此之前,線程狀態(thread state) 是AbortRequested,調用Thread.ResetAbort來阻止異常被自動重新拋出之後,
線程重新進入Running狀態(從這開始,它可能被再次中止)

static void Main()
{
  Thread t = new Thread (Work);
  t.Start();
  Thread.Sleep (1000); t.Abort();
  Thread.Sleep (1000); t.Abort();
  Thread.Sleep (1000); t.Abort();
}

static void Work()
{
  while (true)
  {
    try { while (true); }
    catch (ThreadAbortException) { Thread.ResetAbort(); }
    Console.WriteLine ("我沒死!");
  }
}

Thread.Abort在NET 5被棄用了:https://learn.microsoft.com/zh-cn/dotnet/core/compatibility/core-libraries/5.0/thread-abort-obsolete

未處理的ThreadAbortException是僅有的兩個不會導致應用程式關閉的異常之一,另一個是AppDomainUnloadException。

Abort幾乎對處於任何狀態的線程都有效:Running、Blocked、Suspended以及Stopped。然而,當掛起的線程被中止時,會拋出ThreadStateException異常。中止會直到線程之後恢復時才會起作用。

try { suspendedThread.Abort(); }
catch (ThreadStateException) { suspendedThread.Resume(); }
// 現在 suspendedThread 才會中止

Interrupt和Abort最大的不同是:調用Interrupt線程會繼續工作直到下次被阻塞時拋出異常,而調用Abort會立即線上程正在執行的地方拋出異常(非托管代碼除外)。

這將導致一個新的問題:.NET Framework 中的代碼可能會被中止,而且不是安全的中止。如果中止發生在FileStream被構造期間,很可能造成一個非托管文件句柄會一直保持打開直到應用程式域結束。

協作取消模式

正如上面所說Interrupt和Abort總是危險的,替代方案就是實現一個協作模式(cooperative ):工作線程定期檢查一個用於指示是否應該結束的標識,發起者只需要設置這個標識,等待工作線程響應,即可取消線程執行。

Framework 4.0 提供了兩個類CancellationTokenSourceCancellationToken來完成這個模式:

  • CancellationTokenSource定義了Cancel方法。
  • CancellationToken定義了IsCancellationRequested屬性和ThrowIfCancellationRequested方法。
void 取消令牌()
{
    var cancelSource = new CancellationTokenSource();
    cancelSource.CancelAfter(3000);
    var t = new Thread(() => Work(cancelSource.Token));
    t.Start();
    t.Join();
}
void Work(CancellationToken cancelToken)
{
    while (true)
    {
        cancelToken.ThrowIfCancellationRequested();
        // ...
        Thread.Sleep(1000);
    }
}

四、非同步編程模式

MSDN文檔:https://learn.microsoft.com/zh-cn/dotnet/standard/asynchronous-programming-patterns/

非同步編程模型(APM)

非同步編程模型(Asynchronous Programming Model),提出於.NET Framework 1.x 的時代,基於IAsyncResult介面實現類似BeginXXX和EndXXX的方法。

APM是建立在委托之上的,Net Core中的委托 不支持非同步調用,也就是 BeginInvoke 和 EndInvoke 方法。

void APM()
{
    var uri = new Uri("https://www.albahari.com/threading/part3.aspx");
    Func<Uri, int> f = CalcUriStringCount;
    var res = f.BeginInvoke(uri, null, null);
    // do something
    _testOutputHelper.WriteLine("我可以做別的事情");
    _testOutputHelper.WriteLine("共下載字元數:" + f.EndInvoke(res));
}
int CalcUriStringCount(Uri uri)
{
    var client = new WebClient();
    var res = client.DownloadString(uri);
    return res.Length;
}

EndInvoke會做三件事:

  1. 如果非同步委托還沒有結束,它會等待非同步委托執行完成。
  2. 它會接收返回值(也包括refout方式的參數)。
  3. 它會向調用線程拋出未處理的異常。

不要因為非同步委托調用的方法沒有返回值就不調用EndInvoke,因為這將導致其內部的異常無法被調用線程察覺。MSDN文檔中明確寫了 “無論您使用何種方法,都要調用 EndInvoke 來完成非同步調用。”

BeginInvoke也可以指定一個回調委托。這是一個在完成時會被自動調用的、接受IAsyncResult對象的方法。

BeginInvoke的最後一個參數是一個用戶狀態對象,用於設置IAsyncResultAsyncState屬性。它可以是需要的任何東西,在這個例子中,我們用它向回調方法傳遞method委托,這樣才能夠在它上面調用EndInvoke

var uri = new Uri("https://www.albahari.com/threading/part3.aspx");
Func<Uri, int> func = CalcUriStringCount;
var res = func.BeginInvoke(uri, new AsyncCallback(res =>
{
    var target = res.AsyncState as Func<string, int>;
    _testOutputHelper.WriteLine("共下載字元數:" + target!.EndInvoke(res));
    _testOutputHelper.WriteLine("非同步狀態:" + res.AsyncState);
}), func);
// do something
_testOutputHelper.WriteLine("我可以做別的事情");
func.EndInvoke(res);

基於事件的非同步模式(EAP)

基於事件的非同步模式(event-based asynchronous pattern),EAP 是在 .NET Framework 2.0 中提出的,讓類可以提供多線程的能力,而不需要使用者顯式啟動和管理線程。這種模式具有以下能力:

  • 協作取消模型(cooperative cancellation model)
  • 線程親和性(thread affinity)
  • 將異常轉發到完成事件(forwarding exceptions)

這個模式本質上就是:類提供一組成員,用於在內部管理多線程,類似於下邊的代碼:

// 這些成員來自於 WebClient 類:

public byte[] DownloadData (Uri address);    // 同步版本
public void DownloadDataAsync (Uri address);
public void DownloadDataAsync (Uri address, object userToken);
public event DownloadDataCompletedEventHandler DownloadDataCompleted;

public void CancelAsync (object userState);  // 取消一個操作
public bool IsBusy { get; }                  // 指示是否仍在運行

當調用基於EAP模式的類的XXXAsync方法時,就開始了一個非同步操作,EAP模式是基於APM模式之上的。

var client = new WebClient();
client.DownloadStringCompleted += (sender, args) =>
{
    if (args.Cancelled) _testOutputHelper.WriteLine("已取消");
    else if (args.Error != null) _testOutputHelper.WriteLine("發生異常:" + args.Error.Message);
    else
    {
        _testOutputHelper.WriteLine("共下載字元數:" + args.Result.Length);
        // 可以在這裡更新UI。。
    }
};
_testOutputHelper.WriteLine("我在做別的事情");
client.DownloadStringAsync(new Uri("https://www.albahari.com/threading/part3.aspx"));

BackgroundWorker是命名空間System.ComponentModel中的一個工具類,用於管理工作線程。它可以被認為是一個 EAP 的通用實現,在EAP功能的基礎上額外提供了:

  • 報告工作進度的協議
  • 實現了IComponent介面

另外BackgroundWorker使用了線程池,意味著絕不應該在BackgroundWorker線程上調用Abort

void 工作進度報告()
{
    worker = new BackgroundWorker();
    worker.WorkerReportsProgress = true;  // 支持進度報告
    worker.WorkerSupportsCancellation = true;  // 支持取消
    worker.DoWork += DoWoker;
    worker.ProgressChanged += (_, args) => _testOutputHelper.WriteLine($"當前進度:{args.ProgressPercentage}%");
    worker.RunWorkerCompleted += (sender, args) =>
    {
        if (args.Cancelled) _testOutputHelper.WriteLine("工作線程已被取消");
        else if (args.Error != null) _testOutputHelper.WriteLine("工作線程發生異常: " + args.Error);
        else _testOutputHelper.WriteLine("任務完成,結果: " + args.Result); // Result來自DoWork
    };
    
    worker.RunWorkerAsync();
}

private void DoWoker(object? sender, DoWorkEventArgs e)
{
    for (int i = 0; i < 100; i+= 10)
    {
        if (worker.CancellationPending)
        {
            e.Cancel = true;
            return;
        }
        worker.ReportProgress(i);  // 上報進度
        Thread.Sleep(1000);  // 模擬耗時任務
    }

    e.Result = int.MaxValue;  // 這個值會回傳給RunWorkerCompleted
}

基於任務的非同步模式 (TAP)

從 .NET Framework 4 開始引入

五、拓展知識

小林coding:https://xiaolincoding.com/os/4_process/process_base.html#進程的控制結構

線程優先順序

線程的Priority屬性決定了相對於操作系統中的其它活動線程,它可以獲得多少CPU 時間片(time slice)

優先順序依次遞增,在提升線程優先順序前請三思,這可能會導致其它線程的 資源饑餓(resource starvation)

enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }

提升線程的優先順序並不等於直接優先,因為線程還受進程優先順序影響,因此還需要使用System.Diagnostics中的Process類

using (Process p = Process.GetCurrentProcess())
{
    p.PriorityClass = ProcessPriorityClass.High;
}

ProcessPriorityClass.High是一個略低於最高優先順序Realtime的級別。將一個進程的優先順序設置為Realtime是通知操作系統,我們絕不希望該進程將 CPU 時間出讓給其它進程。
如果你的程式誤入一個死迴圈,會發現甚至是操作系統也被鎖住了,就只好去按電源按鈕了o(>_<)o 正是由於這一原因,High 通常是實時程式的最好選擇。

什麼是進程退出?

進程退出一般出現在以下幾種情況:

  • 正常退出,進程執行完任務。

  • 錯誤退出,進程遇到不可繼續運行的錯誤(發生異常未捕獲導致程式退出)

  • 被操作系統終止,進程本身有問題,比如進程企圖訪問不屬於自己的記憶體地址

  • 被其它進程終止,比如通過資源管理器我們可以選擇終止掉某個進程

以上只有前兩種情況是進程自願退出的,因此,總體上可以分為三類:進程自願退出,操作系統終止進程以及進程終止進程。

main()執行結束時會自動隱式調用exit(),windows下叫ExitProcess。中止整個程式的執行,把控制返還給操作系統,並返回一個整數值,通常0表示正常終止,非0表示異常終止,這個值將會返回給操作系統。

windows中通過任務管理器,linux中通過kill去殺掉一個進程,其資源是否會釋放?

會。進程的特征之一就是動態性,其生存周期就是產生到消亡。當發生進程終止後,調用進程終止原語,從PCB匯流排中將其刪除,將PCB結構歸還給系統,釋放該進程的資源給其父進程或者操作系統。

但不完全會。如果用戶強行終止了.NET 進程,所有線程都會被當作後臺線程一般丟棄,有的資源沒來得及釋放,需要等待一段時間

Process類有以下兩種方法:

  • CloseMainWindow:向主視窗消息迴圈發送wm_quit消息以請求關閉進程,這使程式有機會重新調用其子視窗和內核對象。
  • Kill:強制終止進程,就像在任務管理器中終止進程一樣。

我們可以使用visual studio組件:記憶體分析器 分析發現幾乎在所有情況下,kill速度更快,但通過檢查實時記憶體圖可以發現其“根引用”和“實例引用”釋放的記憶體更少。


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

-Advertisement-
Play Games
更多相關文章
  • ##SpringBoot集成JWT(極簡版) ###在WebConfig配置類中設置介面統一首碼 import org.springframework.context.annotation.Configuration; import org.springframework.web.bind.anno ...
  • 這一篇問文章主要介紹元組的相關知識。 元組:不可修改的序列 與列表一樣,元組也是序列,唯一的差別在於元組是不能修改的(同樣的,字元串也不能修改)。 元組的語法很簡單。 >>> >>> 1, 2, 3 (1, 2, 3) >>> (1, 2, 3) (1, 2, 3) >>> >>> () () >> ...
  • 大家好,我是三友~~ 之前有小伙伴私信我說看源碼的時候感覺源碼很難,不知道該怎麼看,其實這有部分原因是因為沒有弄懂一些源碼實現的套路,也就是設計模式,所以本文我就總結了9種在源碼中非常常見的設計模式,併列舉了很多源碼的實現例子,希望對你看源碼和日常工作中有所幫助。 單例模式 單例模式是指一個類在一個 ...
  • 目錄 一.glut 簡介 二.猜你喜歡 零基礎 OpenGL ES 學習路線推薦 : OpenGL ES 學習目錄 >> OpenGL ES 基礎 零基礎 OpenGL ES 學習路線推薦 : OpenGL ES 學習目錄 >> OpenGL ES 特效 零基礎 OpenGL ES 學習路線推薦 : ...
  • 線程池無論是工作還是面試都是必備的技能,但是很多人對於線程池的實現原理卻一知半解,並不瞭解線程池內部的工作原理,今天一燈就帶大家一塊剖析線程池底層實現原理。 ...
  • aspnetcore上傳圖片也就是上傳文件有兩種方式,一種是通過form-data,一種是binary。 先介紹第一種form-data: 該方式需要顯示指定一個IFormFile類型,該組件會動態通過打開一個windows視窗選擇文件 及圖片。 postman演示如上,代碼如下: [HttpPos ...
  • 一:背景 1.講故事 前幾天 B 站上有位朋友讓我從高級調試的角度來解讀下 .NET7 新出來的 AOT,畢竟這東西是新的,所以這一篇我就簡單摸索一下。 二:AOT 的幾個問題 1. 如何在 .NET7 中開啟 AOT 功能 在 .NET7 中開啟 AOT 非常方便,先來段測試代碼。 interna ...
  • 前言 拋開死鎖不談,只聊性能問題,儘管鎖總能粗暴的滿足同步需求,但一旦存在競爭關係,意味著一定會有線程被阻塞,競爭越激烈,被阻塞的線程越多,上下文切換次數越多,調度成本越大,顯然在高併發的場景下會損害性能。在高併發高性能且要求線程安全的述求下,無鎖構造(非阻塞構造)閃亮登場。 參考文檔: C# - ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...