C#基础之多线程讲解

2024-12-01  本文已影响0人  上善若泪

1 多线程

1.1 简介

1.1.1 进程&线程

线程与进程区别:

1.1.2 线程优缺点

多线程的优点:可以同时完成多个任务;可以使程序的响应速度更快;可以让占用大量处理时间的任务或当前没有进行处理的任务定期将处理时间让给别的任务;可以随时停止任务;可以设置每个任务的优先级以优化程序性能。

缺点:

1.1.3 主线程

C# 中,System.Threading.Thread 类用于线程的工作。创建并访问多线程应用程序中的单个线程。进程中第一个被执行的线程称为主线程
当 C# 程序开始执行时,主线程自动创建。使用 Thread 类创建的线程被主线程的子线程调用。可以使用 Thread 类的 CurrentThread 属性访问线程。

下面的程序演示了主线程的执行:

using System;
using System.Threading;

namespace MultithreadingApplication
{
    class MainThreadProgram
    {
        static void Main(string[] args)
        {
            Thread th = Thread.CurrentThread;
            th.Name = "MainThread";
            Console.WriteLine("This is {0}", th.Name);
            Console.ReadKey();
        }
    }
}
结果:
This is MainThread

1.2 线程生命周期

线程生命周期开始于 System.Threading.Thread 类的对象被创建时,结束于线程被终止或完成执行时。

下面列出了线程生命周期中的各种状态:

1.3 常用属性和方法

下表列出了 Thread 类的一些常用的 属性:

属性 描述
CurrentContext 获取线程正在其中执行的当前上下文
CurrentCulture 获取或设置当前线程的区域性
CurrentPrincipal 获取或设置线程的当前负责人(对基于角色的安全性而言)
CurrentThread 获取当前正在运行的线程
CurrentUICulture 获取或设置资源管理器使用的当前区域性以便在运行时查找区域性特定的资源
ExecutionContext 获取一个 ExecutionContext 对象,该对象包含有关当前线程的各种上下文的信息
IsAlive 获取一个值,该值指示当前线程的执行状态
IsBackground 获取或设置一个值,该值指示某个线程是否为后台线程
IsThreadPoolThread 获取一个值,该值指示线程是否属于托管线程池
ManagedThreadId 获取当前托管线程的唯一标识符
Name 获取或设置线程的名称
Priority 获取或设置一个值,该值指示线程的调度优先级
ThreadState 获取一个值,该值包含当前线程的状态

下表列出了 Thread 类的一些常用的 方法:

方法名 描述
public void Abort() 在调用此方法的线程上引发 ThreadAbortException,以开始终止此线程的过程。调用此方法通常会终止线程
public static LocalDataStoreSlot AllocateDataSlot() 在所有的线程上分配未命名的数据槽。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段
public static LocalDataStoreSlot AllocateNamedDataSlot( string name) 在所有线程上分配已命名的数据槽。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段
public static void BeginCriticalRegion() 通知主机执行将要进入一个代码区域,在该代码区域内线程中止或未经处理的异常的影响可能会危害应用程序域中的其他任务
public static void BeginThreadAffinity() 通知主机托管代码将要执行依赖于当前物理操作系统线程的标识的指令
public static void EndCriticalRegion() 通知主机执行将要进入一个代码区域,在该代码区域内线程中止或未经处理的异常仅影响当前任务
public static void EndThreadAffinity() 通知主机托管代码已执行完依赖于当前物理操作系统线程的标识的指令
public static void FreeNamedDataSlot(string name) 为进程中的所有线程消除名称与槽之间的关联。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段
public static Object GetData( LocalDataStoreSlot slot ) 在当前线程的当前域中从当前线程上指定的槽中检索值。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段
public static AppDomain GetDomain() 返回当前线程正在其中运行的当前域
public static AppDomain GetDomainID() 返回唯一的应用程序域标识符
public static LocalDataStoreSlot GetNamedDataSlot( string name ) 查找已命名的数据槽。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段
public void Interrupt() 中断处于 WaitSleepJoin 线程状态的线程
public void Join() 在继续执行标准的 COM 和 SendMessage 消息泵处理期间,阻塞调用线程,直到某个线程终止为止。此方法有不同的重载形式
public static void MemoryBarrier() 按如下方式同步内存存取:执行当前线程的处理器在对指令重新排序时,不能采用先执行 MemoryBarrier 调用之后的内存存取,再执行 MemoryBarrier 调用之前的内存存取的方式
public static void ResetAbort() 取消为当前线程请求的 Abort
public static void SetData( LocalDataStoreSlot slot, Object data ) 在当前正在运行的线程上为此线程的当前域在指定槽中设置数据。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段
public void Start() 开始一个线程
public static void Sleep( int millisecondsTimeout ) 让线程暂停一段时间
public static void SpinWait( int iterations ) 导致线程等待由 iterations 参数定义的时间量
public static byte VolatileRead( ref byte address )
public static double VolatileRead( ref double address )
public static int VolatileRead( ref int address )
public static Object VolatileRead( ref Object address )
读取字段值。无论处理器的数目或处理器缓存的状态如何,该值都是由计算机的任何处理器写入的最新值。此方法有不同的重载形式。这里只给出了一些形式
public static void VolatileWrite( ref byte address, byte value )
public static void VolatileWrite( ref double address, double value )
public static void VolatileWrite( ref int address, int value )
public static void VolatileWrite( ref Object address, Object value )
立即向字段写入一个值,以使该值对计算机中的所有处理器都可见。此方法有不同的重载形式。这里只给出了一些形式
public static bool Yield() 导致调用线程执行准备好在当前处理器上运行的另一个线程。由操作系统选择要执行的线程

