本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:在C#编程中,线程是实现多任务并行处理的基础,能显著提升程序的执行效率和响应能力。本学习资料“C#线程学习.zip”详细讲解了C#中线程的创建、管理与同步机制,涵盖线程池、异步编程模型、任务并行库(TPL)以及HttpClient的异步使用。通过实例演示和代码示例,帮助开发者深入理解多线程编程的核心概念与实战技巧,为构建高性能、稳定的应用程序打下基础。
C Sharp线程学习.zip

1. C#线程基础与创建

在现代软件开发中,多线程编程是构建高性能、响应式应用程序的关键技术之一。本章旨在引导读者理解线程的基本概念,并掌握如何在C#中创建和管理线程。

1.1 线程与进程的基本区别

在操作系统中, 进程 是资源分配的基本单位,它包含独立的内存空间、代码、数据和系统资源。而 线程 是CPU调度的基本单位,一个进程可以包含多个线程,这些线程共享进程的资源,但各自拥有独立的执行路径。

对比项 进程(Process) 线程(Thread)
资源开销 大,独立内存空间 小,共享进程资源
通信机制 需要进程间通信(IPC) 直接共享内存
切换效率 较低 较高
安全性 更高,资源隔离 线程间错误可能影响整个进程

通过理解这些差异,我们可以更有针对性地选择使用线程来提升程序性能。

2. Thread类与线程启动方法

在C#中, Thread 类是多线程编程的核心类之一,位于 System.Threading 命名空间下。通过 Thread 类,开发者可以创建、启动、控制和终止线程,从而实现并行处理任务,提升程序性能。本章将从 Thread 类的结构入手,详细讲解线程的创建与启动方法、参数传递方式以及异常处理机制,帮助读者掌握线程的基本控制手段。

2.1 Thread类的基本结构

Thread 类是.NET Framework中用于管理线程的基础类,提供了丰富的属性和方法来控制线程的生命周期。掌握其结构是进行多线程编程的第一步。

2.1.1 线程对象的实例化方式

在C#中,创建线程对象主要通过 Thread 类的构造函数来完成。构造函数接受一个委托作为线程入口点,常见的委托类型是 ThreadStart ParameterizedThreadStart

// 使用 ThreadStart 委托(无参数)
Thread thread1 = new Thread(new ThreadStart(MyMethod));

// 使用 ParameterizedThreadStart 委托(带参数)
Thread thread2 = new Thread(new ParameterizedThreadStart(MyMethodWithParam));
构造函数说明:
  • Thread(ThreadStart start) :用于创建一个不带参数的线程。
  • Thread(ParameterizedThreadStart start) :用于创建一个带参数的线程。
线程对象实例化流程图(mermaid):
graph TD
    A[创建线程对象] --> B{选择委托类型}
    B -->|ThreadStart| C[无参数线程]
    B -->|ParameterizedThreadStart| D[带参数线程]
示例代码:
using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Thread t1 = new Thread(PrintNumbers);
        t1.Start();

        Console.WriteLine("Main thread continues...");
    }

    static void PrintNumbers()
    {
        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine($"Number: {i}");
            Thread.Sleep(500);
        }
    }
}

逐行分析:

  • Thread t1 = new Thread(PrintNumbers); :创建一个线程对象 t1 ,其入口方法为 PrintNumbers
  • t1.Start(); :启动线程 t1 ,开始执行 PrintNumbers 方法。
  • PrintNumbers() :线程执行的逻辑,输出0~4的数字,并每次休眠500毫秒。
  • Console.WriteLine("Main thread continues..."); :主线程继续执行输出。
说明:
  • 该示例演示了如何创建一个线程并启动它,主线程与子线程并发执行。
  • Thread.Sleep(500) 用于模拟耗时操作。

2.1.2 线程入口方法的定义与参数传递

线程的入口方法是线程开始执行的起点,其签名必须匹配 ThreadStart ParameterizedThreadStart 委托。

示例:使用 ParameterizedThreadStart 传递参数
static void Main()
{
    Thread t = new Thread(PrintNumbersWithParam);
    t.Start(10);  // 传递参数
}

static void PrintNumbersWithParam(object param)
{
    int count = (int)param;
    for (int i = 0; i < count; i++)
    {
        Console.WriteLine($"Number: {i}");
        Thread.Sleep(300);
    }
}

参数传递说明:

  • Start(object parameter) :启动线程并传入一个对象参数。
  • 参数必须是 object 类型,使用前需进行类型转换。
  • 适合传递简单的参数,但不适合传递复杂对象,容易引发线程安全问题。
参数传递流程图(mermaid):
graph TD
    A[主线程调用 Start] --> B[传递参数]
    B --> C[子线程接收参数]
    C --> D[强制类型转换]
    D --> E[执行任务]

2.2 线程的启动与控制

创建线程后,需要使用 Start 方法启动线程,并通过 Sleep Join Abort Interrupt 等方法控制线程的行为。

2.2.1 启动线程:Start方法的使用

Start() 方法用于启动线程,一旦调用,线程将进入就绪状态,等待操作系统调度执行。

示例:
Thread t = new Thread(PrintNumbers);
t.Start();

说明:

  • Start() 方法只能调用一次,否则会抛出 ThreadStateException
  • 线程一旦启动,其生命周期由操作系统管理。

2.2.2 线程状态控制:Sleep、Join、Abort与Interrupt

Sleep方法
Thread.Sleep(1000);  // 当前线程休眠1秒
  • 作用 :让当前线程暂停执行指定时间(毫秒),让出CPU资源。
  • 应用场景 :模拟延迟、防止CPU占用过高。
Join方法
Thread t = new Thread(PrintNumbers);
t.Start();
t.Join();  // 等待线程t执行完成
Console.WriteLine("线程执行完毕");
  • 作用 :阻塞主线程,直到子线程执行完毕。
  • 用途 :确保主线程等待子线程完成后再继续执行。
Abort方法(已过时)
t.Abort();  // 终止线程(不推荐)
  • 说明 :强制终止线程,可能导致资源未释放或状态不一致。
  • 替代方案 :应使用取消标志或 CancellationToken 来优雅终止线程。
Interrupt方法
t.Interrupt();  // 打断线程的休眠状态
  • 作用 :如果线程处于等待、休眠或加入状态,将抛出 ThreadInterruptedException
  • 用途 :用于唤醒阻塞线程,实现线程间的通信。
线程控制流程图(mermaid):
graph TD
    A[启动线程 Start] --> B{线程状态}
    B -->|运行| C[执行任务]
    B -->|休眠| D[Thread.Sleep]
    B -->|阻塞| E[Thread.Join]
    B -->|中断| F[Thread.Interrupt]
    B -->|终止| G[Thread.Abort]

2.3 线程参数传递实践

参数传递是多线程开发中常见的需求,但如果不当使用,可能引发线程安全问题。

2.3.1 使用ParameterizedThreadStart传递参数

如前所述, ParameterizedThreadStart 允许传递一个 object 类型的参数,适合传递简单数据。

示例:
Thread t = new Thread(PrintNumbersWithParam);
t.Start(5);  // 传递数字5

2.3.2 参数封装与线程安全问题初探

当传递的是引用类型时,多个线程可能共享同一对象,从而引发数据竞争问题。

示例:不安全的参数共享
class Data
{
    public int Value { get; set; }
}

static void Main()
{
    Data data = new Data { Value = 0 };
    Thread t1 = new Thread(IncrementData);
    Thread t2 = new Thread(IncrementData);
    t1.Start(data);
    t2.Start(data);
    t1.Join();
    t2.Join();
    Console.WriteLine($"Final Value: {data.Value}");
}

static void IncrementData(object obj)
{
    Data data = obj as Data;
    for (int i = 0; i < 1000; i++)
    {
        data.Value++;
    }
}

输出结果:

  • Final Value 可能小于2000,因为两个线程同时修改 data.Value ,导致数据竞争。
解决方案:
  • 使用同步机制如 lock Interlocked Mutex 来保护共享资源。
  • 优化参数设计,避免共享可变对象。
安全封装参数示例:
class SafeData
{
    private int value = 0;
    public int GetValue()
    {
        return value;
    }

    public void Increment()
    {
        lock (this)
        {
            value++;
        }
    }
}

2.4 线程异常处理机制

线程中的异常处理与主线程不同,必须在子线程内部捕获异常,否则会导致程序崩溃。

2.4.1 捕获线程内部异常

Thread t = new Thread(() =>
{
    try
    {
        // 可能抛出异常的代码
        throw new Exception("线程异常");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"捕获线程异常:{ex.Message}");
    }
});
t.Start();
  • 说明 :在子线程内部使用 try-catch 捕获异常,避免异常传播到主线程。

2.4.2 使用全局异常处理策略

在应用程序级别设置全局异常处理器,捕获未处理的线程异常。

AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
    Console.WriteLine($"全局异常捕获:{args.ExceptionObject}");
};

Thread t = new Thread(() =>
{
    throw new Exception("未处理的线程异常");
});
t.Start();
  • 说明 UnhandledException 事件用于捕获未处理的异常,但不能恢复线程执行,只能记录日志或通知用户。
异常处理流程图(mermaid):
graph TD
    A[线程执行代码] --> B{是否发生异常}
    B -->|是| C[try-catch捕获]
    B -->|否| D[正常执行]
    C --> E[输出错误信息]
    B -->|未处理| F[触发全局异常处理器]
    F --> G[记录日志或退出程序]

总结:

本章详细讲解了C#中 Thread 类的基本结构、线程的启动与控制方法、参数传递方式以及异常处理机制。通过代码示例、流程图和表格分析,帮助开发者掌握多线程编程的核心技能。下一章将介绍 BackgroundWorker 组件的使用,适用于更复杂的异步任务处理场景。

3. BackgroundWorker组件使用

3.1 BackgroundWorker简介与使用场景

3.1.1 异步操作与UI线程交互的必要性

在Windows桌面应用程序中,尤其是使用WinForms或WPF开发的应用,长时间运行的任务(如文件读写、网络请求、复杂计算等)如果直接在主线程(UI线程)中执行,会导致界面“冻结”,无法响应用户操作,严重影响用户体验。为了解决这一问题,C#提供了多种异步编程模型,其中 BackgroundWorker 是一种面向事件驱动的异步组件,特别适用于需要在后台执行长时间任务同时更新用户界面的场景。

BackgroundWorker 的设计初衷是为了简化异步编程中线程与UI交互的复杂性。它通过封装线程操作,并提供事件机制(如 DoWork ProgressChanged RunWorkerCompleted )来实现后台任务执行与UI更新的分离。其核心优势在于:

  • UI线程安全 :允许从后台线程安全地报告进度或更新界面,而无需手动进行跨线程访问控制。
  • 取消机制 :支持在任务执行过程中随时取消操作。
  • 异常处理 :后台任务中抛出的异常可以被捕获并传递到主线程进行处理。

3.1.2 BackgroundWorker组件的核心事件模型

BackgroundWorker 组件围绕三个核心事件构建其异步执行流程:

事件名称 触发时机 用途说明
DoWork 调用 RunWorkerAsync() 方法后触发 执行后台任务,不可在此事件中更新UI
ProgressChanged 调用 ReportProgress() 方法后触发 报告任务进度,可安全更新UI控件
RunWorkerCompleted 后台任务完成后(正常结束或被取消)触发 处理结果或异常,可安全更新UI

此外, BackgroundWorker 提供了以下关键属性与方法:

  • WorkerReportsProgress :布尔值,表示是否启用进度报告。
  • WorkerSupportsCancellation :布尔值,表示是否支持取消操作。
  • CancelAsync() :发起取消请求。
  • ReportProgress(int percentProgress, object userState) :用于报告任务进度。

这些事件与属性构成了一个完整的异步处理模型,使得开发者可以更轻松地实现后台任务与UI交互的协调。

示例代码:基本事件绑定
private BackgroundWorker backgroundWorker;

public MainForm()
{
    InitializeComponent();
    InitializeBackgroundWorker();
}

private void InitializeBackgroundWorker()
{
    backgroundWorker = new BackgroundWorker
    {
        WorkerReportsProgress = true,
        WorkerSupportsCancellation = true
    };
    backgroundWorker.DoWork += BackgroundWorker_DoWork;
    backgroundWorker.ProgressChanged += BackgroundWorker_ProgressChanged;
    backgroundWorker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;
}

这段代码演示了如何初始化一个 BackgroundWorker 实例,并注册三个核心事件。接下来我们将深入探讨每个事件的具体使用方式。

3.2 异步任务执行流程

3.2.1 DoWork事件:后台计算逻辑

DoWork BackgroundWorker 的核心事件,所有耗时操作应在此事件中执行。该事件运行在非UI线程中,因此不能在此事件中直接访问或修改UI控件。

示例代码:模拟长时间任务
private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker worker = sender as BackgroundWorker;
    int totalSteps = (int)e.Argument;

    for (int i = 0; i < totalSteps; i++)
    {
        if (worker.CancellationPending)
        {
            e.Cancel = true;
            return;
        }

        // 模拟工作
        System.Threading.Thread.Sleep(100);

        // 报告进度
        int progress = (int)(((double)i / totalSteps) * 100);
        worker.ReportProgress(progress, $"Processing step {i + 1}");
    }

    e.Result = "Task completed successfully";
}
代码逻辑分析:
  • sender 是当前 BackgroundWorker 实例,用于调用报告进度或检查取消状态。
  • e.Argument 是传入的参数,通常由 RunWorkerAsync() 方法传入。
  • CancellationPending 属性用于检测是否已调用 CancelAsync() ,若为真则应中止任务。
  • ReportProgress 用于通知主线程当前进度。
  • e.Result 设置任务执行结果,供 RunWorkerCompleted 事件使用。