1.4 创建线程

1.4.1 System.Threading.Thread

线程是通过扩展 Thread 类创建的。扩展的 Thread 类调用 Start() 方法来开始子线程的执行。

1.4.1.1 不带参数处理

下面的程序演示了这个概念:

using System;
using System.Threading;

namespace MultithreadingApplication
{
    class ThreadCreationProgram
    {
        public static void CallToChildThread()
        {
            Console.WriteLine("Child thread starts");
        }
       
        static void Main(string[] args)
        {
            Console.WriteLine("In Main: Creating the Child thread");
            ThreadStart childref = new ThreadStart(CallToChildThread);
            Thread childThread = new Thread(childref);
            childThread.Start();
            Console.ReadKey();
        }
    }
}

结果:
In Main: Creating the Child thread
Child thread starts

1.4.1.2 带参数处理

线程函数通过委托传递,可以不带参数,也可以带参数(只能有一个参数),可以用一个类或结构体封装参数

using System;
using System.Threading;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread t1 = new Thread(new ThreadStart(TestMethod));
            Thread t2 = new Thread(new ParameterizedThreadStart(TestMethod));
            t1.IsBackground = true;
            t2.IsBackground = true;
            t1.Start();
            t2.Start("hello");
            Console.ReadKey();
        }

        public static void TestMethod()
        {
            Console.WriteLine("不带参数的线程函数");
        }

        public static void TestMethod(object data)
        {
            string datastr = data as string;
            Console.WriteLine("带参数的线程函数,参数为:{0}", datastr);
        }
    }
}

1.4.1.3 不用new ThreadStart

c#2.0 后可以直接传入方法而不用写 new ThreadStart,但是需要注意,如果 CallToChildThread 方法没有重载,可以直接用下面的简化写法,如果有重载就需要先用 new ThreadStart 创建线程入口,不然编辑器会报“方法或属性调用不明确”错误

using System.Threading.Tasks;
using System.Threading;
namespace ConsoleApp
{
    class Program
    {
        public static void CallToChildThread()
        {
            Console.WriteLine("Child thread starts");
        }

        static void Main(string[] args)
        {
            //ThreadStart childref = new ThreadStart(CallToChildThread);
            Console.WriteLine("In Main: Creating the Child thread");
            Thread childThread = new Thread(CallToChildThread);
            childThread.Start();
            Console.ReadKey();
        }
    }
}

1.4.2 ThreadPool

ThreadPool 由 .Net 自己管理, 只需要把需要处理的方法写好, 然后交个.Net Framework, 后续只要方法执行完毕, 则自动退出。ThreadPool 提供了一种简单而高效的方式来管理线程池,从而可以方便地创建和管理多线程应用程序。使用线程池可以显著降低创建和销毁线程的开销,并且有助于更好地管理系统资源。

using System;  
using System.Threading;  
  