3.2.2 ProgressChanged事件:进度报告机制

ProgressChanged 事件运行在UI线程上,因此可以安全地更新控件,如进度条、状态标签等。

示例代码:更新进度条
private void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    int progressPercentage = e.ProgressPercentage;
    string statusMessage = e.UserState as string;

    progressBar.Value = progressPercentage;
    labelStatus.Text = statusMessage;
}
代码逻辑分析:
  • e.ProgressPercentage 表示当前进度百分比。
  • e.UserState 是传递的用户状态对象,可用于显示额外信息。
  • 在此方法中,可以安全地更新UI控件,如 ProgressBar Label 等。

3.2.3 RunWorkerCompleted事件:结果处理与UI更新

当后台任务完成时, RunWorkerCompleted 事件被触发。无论任务是正常完成、被取消还是抛出异常,都会在此事件中处理最终结果。

示例代码:处理完成事件
private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if (e.Cancelled)
    {
        MessageBox.Show("任务已取消");
    }
    else if (e.Error != null)
    {
        MessageBox.Show($"发生错误:{e.Error.Message}");
    }
    else
    {
        string result = e.Result as string;
        MessageBox.Show(result);
    }
}
代码逻辑分析:
  • e.Cancelled 判断任务是否被取消。
  • e.Error 捕获后台任务中抛出的异常。
  • e.Result 获取任务执行结果。
状态转换流程图(mermaid)
graph TD
    A[启动 BackgroundWorker] --> B(触发 DoWork)
    B --> C{任务是否完成?}
    C -->|是| D[触发 RunWorkerCompleted]
    C -->|否| E[继续执行]
    B --> F[调用 ReportProgress]
    F --> G[触发 ProgressChanged]

此流程图展示了 BackgroundWorker 的完整状态流转过程。

3.3 实战演练:文件下载器与进度条更新

3.3.1 构建异步下载界面

我们以实现一个简单的文件下载器为例,展示如何使用 BackgroundWorker 实现异步下载与进度更新。

UI界面结构(WinForms)
  • textBoxUrl :输入文件URL
  • buttonDownload :开始下载按钮
  • buttonCancel :取消下载按钮
  • progressBar :显示下载进度
  • labelStatus :显示状态信息
启动下载逻辑
private void buttonDownload_Click(object sender, EventArgs e)
{
    string url = textBoxUrl.Text;
    if (string.IsNullOrEmpty(url))
    {
        MessageBox.Show("请输入有效的URL");
        return;
    }

    if (!backgroundWorker.IsBusy)
    {
        backgroundWorker.RunWorkerAsync(url);
    }
}
后台下载逻辑(DoWork)
private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    string url = e.Argument as string;
    BackgroundWorker worker = sender as BackgroundWorker;

    using (var client = new WebClient())
    {
        client.DownloadProgressChanged += (s, ev) =>
        {
            worker.ReportProgress(ev.ProgressPercentage, $"Downloaded {ev.BytesReceived} of {ev.TotalBytesToReceive}");
        };

        try
        {
            string localPath = Path.Combine(Path.GetTempPath(), "downloaded_file");
            client.DownloadFile(url, localPath);
            e.Result = localPath;
        }
        catch (Exception ex)
        {
            e.Result = null;
            e.Cancel = false;
            e.Error = ex;
        }
    }
}
代码逻辑分析:
  • 使用 WebClient 实现文件下载。
  • 绑定 DownloadProgressChanged 事件,通过 ReportProgress 传递进度信息。
  • 下载完成后返回本地路径,或在出错时设置异常。
取消操作逻辑
private void buttonCancel_Click(object sender, EventArgs e)
{
    if (backgroundWorker.IsBusy)
    {
        backgroundWorker.CancelAsync();
    }
}
完整性处理(RunWorkerCompleted)
private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if (e.Cancelled)
    {
        labelStatus.Text = "下载已取消";
    }
    else if (e.Error != null)
    {
        labelStatus.Text = $"下载失败:{e.Error.Message}";
    }
    else
    {
        string filePath = e.Result as string;
        labelStatus.Text = $"文件已下载至:{filePath}";
    }
}

3.3.2 取消操作与异常处理的完整实现

BackgroundWorker 提供了对取消操作和异常处理的完整支持:

  • 取消操作 :通过 CancelAsync() 发起取消请求,后台任务需在 DoWork 中定期检查 CancellationPending 并主动退出。
  • 异常处理 :任何在 DoWork 中抛出的异常都会被封装到 RunWorkerCompletedEventArgs.Error 中,确保主线程能安全捕获。
异常处理流程图(mermaid)
graph TD
    A[DoWork开始] --> B[执行下载]
    B --> C{是否发生异常?}
    C -->|是| D[设置 e.Error]
    C -->|否| E[设置 e.Result]
    D --> F[RunWorkerCompleted]
    E --> F
异常处理表格
异常类型 原因说明 处理方式示例
WebException 网络连接失败或服务器错误 显示错误提示
IOException 文件写入失败 提示用户检查路径权限
ArgumentException URL格式错误 提示用户重新输入
SecurityException 无权限访问网络或本地文件系统 提示权限问题

通过以上实战示例与流程分析,我们可以看到 BackgroundWorker 是一个功能强大且易于使用的异步组件,尤其适合在WinForms或WPF应用中实现后台任务与UI交互的结合。它不仅简化了多线程编程的复杂性,还提供了清晰的事件模型和完整的异常处理机制,是早期C#异步编程中不可或缺的重要组件。

4. 线程同步机制概述

4.1 多线程同步的基本问题

4.1.1 资源竞争与数据不一致现象

在多线程环境中,多个线程同时访问共享资源(如全局变量、文件、数据库等)时,如果没有适当的同步机制,将导致资源竞争(race condition),进而引发数据不一致问题。例如,两个线程同时对一个计数器进行自增操作,最终结果可能小于预期值。

int counter = 0;

void IncrementCounter()
{
    for (int i = 0; i < 100000; i++)
    {
        counter++;
    }
}

在多线程并发执行该方法时, counter++ 操作不是原子的,它包括读取、自增、写回三个步骤。当两个线程同时读取 counter 的值时,可能读取到相同的值,导致最终只增加了一次。

逻辑分析:

  • counter++ 操作包含三个步骤:
    1. 从内存中读取 counter 的当前值;
    2. 将值加1;
    3. 将新值写回内存。
  • 在并发执行时,两个线程可能同时读取到相同的值,导致其中一个线程的操作被覆盖。

结果示例:

线程A 线程B 内存值
读取0 0
加1为1
写回1 1
读取1
加1为2
写回2 2

但如果两个线程同时读取了0,最终可能只增加了一次。

4.1.2 同步机制的目标与分类

为了解决上述问题,C#提供了多种线程同步机制,目标包括:

  • 互斥访问共享资源 :确保同一时间只有一个线程可以访问共享资源;
  • 保证操作的原子性 :使多个操作作为一个整体执行;
  • 协调线程执行顺序 :控制线程之间的协作顺序。

常见的同步机制包括:

同步机制 描述
lock 关键字 最常用,适用于同一进程内线程间的同步
Monitor 提供更细粒度的控制,如超时、等待
Mutex 跨进程同步,支持命名互斥体
Semaphore 控制同时访问的线程数量
ReaderWriterLockSlim 支持多读单写模式,适合读多写少场景
Interlocked 提供原子操作,如递增、交换等

4.2 lock关键字与临界区控制

4.2.1 lock语句的语法与工作原理

lock 是 C# 中用于线程同步的最常见方式,它确保在同一时间只有一个线程可以进入被锁定的代码块。

object lockObj = new object();

void SafeIncrement()
{
    lock (lockObj)
    {
        counter++;
    }
}

逻辑分析:

  • 当线程进入 lock(lockObj) 代码块时,它会尝试获取 lockObj 的锁;
  • 如果锁未被其他线程持有,则当前线程获取锁并执行代码;
  • 如果锁已被其他线程持有,当前线程将进入等待状态,直到锁被释放;
  • 一旦代码块执行完毕,锁自动释放。

lock语句等效于:

bool lockTaken = false;
try
{
    Monitor.Enter(lockObj, ref lockTaken);
    // 临界区代码
}
finally
{
    if (lockTaken)
    {
        Monitor.Exit(lockObj);
    }
}

4.2.2 lock的性能影响与最佳使用实践

虽然 lock 简单易用,但在高并发场景下可能会带来性能问题,尤其是在锁竞争激烈的情况下。

性能影响:

  • 上下文切换开销 :线程在等待锁时会进入阻塞状态,需要进行上下文切换;
  • 死锁风险 :若多个线程交叉等待彼此持有的锁,可能导致死锁;
  • 可伸缩性下降 :随着线程数增加,锁竞争加剧,程序性能可能下降。

使用建议:

  1. 尽量缩小锁的范围 :只在必要时锁定共享资源;
  2. 避免嵌套锁定 :防止死锁;
  3. 使用轻量级同步结构 :如 Interlocked SpinLock
  4. 使用并发集合 :如 ConcurrentDictionary ConcurrentQueue
  5. 避免锁定字符串等常量对象 :因为它们可能被多个线程共享;
  6. 锁定对象应为私有引用类型 :推荐使用 private readonly object

4.3 Monitor.Enter/Exit使用方法

4.3.1 Monitor类的基本操作

Monitor 类是 lock 语句背后的实现机制,提供了更灵活的同步控制方式,适合需要精细控制同步逻辑的场景。

object syncRoot = new object();

void MonitorExample()
{
    Monitor.Enter(syncRoot);
    try
    {
        // 临界区代码
    }
    finally
    {
        Monitor.Exit(syncRoot);
    }
}

特点:

  • 支持递归锁定(同一个线程多次进入同一锁);
  • 支持尝试获取锁并设置超时;
  • 可与 Wait Pulse 配合实现线程间通信。

4.3.2 TryEnter实现超时等待机制

Monitor.TryEnter 方法允许线程尝试获取锁,并设置等待时间,避免线程无限期阻塞。

bool lockTaken = false;
try
{
    lockTaken = Monitor.TryEnter(syncRoot, TimeSpan.FromSeconds(1));
    if (lockTaken)
    {
        // 执行临界区代码
    }
    else
    {
        Console.WriteLine("无法在1秒内获取锁");
    }
}
finally
{
    if (lockTaken)
    {
        Monitor.Exit(syncRoot);
    }
}

应用场景:

  • 避免死锁;
  • 在 UI 线程中防止界面冻结;
  • 处理实时性要求高的系统,如游戏或嵌入式应用。

4.4 Mutex与Semaphore跨线程同步

4.4.1 Mutex互斥锁的使用场景与实现

Mutex 是一种更重量级的同步机制,它不仅可以用于线程间的同步,还可以用于跨进程同步。

using System.Threading;

Mutex mutex = new Mutex(false, "MySharedMutex");

if (mutex.WaitOne(TimeSpan.FromSeconds(5)))
{
    try
    {
        // 执行受保护代码
    }
    finally
    {
        mutex.ReleaseMutex();
    }
}
else
{
    Console.WriteLine("未能在5秒内获得互斥锁");
}

特点:

  • 支持跨进程同步;
  • 拥有“线程所有权”概念,只有获得锁的线程才能释放;
  • 适合资源只允许一个线程/进程访问的场景。

流程图:

graph TD
    A[线程尝试获取Mutex] --> B{是否成功?}
    B -->|是| C[执行临界区代码]
    B -->|否| D[等待或退出]
    C --> E[释放Mutex]

4.4.2 Semaphore信号量控制并发线程数量

Semaphore 允许指定数量的线程同时访问资源,适用于控制并发访问数量,如数据库连接池、线程池等。

SemaphoreSlim semaphore = new SemaphoreSlim(3); // 允许最多3个线程同时访问

async Task AccessResourceAsync(int id)
{
    await semaphore.WaitAsync();
    try
    {
        Console.WriteLine($"线程 {id} 正在访问资源");
        await Task.Delay(1000); // 模拟工作
    }
    finally
    {
        semaphore.Release();
        Console.WriteLine($"线程 {id} 释放资源");
    }
}

逻辑分析:

  • SemaphoreSlim 是轻量级版本,适合异步编程;
  • 初始化时设置最大并发数(如3);
  • 每个线程调用 WaitAsync() 获取许可;
  • 当许可数为0时,线程进入等待;
  • 线程完成任务后调用 Release() 释放许可。

参数说明:

  • initialCount :初始可用的许可数量;
  • maximumCount :最大许可数量。

使用场景:

  • 限制并发访问数据库连接;
  • 控制线程池任务并发数;
  • 实现异步资源池管理。

本章通过多个代码示例与流程图,详细讲解了线程同步的基本问题与解决机制,包括 lock Monitor Mutex Semaphore 等同步手段的使用方式与适用场景。下一章节将继续深入探讨线程池的使用与优化策略。

5. 线程池ThreadPool原理与使用

线程池(ThreadPool)是.NET平台中用于高效管理线程资源的重要机制之一。它通过复用线程来避免频繁创建和销毁线程带来的性能损耗,特别适用于处理大量短生命周期的任务。本章将从线程池的基本原理入手,深入探讨其工作机制与资源管理策略,随后介绍如何通过 QueueUserWorkItem 提交任务,并进一步分析线程池任务调度的优化技巧,如任务优先级控制、定时任务的实现等,帮助开发者更好地利用线程池提升应用程序性能。