class Program  
{  
    // 工作方法,线程池中的线程将执行这个方法。  
    static void ThreadPoolCallback(object state)  
    {  
        // 将传递的状态信息转换为字符串并打印。  
        Console.WriteLine("Thread ID: {0}, State: {1}", Thread.CurrentThread.ManagedThreadId, state);  
        // 模拟一些工作负载。  
        Thread.Sleep(2000); // 休眠2秒  
        // 打印完成信息。  
        Console.WriteLine("Thread ID: {0} completed.", Thread.CurrentThread.ManagedThreadId);  
    }  
  
    static void Main()  
    {  
        // 要排队到线程池中的任务数量。  
        int numberOfTasks = 5;  
        // 排队任务到线程池中。  
        for (int i = 0; i < numberOfTasks; i++)  
        {  
            // 使用 ThreadPool.QueueUserWorkItem 方法将任务排队到线程池中。  
            // 这个方法接受一个 WaitCallback 委托和一个状态对象作为参数。  
            ThreadPool.QueueUserWorkItem(ThreadPoolCallback, i);  
        }  
  
        // 防止主线程在所有任务完成之前退出。  
        Console.WriteLine("Main thread waiting for ThreadPool tasks to complete...");  
        Thread.Sleep(15000); // 休眠15秒以等待所有任务完成  
        Console.WriteLine("Main thread exiting.");  
    }  
}

1.4.3 System.Threading.Tasks.Task

1.4.3.1 Task与ThreadPool区别

TaskThreadPoolC# 中都是用于实现多线程和并行编程的机制,但它们在使用方式和效率上有所不同。

1.4.3.2 Task 的使用方式

1.4.3.3 Start 和 Wait

什么时候需要调用 Start

调用 Start 是 手动启动任务 的一种方式,仅适用于通过 Task 构造函数创建的任务。这类任务初始状态是 未启动 的,必须显式调用 Start 方法将其加入线程池。

什么时候不需要调用 Start?

通过 Task.RunTask.Factory.StartNew 创建的任务会自动开始执行,无需调用 Start,而且尝试调用 Start 会导致异常。
这时候就需要 Task.Wait ,阻塞调用线程,直到 Task 完成执行。如果任务已经完成,则立即返回。如果任务尚未开始或正在运行,则阻塞调用线程,直到任务完成。

区别与使用场景

方法 作用 适用场景
Start 显式启动任务 需要手动控制任务启动时间,适用于通过构造函数创建的任务
Wait 阻塞调用线程,等待任务完成 当需要确保任务完成后再继续后续代码时使用

Start使用场景

创建方式 是否需要调用 Start 推荐做法
new Task(...) 构造函数 必须调用 Start 手动启动任务
Task.Run 不需要调用 Start,直接创建并运行
Task.Factory.StartNew 不需要调用 Start,直接创建并运行

在现代 C# 开发中,通常建议使用 Task.Runasync/await 处理异步任务,而不是通过显式调用 Start 来管理任务

1.4.3.4 示例

基本任务创建与执行

using System;  
using System.Threading.Tasks;  
  
class Program  
{  
    static void Main()  
    {  
        // 创建一个简单的任务,该任务会打印一条消息  
        Task task = Task.Run(() =>  
        {  
            Console.WriteLine("Task is executing...");  
        });  
  
        // 等待任务完成  
        task.Wait();  
  
        Console.WriteLine("Task is completed.");  
    }  
}

返回结果的任务

using System;  
using System.Threading.Tasks;  
  
class Program  
{  
    static void Main()  
    {  
        // 创建一个返回整数结果的任务  
        Task<int> task = Task.Run(() =>  
        {  
            // 模拟一些计算或处理  
            return 42;  
        });  
  
        // 等待并获取任务结果  
        int result = task.Result;  
  
        Console.WriteLine("Task result: " + result);  
    }  
}

并行执行多个任务

using System;  
using System.Threading.Tasks;  
  
class Program  
{  
    static void Main()  
    {  
        // 创建三个任务  
        Task task1 = Task.Run(() =>  
        {  
            Console.WriteLine("Task 1 is executing...");  
        });  
  
        Task<int> task2 = Task.Run(() =>  
        {  
            Console.WriteLine("Task 2 is executing...");  
            return 42;  
        });  
  
        Task task3 = Task.Run(() =>  
        {  
            Console.WriteLine("Task 3 is executing...");  
        });  
  
        // 等待所有任务完成  
        Task.WhenAll(task1, task2, task3).Wait();  
  
        Console.WriteLine("All tasks are completed.");  
    }  
}

使用 ContinueWith 创建延续任务

using System;  
using System.Threading.Tasks;  
  
class Program  
{  
    static void Main()  
    {  
        // 创建一个任务  
        Task<int> task = Task.Run(() =>  
        {  
            // 模拟一些计算或处理  
            return 42;  
        });  
  
        // 创建一个在任务完成后执行的延续任务  
        task.ContinueWith(t =>  
        {  
            // 获取任务结果并打印  
            int result = t.Result;  
            Console.WriteLine("Continuation task: Task result is " + result);  
        });  
  
        // 为了演示效果,这里等待主任务完成(实际使用中可能不需要这样做)  
        task.Wait();  
    }  
}

1.4.4 async/await

1.4.4.1 解释分析

在 C# 中,asyncawait 是用于异步编程的关键字

1.4.4.2 异步方法的异常处理

异步方法中的异常会被包装在 Task 对象中。要捕获异常,可以使用 try-catch 语句,并在 await 表达式中捕获,避免 async void 方法<除非是事件处理器,不推荐使用 async void,因为它不允许捕获异常,且调用者无法等待其完成。应优先使用 TaskTask<T>

public async Task MyMethod()
{
    try
    {
        await SomeAsyncMethod();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"捕获异常: {ex.Message}");
    }
}

1.4.4.3 await 返回

await 只能用于 TaskTask<T>,或实现了 GetAwaiter 方法的类型。

public class AsyncExample
{
    public async Task<string> FetchDataAsync()
    {
        // 模拟一个耗时操作,例如网络请求
        await Task.Delay(2000);
        return "异步数据";
    }

    public async Task RunExampleAsync()
    {
        try
        {
            string data = await FetchDataAsync();
            Console.WriteLine(data);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"捕获异常: {ex.Message}");
        }
    }
}

1.4.4.4 示例分析

using System;
using System.Threading.Tasks;

public class Program
{
    public static async Task Main(string[] args)
    {
        Console.WriteLine("开始获取数据...");

        // 调用异步方法,并等待其结果
        string result = await GetDataAsync();

        Console.WriteLine($"数据获取完成: {result}");
    }
    // 定义一个异步方法,用 async 标记
    public static async Task<string> GetDataAsync()
    {
        // 使用 Task.Delay 来模拟一个耗时操作,例如网络请求
        await Task.Delay(3000); // 等待3秒

        return "这里是获取到的数据";
    }
}
结果
开始获取数据...
(等待 3 秒)
数据获取完成: 这里是获取到的数据

详细解释:

1.4.5 System.Threading.Tasks.Parallel

System.Threading.Tasks.Parallel类并行执行,不是真正的多线程,但用于并行处理数据
这个类是 C# 中用于并行编程的一个重要工具,并行地执行循环或操作,从而充分利用多核处理器的性能。

Parallel类利用线程池(ThreadPool)来管理线程,并自动将任务分配给可用的线程。这意味着开发者不需要显式地创建和管理线程,而是将精力集中在任务的定义和并行执行上。尽管Parallel类并不直接创建线程,但它确实利用了多线程来实现并行处理。线程池中的线程是实际执行任务的实体,而Parallel类则提供了更高层次的抽象,使开发者能够更容易地实现并行处理。
因此,可以说Parallel类提供了一种并行处理数据的方式,这种方式基于多线程,但又不完全等同于多线程编程。它更多地是一种任务并行化的编程模型,允许开发者以更简单、更直观的方式实现并行处理。

Parallel 类是一个静态类,它提供了几个静态方法用于并行执行代码,主要包括 Parallel.ForParallel.ForEachParallel.Invoke

csharp
Parallel.For(0, 10, i =>  
{  
    Console.WriteLine($"Iteration {i} is running on thread {Thread.CurrentThread.ManagedThreadId}");  
});
var numbers = new List<int> { 1, 2, 3, 4, 5 };  
Parallel.ForEach(numbers, number =>  
{  
    Console.WriteLine($"Processing {number} on thread {Thread.CurrentThread.ManagedThreadId}");  
});
Parallel.Invoke(  
    () => { /* Task 1 */ },  
    () => { /* Task 2 */ },  
    () => { /* Task 3 */ }  
);
上一篇 下一篇

猜你喜欢

热点阅读