5.1 线程池的基本原理

线程池是一种管理线程生命周期和调度任务的机制。在.NET中, ThreadPool 类提供了线程池的底层实现,允许开发者将任务提交给线程池,由其自动分配线程执行。线程池通过维护一个线程集合,根据系统负载动态调整线程数量,从而提高应用程序的并发处理能力。

5.1.1 线程池的工作机制与资源管理

线程池的核心在于 任务队列 线程调度器 。当应用程序提交任务到线程池时,这些任务被放入一个内部队列中,由线程池中的线程依次取出并执行。这种机制避免了为每个任务单独创建线程所带来的开销。

线程池的工作流程如下:

graph TD
    A[应用程序提交任务] --> B{任务队列是否为空?}
    B -->|否| C[线程池唤醒空闲线程]
    B -->|是| D[等待新任务]
    C --> E[线程执行任务]
    E --> F[任务执行完成]
    F --> G[线程返回线程池]
    G --> H[线程进入等待状态]

线程池内部维护两个关键参数:

  • 最小线程数(MinThreads) :线程池中始终保持的可用线程数。
  • 最大线程数(MaxThreads) :线程池中最多允许创建的线程数。

当任务队列积压较多任务时,线程池会逐步增加线程数量,直到达到最大线程数限制。若任务量持续增加且超过最大线程数限制,新提交的任务将进入队列等待。

代码示例:查看线程池参数
using System;
using System.Threading;

class Program
{
    static void Main()
    {
        int workerThreads, completionPortThreads;
        ThreadPool.GetMinThreads(out workerThreads, out completionPortThreads);
        Console.WriteLine($"最小线程数 - 工作线程: {workerThreads}, I/O完成线程: {completionPortThreads}");

        ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads);
        Console.WriteLine($"最大线程数 - 工作线程: {workerThreads}, I/O完成线程: {completionPortThreads}");
    }
}
逐行解读:
  • ThreadPool.GetMinThreads :获取线程池维持的最小线程数量。
  • ThreadPool.GetMaxThreads :获取线程池允许创建的最大线程数量。
  • workerThreads 用于处理普通的线程池任务, completionPortThreads 用于处理异步I/O操作(如网络请求)。
参数说明:
  • workerThreads :表示线程池中用于处理普通任务的线程数量。
  • completionPortThreads :表示用于处理I/O异步操作的线程数量。
性能影响分析:

线程池通过限制最大线程数来防止系统资源耗尽。但如果设置过低,可能导致任务排队时间过长;设置过高,则可能引发系统性能下降。因此,合理配置线程池参数对性能优化至关重要。

5.1.2 避免过度创建线程的优化策略

在高并发场景下,开发者容易误用 Thread 类频繁创建线程,导致系统资源耗尽。线程池通过以下策略避免线程过度创建:

  • 线程复用机制 :线程执行完任务后不会立即销毁,而是返回线程池继续等待新任务。
  • 任务排队机制 :任务进入队列后按顺序执行,避免同时创建大量线程。
  • 动态线程管理 :线程池根据系统负载动态增加或减少线程数量。
线程池优化建议表:
优化策略 描述 适用场景
设置最小线程数 保证系统在空闲时仍有一定并发能力 高性能服务器、后台服务
控制最大线程数 防止资源耗尽,避免系统崩溃 多任务处理、并发请求
避免长时间阻塞任务 防止线程被占用,影响其他任务执行 异步操作、I/O密集型任务
代码示例:设置线程池最小与最大线程数
using System;
using System.Threading;

class Program
{
    static void Main()
    {
        int workerThreads = 10;
        int completionPortThreads = 10;

        // 设置最小线程数
        ThreadPool.SetMinThreads(workerThreads, completionPortThreads);

        // 设置最大线程数
        ThreadPool.SetMaxThreads(200, 200);

        Console.WriteLine("线程池最小与最大线程数已设置");
    }
}
逐行解读:
  • ThreadPool.SetMinThreads :设置线程池维持的最小线程数。
  • ThreadPool.SetMaxThreads :设置线程池允许的最大线程数。
注意事项:
  • 不建议将最大线程数设置得过高,避免系统资源耗尽。
  • 在I/O密集型任务中,适当增加 completionPortThreads 有助于提升性能。
  • 修改线程池参数应谨慎,建议通过性能测试验证。

5.2 QueueUserWorkItem调度任务

QueueUserWorkItem 是线程池提供的一个核心方法,用于将任务提交到线程池队列中,由线程池自动调度执行。它接受一个 WaitCallback 委托作为任务入口点,可以携带一个状态对象作为参数。

5.2.1 工作项的提交与执行流程

QueueUserWorkItem 方法的调用流程如下:

sequenceDiagram
    participant App as 应用程序
    participant TP as 线程池
    participant T as 线程

    App->>TP: 调用QueueUserWorkItem提交任务
    TP->>TP: 将任务加入内部队列
    TP->>T: 线程从队列取出任务
    T->>T: 执行任务逻辑
    T->>App: 任务完成,返回结果(如需)
代码示例:使用QueueUserWorkItem执行任务
using System;
using System.Threading;

class Program
{
    static void Main()
    {
        for (int i = 0; i < 5; i++)
        {
            int taskId = i;
            ThreadPool.QueueUserWorkItem(state =>
            {
                Console.WriteLine($"任务 {taskId} 正在执行,线程ID: {Thread.CurrentThread.ManagedThreadId}");
                Thread.Sleep(1000); // 模拟耗时操作
                Console.WriteLine($"任务 {taskId} 完成");
            });
        }

        Console.WriteLine("所有任务已提交,主线程等待中...");
        Thread.Sleep(3000); // 主线程等待任务完成
    }
}
逐行解读:
  • ThreadPool.QueueUserWorkItem :提交任务到线程池。
  • state :传递的状态对象(此处未使用)。
  • taskId :任务编号,用于区分不同任务。
  • Thread.CurrentThread.ManagedThreadId :获取当前执行线程的唯一标识。
  • Thread.Sleep(1000) :模拟耗时操作。
  • Thread.Sleep(3000) :主线程等待任务执行完成。
输出示例:
所有任务已提交,主线程等待中...
任务 0 正在执行,线程ID: 4
任务 1 正在执行,线程ID: 5
任务 2 正在执行,线程ID: 6
任务 3 正在执行,线程ID: 7
任务 4 正在执行,线程ID: 8
任务 0 完成
任务 1 完成
任务 2 完成
任务 3 完成
任务 4 完成

5.2.2 线程池任务的参数传递与状态管理

QueueUserWorkItem 允许通过状态对象传递参数。该参数可以是任意类型,但需注意线程安全问题。

代码示例:带参数的任务执行
using System;
using System.Threading;

class Program
{
    static void ProcessTask(object state)
    {
        string taskName = state as string;
        Console.WriteLine($"{taskName} 正在执行,线程ID: {Thread.CurrentThread.ManagedThreadId}");
        Thread.Sleep(1000);
        Console.WriteLine($"{taskName} 完成");
    }

    static void Main()
    {
        ThreadPool.QueueUserWorkItem(ProcessTask, "任务A");
        ThreadPool.QueueUserWorkItem(ProcessTask, "任务B");

        Console.WriteLine("任务已提交,主线程等待中...");
        Thread.Sleep(3000);
    }
}
逐行解读:
  • ProcessTask :定义任务方法,接受一个 object 类型的参数。
  • state as string :将状态对象转换为字符串。
  • ThreadPool.QueueUserWorkItem(ProcessTask, "任务A") :提交任务并传递参数。
状态管理注意事项:
  • 状态对象应尽量使用不可变类型,避免多线程修改引发数据竞争。
  • 若需共享状态,应使用锁机制或线程安全容器(如 ConcurrentDictionary )进行管理。

5.3 线程池任务调度优化技巧

线程池默认按FIFO(先进先出)顺序调度任务,但在实际开发中,可能需要对任务优先级进行控制,或实现定时任务调度。本节将介绍任务优先级控制策略和定时任务的实现方式。

5.3.1 任务优先级与执行顺序控制

线程池本身不支持任务优先级的概念,但可以通过任务队列管理或外部调度器实现优先级控制。例如,将高优先级任务插入队列头部,或使用多个线程池分别处理不同优先级任务。

示例:使用队列实现优先级控制
using System;
using System.Collections.Generic;
using System.Threading;

class TaskItem
{
    public string Name { get; set; }
    public int Priority { get; set; } // 0为高优先级
    public Action Action { get; set; }
}

class PriorityTaskScheduler
{
    private readonly Queue<TaskItem> highPriorityQueue = new Queue<TaskItem>();
    private readonly Queue<TaskItem> normalPriorityQueue = new Queue<TaskItem>();

    public void Enqueue(TaskItem task)
    {
        if (task.Priority == 0)
            highPriorityQueue.Enqueue(task);
        else
            normalPriorityQueue.Enqueue(task);

        ThreadPool.QueueUserWorkItem(ExecuteNextTask);
    }

    private void ExecuteNextTask(object state)
    {
        TaskItem task = null;
        lock (highPriorityQueue)
        {
            if (highPriorityQueue.Count > 0)
                task = highPriorityQueue.Dequeue();
            else if (normalPriorityQueue.Count > 0)
                task = normalPriorityQueue.Dequeue();
        }

        if (task != null)
            task.Action();
    }
}

class Program
{
    static void Main()
    {
        var scheduler = new PriorityTaskScheduler();

        scheduler.Enqueue(new TaskItem { Name = "任务1", Priority = 0, Action = () =>
        {
            Console.WriteLine("执行高优先级任务1");
        }});

        scheduler.Enqueue(new TaskItem { Name = "任务2", Priority = 1, Action = () =>
        {
            Console.WriteLine("执行普通优先级任务2");
        }});

        Console.WriteLine("任务已提交,等待执行...");
        Thread.Sleep(2000);
    }
}
逐行解读:
  • TaskItem :定义任务结构,包含名称、优先级和动作。
  • PriorityTaskScheduler :自定义调度器,区分高优先级与普通优先级任务。
  • Enqueue :将任务加入对应队列,并触发线程池执行。
  • ExecuteNextTask :从高优先级队列中取出任务执行,若无则执行普通任务。
优化策略表:
优化策略 描述 适用场景
队列优先级排序 按优先级顺序执行任务 实时任务、关键路径任务
多线程池隔离 不同优先级使用不同线程池 多级任务调度、资源隔离
动态调整线程池 根据任务负载调整线程池参数 高并发系统、动态负载

5.3.2 利用计时器与回调实现定时任务

线程池支持使用 Timer 类实现定时任务。 Timer 通过回调机制定期触发任务执行,适用于周期性操作(如心跳检测、日志轮询等)。

代码示例:定时任务实现
using System;
using System.Threading;

class Program
{
    static void TimerCallback(object state)
    {
        Console.WriteLine($"定时任务触发,线程ID: {Thread.CurrentThread.ManagedThreadId}");
    }

    static void Main()
    {
        Timer timer = new Timer(TimerCallback, null, 0, 1000); // 初始延迟0ms,间隔1000ms

        Console.WriteLine("定时任务已启动,按任意键退出...");
        Console.ReadKey();

        timer.Dispose();
    }
}
逐行解读:
  • Timer :创建定时器对象。
  • TimerCallback :定时器触发的回调方法。
  • 0, 1000 :初始延迟0毫秒,之后每隔1000毫秒执行一次。
  • timer.Dispose() :释放定时器资源。
参数说明:
  • TimerCallback :定时器触发时执行的方法。
  • null :传递给回调的状态对象。
  • 0 :首次触发延迟时间(毫秒)。
  • 1000 :后续触发间隔时间(毫秒)。
应用场景:
  • 心跳检测(如服务保活)
  • 日志自动刷新
  • 定时清理缓存

通过结合线程池与定时器,开发者可以实现高效的周期性任务调度机制,避免单独创建线程带来的资源浪费。

6. 异步编程模型分类

在C#中,异步编程模型是实现高效、响应式应用程序的关键。随着.NET框架的发展,异步编程经历了多个阶段的演进,形成了多种异步模型。本章将系统性地介绍C#中常见的异步编程模型,包括回调式异步、事件驱动异步、任务并行库(TPL)以及async/await语法模型。通过深入分析每种模型的工作机制、适用场景和实现细节,帮助开发者选择最合适的异步编程方式,以提升程序性能与用户体验。

6.1 回调式异步编程(Begin/End)

回调式异步编程模型(也称为IAsyncResult模式)是.NET早期版本中主要的异步实现方式。它通过 BeginInvoke EndInvoke 方法实现异步调用,并通过回调函数处理异步操作完成后的逻辑。

6.1.1 IAsyncResult模式详解

IAsyncResult模式的核心在于 IAsyncResult 接口和对应的异步方法对,如 BeginXXX EndXXX 。该模式允许开发者在不阻塞主线程的前提下执行耗时操作。

public delegate int AddDelegate(int a, int b);

public class AsyncAdd
{
    public static void Main()
    {
        AddDelegate add = new AddDelegate(AddNumbers);
        IAsyncResult result = add.BeginInvoke(5, 10, new AsyncCallback(AdditionCompleted), null);
    }

    static int AddNumbers(int a, int b)
    {
        Thread.Sleep(2000); // 模拟耗时操作
        return a + b;
    }

    static void AdditionCompleted(IAsyncResult ar)
    {
        AsyncResult result = (AsyncResult)ar;
        AddDelegate add = (AddDelegate)result.AsyncDelegate;
        int sum = add.EndInvoke(ar);
        Console.WriteLine("Sum is: " + sum);
    }
}
代码解析:
  • AddDelegate :定义了一个委托,指向异步执行的方法。
  • BeginInvoke :启动异步调用,传入参数和回调函数 AdditionCompleted
  • EndInvoke :在回调中调用,获取异步操作结果。
  • AsyncCallback :定义回调方法,处理异步完成后逻辑。
参数说明:
  • a , b :传入的两个整数。
  • AsyncCallback :回调委托,用于处理异步结果。
  • object state :状态对象,可选参数,用于传递上下文信息。
逻辑流程图:
graph TD
    A[开始异步调用 BeginInvoke] --> B[调用 AddNumbers 方法]
    B --> C[方法执行完成]
    C --> D[调用回调函数 AdditionCompleted]
    D --> E[调用 EndInvoke 获取结果]
    E --> F[输出结果]

6.1.2 BeginInvoke与EndInvoke的实际应用

BeginInvoke EndInvoke 不仅适用于自定义方法,也广泛用于.NET中已有的异步API,例如网络请求、文件读写等。

示例:异步网络请求
public static void MakeWebRequest()
{
    WebClient client = new WebClient();
    client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(DownloadComplete);
    client.DownloadStringAsync(new Uri("https://example.com"));
}

private static void DownloadComplete(object sender, DownloadStringCompletedEventArgs e)
{
    if (e.Error == null)
    {
        Console.WriteLine("Downloaded content length: " + e.Result.Length);
    }
}

虽然此例使用的是事件驱动模型,但其底层机制仍依赖于 BeginInvoke/EndInvoke

6.2 事件驱动异步模式(Event-based Async)

事件驱动异步模型通过事件通知机制实现异步操作的完成与结果反馈,常见于.NET Framework 2.0及以后版本中,如 BackgroundWorker WebClient 等组件。

6.2.1 基于事件的异步操作设计原则

事件驱动模型通过注册事件处理程序(如 DoWork ProgressChanged RunWorkerCompleted 等)来实现异步操作的启动与完成通知。其优点是逻辑清晰、易于与UI交互。

核心设计原则:
  • 事件分离 :将异步操作分为启动、执行、完成三个阶段,分别绑定不同事件。
  • 线程安全 :异步操作通常在后台线程中执行,UI更新必须通过 Invoke ProgressChanged 安全地切换回主线程。
  • 可取消性 :提供 CancelAsync 方法允许用户中断异步操作。

6.2.2 BackgroundWorker与WebClient的异步实现

示例:使用BackgroundWorker进行异步计算
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker worker = sender as BackgroundWorker;
    int total = 0;
    for (int i = 0; i < 100; i++)
    {
        if (worker.CancellationPending)
        {
            e.Cancel = true;
            return;
        }
        total += i;
        worker.ReportProgress(i, "Processing...");
    }
    e.Result = total;
}

private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    labelProgress.Text = e.UserState.ToString();
    progressBar.Value = e.ProgressPercentage;
}

private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if (e.Cancelled)
    {
        MessageBox.Show("Operation Cancelled");
    }
    else if (e.Error != null)
    {
        MessageBox.Show("Error: " + e.Error.Message);
    }
    else
    {
        MessageBox.Show("Result: " + e.Result);
    }
}
参数说明:
  • DoWorkEventArgs :用于传递参数和返回结果。
  • ProgressChangedEventArgs :报告进度并传递用户状态。
  • RunWorkerCompletedEventArgs :包含结果、错误或取消标志。

6.3 任务并行库TPL详解

任务并行库(Task Parallel Library, TPL)是.NET 4.0引入的高级异步编程模型,旨在简化并行和异步操作的开发。TPL的核心是 Task 类,它封装了线程池任务的管理与调度。

6.3.1 TPL的核心组件与执行模型

TPL的主要组件包括:

组件 功能
Task 表示一个异步操作
Task<TResult> 表示带有返回值的异步操作
TaskFactory 提供创建和调度任务的工厂方法
CancellationToken 用于取消任务
Parallel 实现数据并行
示例:创建并执行任务
Task<int> task = Task.Run(() =>
{
    int sum = 0;
    for (int i = 0; i < 1000; i++) sum += i;
    return sum;
});

task.ContinueWith(t =>
{
    Console.WriteLine("Task completed with result: " + t.Result);
});
逻辑流程图:
graph TD
    A[创建任务 Task.Run] --> B[线程池调度执行]
    B --> C[任务完成]
    C --> D[ContinueWith执行后续操作]

6.3.2 Task类与线程池的集成机制

Task 类内部依赖线程池进行任务调度,但其提供了更高层次的抽象,如延续任务(ContinueWith)、取消机制、异常处理等。

示例:任务链式调用
Task<int> task1 = Task.Run(() => { return 100; });
Task<int> task2 = task1.ContinueWith(prevTask => { return prevTask.Result * 2; });
Task<int> task3 = task2.ContinueWith(prevTask => { return prevTask.Result + 50; });

task3.Wait();
Console.WriteLine("Final result: " + task3.Result);

6.4 async/await关键字实战

C# 5.0引入的 async await 关键字,极大简化了异步编程的代码结构,使异步代码如同同步代码一样易于编写和理解。

6.4.1 异步方法的定义与执行流程

示例:异步方法定义
public async Task<int> DownloadContentLengthAsync()
{
    using (HttpClient client = new HttpClient())
    {
        string content = await client.GetStringAsync("https://example.com");
        return content.Length;
    }
}
执行流程:
  1. await 表达式将方法的执行挂起,释放当前线程。
  2. 当异步操作完成后,控制权返回方法继续执行。
  3. 返回结果或抛出异常。
异步方法调用示例:
public async void StartDownload()
{
    int length = await DownloadContentLengthAsync();
    Console.WriteLine("Downloaded content length: " + length);
}

6.4.2 await表达式与上下文切换的处理

await 关键字不仅用于等待异步操作完成,还负责在适当上下文(如UI线程)恢复执行。例如,在WinForms或WPF应用中, await 会自动切换回UI线程以更新界面。

示例:UI线程安全更新
private async void button1_Click(object sender, EventArgs e)
{
    string result = await FetchDataAsync();
    textBox1.Text = result; // 安全更新UI
}

private async Task<string> FetchDataAsync()
{
    using (HttpClient client = new HttpClient())
    {
        return await client.GetStringAsync("https://example.com");
    }
}
上下文切换说明:
  • 如果在UI线程中调用 await ,则后续代码仍将在UI线程执行。
  • 可使用 ConfigureAwait(false) 避免上下文切换以提升性能(适用于库代码)。

小结

本章系统介绍了C#中四种主要的异步编程模型:回调式异步、事件驱动异步、任务并行库(TPL)和 async/await 模型。每种模型都有其适用的场景和优势:

  • 回调式异步 适合底层API和需要细粒度控制的场景。
  • 事件驱动模型 适用于WinForms等UI应用。
  • TPL 是现代.NET开发的核心工具,适用于大多数异步和并行操作。
  • async/await 则是当前最推荐的异步编程方式,因其简洁、可维护性强。

理解并熟练掌握这些模型,将有助于开发者构建高效、健壮的异步应用程序。

7. HttpClient异步HTTP请求处理

7.1 HttpClient基础与异步调用

HttpClient 是 C# 中用于发起 HTTP 请求的核心类,位于 System.Net.Http 命名空间中。它支持异步编程模型,非常适合在多线程或异步应用程序中执行网络操作,避免阻塞主线程。

7.1.1 HttpClient类的创建与配置

创建 HttpClient 实例非常简单,推荐使用 using 语句确保资源及时释放:

using (HttpClient client = new HttpClient())
{
    // 配置基础地址
    client.BaseAddress = new Uri("https://api.example.com/");
    // 设置默认请求头
    client.DefaultRequestHeaders.Add("User-Agent", "C# App");
}

⚠️ 注意:虽然 HttpClient 实现了 IDisposable 接口,但在实际项目中频繁创建和释放可能引起性能问题。建议使用单例模式或 HttpClientFactory (.NET Core 2.1+)来管理实例。

7.1.2 发起GET与POST异步请求的方法

HttpClient 提供了多个异步方法来发送 HTTP 请求,例如:

  • GetAsync(Uri) :发送 GET 请求并返回响应。
  • PostAsync(Uri, HttpContent) :发送 POST 请求并传递内容。
  • GetStringAsync(Uri) :直接获取响应字符串。
示例:GET 请求
public async Task<string> FetchDataAsync(string url)
{
    using (HttpClient client = new HttpClient())
    {
        try
        {
            string result = await client.GetStringAsync(url);
            return result;
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine($"请求失败: {ex.Message}");
            return null;
        }
    }
}
示例:POST 请求
public async Task<string> SendPostRequestAsync(string url, string jsonContent)
{
    using (HttpClient client = new HttpClient())
    {
        var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");

        HttpResponseMessage response = await client.PostAsync(url, content);
        if (response.IsSuccessStatusCode)
        {
            return await response.Content.ReadAsStringAsync();
        }

        Console.WriteLine($"响应失败:{(int)response.StatusCode}");
        return null;
    }
}

7.2 异步请求的异常处理与重试机制

在进行网络请求时,网络波动、服务器错误、超时等问题是常见的。因此,必须合理处理异常,并设计重试机制。

7.2.1 请求超时与网络异常的处理方案

HttpClient 默认没有设置超时时间,建议手动设置 Timeout 属性:

client.Timeout = TimeSpan.FromSeconds(10); // 设置10秒超时

捕获异常时应区分不同类型的异常:

try
{
    var response = await client.GetAsync("https://api.example.com/data");
    response.EnsureSuccessStatusCode(); // 抛出非200-299状态码异常
}
catch (HttpRequestException ex) when (ex.InnerException is TimeoutException)
{
    Console.WriteLine("请求超时");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"HTTP请求错误: {ex.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"未知错误: {ex.Message}");
}

7.2.2 利用Polly实现智能重试策略

Polly 是一个强大的 .NET 弹性与瞬态故障处理库,支持重试、熔断、缓存等策略。

安装 Polly:
dotnet add package Polly
示例:使用 Polly 实现重试策略
var retryPolicy = Policy
    .Handle<HttpRequestException>()
    .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
    .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

var response = await retryPolicy.ExecuteAsync(() => client.GetAsync("https://api.example.com/data"));

7.3 多线程程序性能优化技巧

在使用 HttpClient 进行异步请求时,尤其是在高并发场景下,程序性能可能受到线程调度、资源竞争等因素影响。

7.3.1 线程数量与任务调度的平衡策略

  • 避免过度创建线程 :使用 Task.Run Parallel.ForEach 时,注意不要创建过多线程,否则会增加上下文切换开销。
  • 使用信号量控制并发数
SemaphoreSlim semaphore = new SemaphoreSlim(5); // 最多并发5个请求

List<Task> tasks = new List<Task>();
foreach (var url in urls)
{
    await semaphore.WaitAsync();
    tasks.Add(Task.Run(async () =>
    {
        try
        {
            string result = await FetchDataAsync(url);
            Console.WriteLine(result);
        }
        finally
        {
            semaphore.Release();
        }
    }));
}

await Task.WhenAll(tasks);

7.3.2 避免死锁与资源泄露的最佳实践

  • 避免阻塞异步代码 :如 Result Wait() 方法容易造成死锁。
  • 正确使用 ConfigureAwait(false) :在库代码中建议使用 ConfigureAwait(false) 避免上下文捕获:
string result = await client.GetStringAsync(url).ConfigureAwait(false);

7.4 C#并发编程最佳实践

7.4.1 线程安全与资源共享策略

在并发访问共享资源(如缓存、数据库连接)时,应使用同步机制保护数据一致性:

private static readonly object _lock = new object();
private static Dictionary<string, string> _cache = new Dictionary<string, string>();

public static void UpdateCache(string key, string value)
{
    lock (_lock)
    {
        if (!_cache.ContainsKey(key))
        {
            _cache.Add(key, value);
        }
        else
        {
            _cache[key] = value;
        }
    }
}

7.4.2 推荐的并发编程模式与工具链

模式/工具 用途 优势
async/await 异步编程 简洁、易于维护
Task Parallel Library (TPL) 并行任务处理 自动调度线程
Parallel.ForEach 并行循环 简化多线程循环逻辑
Concurrent Collections 线程安全集合 ConcurrentDictionary , ConcurrentQueue
Channel<T> (.NET Core 3.0+) 生产者-消费者模式 高性能、异步数据流处理
示例:使用 ConcurrentDictionary 缓存响应数据
private static ConcurrentDictionary<string, string> _responseCache = new ConcurrentDictionary<string, string>();

public async Task<string> GetCachedResponseAsync(string url)
{
    return await _responseCache.GetOrAdd(url, async key =>
    {
        return await FetchDataAsync(key);
    });
}

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:在C#编程中,线程是实现多任务并行处理的基础,能显著提升程序的执行效率和响应能力。本学习资料“C#线程学习.zip”详细讲解了C#中线程的创建、管理与同步机制,涵盖线程池、异步编程模型、任务并行库(TPL)以及HttpClient的异步使用。通过实例演示和代码示例,帮助开发者深入理解多线程编程的核心概念与实战技巧,为构建高性能、稳定的应用程序打下基础。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