多线程编程是 C# 一个比较难且涵盖面比较广的知识点,本文整理仓促而且属于笔记向博客,有些地方必然还存在不严谨和错误,本人会在日后的使用过程中不断完善。如果发现问题或有改进意见可以在评论区提出,我会及时修改。
C# 多线程
线程是程序的执行流程,也被称为轻量级进程。一个进程中可以包含多个线程,每个线程有自己独立的运行堆栈和程序计数器,可以独立地执行不同的任务。
我们编写的程序到目前为止只有一个线程,作为应用程序的运行实例,也就是说它是一个单一的进程在运行。然而,这种方式只能同时执行一个任务,限制了应用程序的并发性能。
为了提高应用程序的效率,我们可以使用线程。通过将程序分为多个线程,可以同时处理多个任务,从而减少等待时间和提高程序的响应速度。线程还可以实现并发编程,充分利用多核处理器的性能,提高应用程序的运行效率。
线程的应用非常广泛,包括但不限于操作系统、Web服务器、数据库、游戏等。
1、线程的生命周期
线程生命周期开始于 System.Threading.Thread
类的对象被创建时,结束于线程被终止或完成执行时。下面列出了线程生命周期中的各种状态:
-
创建:使用
System.Threading
命名空间中的Thread
类创建线程实例。 - 就绪:线程实例被创建后,它进入就绪状态。在此状态下,操作系统为线程分配必要的资源,但线程实际上还没有开始执行。
-
运行:一旦调用线程的
Start()
方法,线程就会开始执行,并处于运行状态。在此状态下,线程开始执行它的方法体中的代码。 - 阻塞:线程可能会因为等待某些资源而被阻塞,例如等待用户输入、等待其他线程完成任务等。在此状态下,线程暂时停止执行,并将 CPU 时间片让给其他线程。
- 终止:当线程完成它的任务、遇到异常或被强制终止时,线程将终止。
2、线程的创建与管理
为了创建和控制线程,我们需要使用 System.Threading
命名空间,该命名空间提供了 Thread
、ThreadPool
、Timer
等等类和接口。通过这些类和接口,我们可以实现多线程编程的核心功能,例如创建和启动线程,管理线程的状态和优先级,控制线程同步和互斥,以及使用定时器来调度线程的执行。
2.1 线程的创建
C# 在 4.0 以后一共有3种创建线程的方式,交给线程执行的方法通过委托传递。
-
Thread 类:
Thread 类是 C# 中最基本的创建线程的方式。在使用 Thread 类时,需要创建一个 Thread 对象,并将要执行的方法包装在一个 ThreadStart 委托中。然后,可以调用 Thread 对象的 Start 方法来启动线程。
以下是使用 Thread 类创建线程的示例代码:
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread thread = new Thread(new ThreadStart(DoWork));
thread.Start();
// 主线程继续执行其他操作
}
static void DoWork()
{
// 线程要执行的任务
}
}
-
ThreadPool 类:
ThreadPool 类是 C# 中一个内置的线程池,可以用于管理和复用多个线程,从而避免频繁地创建和销毁线程,提高程序的性能和效率。在使用 ThreadPool 类时,需要将要执行的方法包装在一个 WaitCallback 委托中,并通过 ThreadPool.QueueUserWorkItem 方法将该委托添加到线程池中。
以下是使用 ThreadPool 类创建线程的示例代码:
using System;
using System.Threading;
class Program
{
static void Main()
{
ThreadPool.QueueUserWorkItem(new WaitCallback(DoWork));
// 主线程继续执行其他操作
}
static void DoWork(object state)
{
// 线程要执行的任务
}
}
-
Task 类:
Task 类是 C# 4.0 中引入的一种新的线程创建方式,使用 Task 类可以方便地创建异步任务,避免手动管理线程和线程池。在使用 Task 类时,需要创建一个 Task 对象,并将要执行的方法包装在一个 Action 委托中。然后,可以调用 Task 对象的 Start 或 Run 方法来启动线程。
以下是使用 Task 类创建线程的示例代码:
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
Task task = new Task(new Action(DoWork));
task.Start();
// 主线程继续执行其他操作
}
static void DoWork()
{
// 线程要执行的任务
}
}
如果使用 Task.Factory.StartNew()
方法创建任务,不需要调用 Start()
方法,因为任务会在创建后立即启动执行。例如:
Task myTask = Task.Factory.StartNew(DoWork);
创建线程时其实还有简洁写法,可以不需要在被执行方法外部使用 new 委托类型()
来包装。这是因为当被执行方法符合对应委托类型的签名时,编译器会隐式地将方法转换为委托。下面以Thread 类举例说明:
- 使用无参方法,可直接省略:
Thread t = new Thread(MyMethod);
t.Start();
- 使用有参方法,利用无参的 lambda 表达式(在代码块内传参):
Thread t = new Thread(() => MyMethodWithArgs(arg1, arg2));
t.Start();
- 使用有参方法,利用无参的匿名方法(在代码块内传参):
Thread t = new Thread(delegate() { MyMethodWithArgs(arg1, arg2); });
t.Start();
2.2 线程的管理
线程的管理涉及到 System.Threading
命名空间下相关类的方法和属性的使用,这里简单举例,在后面介绍相关类的时候进一步介绍。
-
Thread.Start()
方法:启动线程。 -
Thread.Join()
方法:等待线程执行完毕。 -
Thread.Sleep()
方法:暂停当前线程的执行一段时间。 -
Thread.Abort()
方法:强制终止线程的执行。 -
ThreadState
属性:表示线程的不同状态。
2.3 多线程实例
以下是一个简单的多线程编码实例,它创建了两个线程并在每个线程中执行不同的函数,以便同时执行多个任务:
using System;
using System.Threading;
class Program
{
static void Main()
{
// 创建第一个线程并启动
Thread thread1 = new Thread(new ThreadStart(DoTask1));
/* Thread thread1 = new Thread(DoTask1); */
thread1.Start();
// 创建第二个线程并启动
Thread thread2 = new Thread(new ThreadStart(DoTask2));
thread2.Start();
// 等待线程结束
thread1.Join();
thread2.Join();
Console.WriteLine("所有任务已完成");
}
static void DoTask1()
{
// 执行任务1的代码
Console.WriteLine("任务1开始执行...");
Thread.Sleep(3000);
Console.WriteLine("任务1执行完成");
}
static void DoTask2()
{
// 执行任务2的代码
Console.WriteLine("任务2开始执行...");
Thread.Sleep(5000);
Console.WriteLine("任务2执行完成");
}
}
在这个例子中,我们使用 Thread
类来创建和管理线程。我们创建了两个线程,每个线程都调用不同的函数(DoTask1
和 DoTask2
)来执行不同的任务。我们使用 Thread.Sleep()
方法来模拟每个任务的耗时,并使用 Join()
方法来等待每个线程的结束。最后,我们在主函数中输出一条消息来表示所有任务已完成。
3、Thread 类
Thread
类是 .NET 提供的用于创建和控制线程的基本类。使用 Thread
类可以在应用程序中创建新线程,同时控制线程的状态和执行。
下表列出了 Thread
类的一些常用的属性:
序号 | 属性 | 类型 | 描述 |
---|---|---|---|
1 | CurrentContext | System.Runtime.Remoting.Contexts.Context | 获取当前线程正在执行的上下文。上下文是一个逻辑容器,它定义了一个对象的代码执行上下文,包括对象所运行的代码、对象的安全策略和上下文中的属性。 |
2 | CurrentCulture | System.Globalization.CultureInfo | 用于确定在将数据类型转换为字符串时要使用的区域性。它主要影响数字和日期时间格式,以及货币和浮点数值的小数点和千位分隔符。 可以通过在程序中显式设置此属性来更改线程的当前区域性。 |
3 | CurrentPrincipal | System.Security.Principal.IPrincipal | 在 Windows 操作系统中,用户和组都有关联的安全令牌,这些安全令牌用于授予或拒绝对某些资源的访问权限。CurrentPrincipal 属性可用于检查当前线程的安全令牌,以确定线程是否有访问某个资源的权限。 可以使用该属性来更改当前线程的负责人,以便在运行时更改线程的权限。 |
4 | CurrentThread | System.Threading.Thread | 获取当前线程的 Thread 对象。 |
5 | CurrentUICulture | System.Globalization.CultureInfo | 用于确定在运行时加载资源文件时使用的区域性。该属性主要用于确定应使用哪个语言和文化特性来显示用户界面(UI)的文本,如按钮、菜单、对话框等。 可以在程序中显式设置此属性来更改线程的当前 UI 区域性。 |
6 | ExecutionContext | System.Threading.ExecutionContext | 获取一个 ExecutionContext 对象,该对象包含有关当前线程的各种上下文的信息。这个对象可以被用来在多个线程之间传递上下文信息。 |
7 | IsAlive | bool | 判断线程是否处于活动状态。如果线程已经启动并正在运行,则返回 true,否则返回 false。 |
8 | IsBackground | bool | 获取或设置一个值,该值指示某个线程是否为后台线程。 |
9 | IsThreadPoolThread | bool | 获取一个值,该值指示线程是否属于托管线程池。 |
10 | ManagedThreadId | int | 获取当前托管线程的唯一标识符。 |
11 | Name | string | 获取或设置线程的名称。 |
12 | Priority | System.Threading.ThreadPriority | 获取或设置一个值,该值指示线程的调度优先级。 |
13 | ThreadState | System.Threading.ThreadState | 获取线程的当前状态,它是一个枚举类型的值,包括 Unstarted 、Running 、Stopped 、WaitSleepJoin 等状态。 |
以下是一个 Thread
类使用上述提到的部分属性的示例:
using System;
using System.Globalization;
using System.Threading;
class Program
{
static void Main()
{
// 获取当前线程
Thread currentThread = Thread.CurrentThread;
// 设置线程名称
currentThread.Name = "MyThread";
// 获取和设置线程优先级
currentThread.Priority = ThreadPriority.Highest;
// 获取线程的唯一标识符
int threadId = currentThread.ManagedThreadId;
Console.WriteLine($"Thread ID: {threadId}");
// 获取和设置线程的后台标志
currentThread.IsBackground = true;
// 获取线程状态
ThreadState threadState = currentThread.ThreadState;
Console.WriteLine($"Thread state: {threadState}");
// 获取和设置线程的区域性
CultureInfo cultureInfo = new CultureInfo("en-US");
currentThread.CurrentCulture = cultureInfo;
// 获取和设置线程的 UI 区域性
CultureInfo uiCultureInfo = new CultureInfo("fr-FR");
currentThread.CurrentUICulture = uiCultureInfo;
// 获取和设置线程的当前负责人(对基于角色的安全性而言)
AppDomain.CurrentDomain.SetPrincipalPolicy(System.Security.Principal.PrincipalPolicy.WindowsPrincipal);
Thread.CurrentPrincipal = new System.Security.Principal.WindowsPrincipal(System.Security.Principal.WindowsIdentity.GetCurrent());
// 获取和设置线程的 ExecutionContext 对象
ExecutionContext executionContext = ExecutionContext.Capture();
currentThread.ExecutionContext = executionContext;
// 输出线程信息
Console.WriteLine($"Thread name: {currentThread.Name}");
Console.WriteLine($"Thread priority: {currentThread.Priority}");
Console.WriteLine($"Thread background: {currentThread.IsBackground}");
Console.WriteLine($"Thread culture: {currentThread.CurrentCulture}");
Console.WriteLine($"Thread UI culture: {currentThread.CurrentUICulture}");
Console.WriteLine($"Thread principal: {Thread.CurrentPrincipal}");
Console.WriteLine($"Thread execution context: {currentThread.ExecutionContext}");
}
}
这个示例代码演示了如何使用 Thread
类的各种实例属性。它创建了一个新的线程,将线程的名称设置为 "MyThread"
,优先级设置为最高,将线程设置为后台线程,并获取了线程的唯一标识符和状态。此外,示例还演示了如何获取和设置线程的区域性、UI 区域性、当前负责人和 ExecutionContext 对象。最后,示例输出了所有线程信息。
下表列出了 Thread
类的一些常用的方法:
序号 | 方法名 | 描述 |
---|---|---|
1 | public void Abort()
|
在调用此方法的线程上引发 ThreadAbortException 异常,以开始终止此线程的过程。调用此方法通常会销毁线程,并触发 finally 块。 |
2 | public static LocalDataStoreSlot AllocateDataSlot()
|
在所有的线程上分配未命名的数据槽。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段。 |
3 | public static LocalDataStoreSlot AllocateNamedDataSlot( string name)
|
在所有线程上分配已命名的数据槽。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段。 |
4 | public static void BeginCriticalRegion()
|
通知主机执行将要进入一个代码区域,在该代码区域内线程中止或未经处理的异常的影响可能会危害应用程序域中的其他任务。 |
5 | public static void BeginThreadAffinity()
|
通知主机托管代码将要执行依赖于当前物理操作系统线程的标识的指令。 |
6 | public static void EndCriticalRegion()
|
通知主机执行将要进入一个代码区域,在该代码区域内线程中止或未经处理的异常仅影响当前任务。 |
7 | public static void EndThreadAffinity()
|
通知主机托管代码已执行完依赖于当前物理操作系统线程的标识的指令。 |
8 | public static void FreeNamedDataSlot(string name)
|
为进程中的所有线程消除名称与槽之间的关联。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段。 |
9 | public static Object GetData( LocalDataStoreSlot slot )
|
在当前线程的当前域中从当前线程上指定的槽中检索值。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段。 |
10 | public static AppDomain GetDomain()
|
返回当前线程正在其中运行的当前域。 |
11 | public static AppDomain GetDomainID()
|
返回唯一的应用程序域标识符。 |
12 | public static LocalDataStoreSlot GetNamedDataSlot( string name )
|
查找已命名的数据槽。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段。 |
13 | public void Interrupt()
|
中断处于 WaitSleepJoin 线程状态的线程。 |
14 | public void Join()
|
在继续执行标准的 COM 和 SendMessage 消息泵处理期间,阻塞调用线程,直到某个线程终止为止。此方法有不同的重载形式。 |
15 | public static void MemoryBarrier()
|
同步内存存取:执行当前线程的处理器在对指令重新排序时,不能采用先执行 MemoryBarrier 调用之后的内存存取,再执行 MemoryBarrier 调用之前的内存存取的方式。 |
16 | public static void ResetAbort()
|
取消为当前线程请求的 Abort。 |
17 | public void Resume()
|
恢复挂起的线程。(已和 Suspend 一同被废弃) |
18 | public static void SetData(LocalDataStoreSlot slot, Object data)
|
在当前正在运行的线程上为此线程的当前域在指定槽中设置数据。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段。 |
19 | public void Start()
|
开始一个线程。 |
20 | public static void Sleep(int millisecondsTimeout)
|
让线程暂停一段时间,注意时间单位是毫秒。 |
21 | public void Suspend()
|
挂起线程。(已被废弃,因为容易导致死锁和应用程序崩溃) |
22 | public static void SpinWait(int iterations)
|
导致线程等待由 iterations 参数定义的时间量。 |
23 | 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)
|
读取字段值。无论处理器的数目或处理器缓存的状态如何,该值都是由计算机的任何处理器写入的最新值。此方法有不同的重载形式,这里只给出了一些形式。 |
24 | 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)
|
立即向字段写入一个值,以使该值对计算机中的所有处理器都可见。此方法有不同的重载形式。这里只给出了一些形式。 |
25 | public static bool Yield()
|
导致调用线程执行准备好在当前处理器上运行的另一个线程。由操作系统选择要执行的线程。 |
下面是一个 Thread
类使用上述提到的部分方法和属性的示例代码:
using System;
using System.Threading;
public class Example
{
public static void Main()
{
// 创建一个新的线程,并指定线程函数
Thread t = new Thread(new ThreadStart(ThreadFunction));
t.Name = "MyThread";
t.Priority = ThreadPriority.Highest;
// 启动线程
t.Start();
// 当前线程等待子线程完成
t.Join();
// 给子线程发送中断信号
t.Interrupt();
// 判断线程是否仍在运行
if (t.IsAlive)
{
Console.WriteLine("Thread is still running.");
}
/* 挂起线程
t.Suspend();
Console.WriteLine("Thread is suspended."); */
/* 恢复线程
t.Resume();
Console.WriteLine("Thread is resumed."); */
// 终止线程
t.Abort();
}
public static void ThreadFunction()
{
try
{
// 休眠 2 秒
Thread.Sleep(2000);
// 输出线程名称和优先级
Console.WriteLine("Thread name: {0}", Thread.CurrentThread.Name);
Console.WriteLine("Thread priority: {0}", Thread.CurrentThread.Priority);
}
catch (ThreadInterruptedException e)
{
Console.WriteLine(e);
}
}
}
这个示例代码创建了一个新的线程,并指定了一个线程函数 ThreadFunction
。
4、ThreadPool 类
ThreadPool
类是 C# 中用于管理线程池的类。它可以使程序员更方便地管理多个线程,而无需手动创建和销毁线程。
-
线程池(ThreadPool)是一种可重用的线程集合,它可以执行多个任务,而不必在每个任务完成后启动新线程。
-
线程池在应用程序启动时会创建一定数量的线程,并将它们保存在线程池中,这些线程称为工作线程。当需要执行任务时,应用程序会将任务添加到线程池的队列中,线程池会自动分配空闲的工作线程来执行这些任务。当任务完成后,工作线程会返回到线程池中,等待下一个任务的分配。
-
使用线程池可以避免频繁地创建和销毁线程,从而减少系统开销和资源浪费。线程池还可以控制同时执行的线程数,防止系统过载,提高应用程序的稳定性和可靠性。
下表列出了 ThreadPool
类的一些常用的属性:
常用属性
序号 | 属性 | 类型 | 描述 |
---|---|---|---|
1 | AvailableThreads | int | 获取可用于执行工作项的空闲工作线程数 |
2 | CompletedWorkItemCount | int | 获取已完成的工作项数量 |
3 | IsThreadPoolThread | bool | 获取当前线程是否属于线程池 |
4 | MaxThreads | int | 获取或设置线程池可同时拥有的最大工作线程数。 |
5 | MinThreads | int | 获取或设置线程池可同时拥有的最小工作线程数。 |
6 | PendingWorkItemCount | int | 获取等待执行的工作项数量 |
7 | ThreadCount | int | 获取当前正在运行的线程池中的工作线程数 |
下表列出了 ThreadPool
类的一些常用的方法:
序号 | 方法名 | 描述 |
---|---|---|
1 | public static bool QueueUserWorkItem(WaitCallback callBack)
|
将方法排队到线程池的工作队列中,等待执行。 |
2 | public static bool QueueUserWorkItem(WaitCallback callBack, object state)
|
将带有状态信息的工作项排入线程池的工作队列中,等待执行。 |
3 | public static void GetMaxThreads(out int workerThreads, out int completionPortThreads)
|
检索可同时处于活动状态的线程数的最大值。 |
4 | public static void GetMinThreads(out int workerThreads, out int completionPortThreads)
|
检索线程池允许的最小线程数。 |
5 | public static bool SetMaxThreads(int workerThreads, int completionPortThreads)
|
设置线程池中允许的最大线程数和异步 I/O 线程数。 |
6 | static bool SetMinThreads(int workerThreads, int completionPortThreads)
|
设置线程池中允许的最小线程数和异步 I/O 线程数。 |
7 | public static bool UnsafeQueueNativeOverlapped(NativeOverlapped* overlapped, IOCompletionCallback iocb, object state)
|
使用指定的回调函数将 NativeOverlapped 结构排队到线程池以便进行异步 I/O 操作。 |
8 | public static bool UnsafeQueueUserWorkItem(WaitCallback callBack, object state)
|
将工作项排入线程池的工作队列中等待执行,但不强制线程池线程在调用方法之前获得安全上下文。 |
下面是一个示例代码:
using System;
using System.Threading;
class Program
{
static void Main()
{
// 获取当前线程池的最大线程数和空闲线程数
int maxThreads, availableThreads;
ThreadPool.GetMaxThreads(out maxThreads, out _); // 获取最大线程数
ThreadPool.GetAvailableThreads(out availableThreads, out _); // 获取可用线程数
Console.WriteLine($"MaxThreads: {maxThreads}, AvailableThreads: {availableThreads}");
// 提交工作项到线程池
ThreadPool.QueueUserWorkItem(WorkItemCallback, "Hello, world!"); // 将工作项提交到线程池中
Console.WriteLine("Work item submitted to thread pool");
// 等待工作项完成
EventWaitHandle waitHandle = new ManualResetEvent(false); // 创建一个事件等待句柄
ThreadPool.QueueUserWorkItem(_ => {
Console.WriteLine("Working on a task...");
Thread.Sleep(1000); // 模拟长时间运行的工作项
Console.WriteLine("Task completed!");
waitHandle.Set(); // 通知等待句柄任务已完成
});
waitHandle.WaitOne(); // 等待等待句柄收到通知
Console.WriteLine("Task has finished");
// 等待所有工作项完成
ManualResetEvent allTasksCompleted = new ManualResetEvent(false); // 创建一个事件等待句柄
for (int i = 0; i < 10; i++)
{
ThreadPool.QueueUserWorkItem(_ => {
Console.WriteLine($"Working on task {i}...");
Thread.Sleep(1000); // 模拟长时间运行的工作项
Console.WriteLine($"Task {i} completed!");
if (Interlocked.Decrement(ref remainingTasks) == 0) // 如果剩余任务数量为0,则通知所有任务已完成
{
allTasksCompleted.Set();
}
});
}
allTasksCompleted.WaitOne(); // 等待所有任务完成
Console.WriteLine("All tasks have completed");
// 等待线程池中的所有线程退出
ThreadPool.SetMaxThreads(0, 0); // 设置最大线程数为0
ThreadPool.SetMinThreads(0, 0); // 设置最小线程数为0
while (ThreadPool.ThreadCount > 0) // 循环等待线程池中的线程全部退出
{
Console.WriteLine($"Waiting for {ThreadPool.ThreadCount} threads to exit...");
Thread.Sleep(1000);
}
Console.WriteLine("All threads have exited");
}
private static int remainingTasks = 10; // 剩余任务数量
private static void WorkItemCallback(object state)
{
Console.WriteLine($"Working on {state}...");
Thread.Sleep(1000); // 模拟长时间运行的工作项
Console.WriteLine($"{state} completed!");
}
}
上述代码存在 QueueUserWorkItem
方法与 lambda 表达式的结合使用:
- 我们通过 lambda 表达式声明了一个匿名函数,作为参数传递给
QueueUserWorkItem
方法,其中_
是一个占位符,表示 lambda 表达式不需要任何参数。
ThreadPool.QueueUserWorkItem( _ => {
Console.WriteLine("Working on a task...");
Thread.Sleep(1000); // 模拟长时间运行的工作项
Console.WriteLine("Task completed!");
waitHandle.Set(); // 通知等待句柄任务已完成
});
5、Task 类
Task
类是 .NET Framework 中用于支持多线程编程的类之一。它提供了一种异步编程模型,可以在一个线程中执行多个操作,从而提高程序的并发性和效率。Task 类的实例代表一个异步操作,可以使用 Task 对象来监视和控制异步操作的执行过程。
-
Task
类是泛型类,它可以实例化出具有特定类型返回值的任务。如果不指定类型参数,会使用Task<object>
作为默认类型。 -
Task
类的实例代表一个异步操作,例如一个 I/O 操作或者一个计算密集型的操作。 - 可以使用 Task 对象来监视和控制异步操作的执行过程。在异步执行期间,我们可以通过轮询 Task 对象来检查操作的进度,或者在操作完成时通过 Task 对象来获取操作的结果。
下表列出了 ThreadPool
类的一些常用的属性:
序号 | 属性 | 类型 | 描述 |
---|---|---|---|
1 | Id | int | 获取任务的唯一标识符。 |
2 | Status | TaskStatus | 获取任务的当前执行状态。 |
3 | Exception | Exception | 获取表示任务执行期间引发的任何异常。 |
4 | CreationOptions | TaskCreationOptions | 获取任务创建时使用的 TaskCreationOptions。 |
5 | AsyncState | Object | 获取任务关联的异步状态对象。 |
6 | IsCanceled | bool | 获取一个值,该值指示任务是否已被取消。 |
7 | IsCompleted | bool | 获取一个值,该值指示任务是否已完成执行。 |
8 | IsFaulted | bool | 获取一个值,该值指示任务是否已发生故障。 |
9 | Factory | TaskFactory | 获取用于创建和调度此任务的 TaskFactory。 |
下表列出了 ThreadPool
类的一些常用的方法:
序号 | 方法 | 描述 |
---|---|---|
1 | public void Start()
|
启动当前任务实例。 |
2 | public static Task Delay(int millisecondsDelay)
|
返回已启动并在指定时间过后完成的延迟任务。 |
3 | public static Task Delay(TimeSpan delay)
|
(重载)返回已启动并在指定时间过后完成的延迟任务。 |
4 | public static Task Run(Action action)
|
在默认任务调度程序上运行指定的操作。 |
5 | public static Task Run<TResult>(Func<TResult> function)
|
(重载)在默认任务调度程序上运行指定的函数,并返回一个带有返回值的任务对象。 |
6 | public static Task WhenAll(params Task[] tasks)
|
创建一个任务,该任务在所有提供的任务完成时完成。 |
7 | public static Task WhenAny(params Task[] tasks)
|
(重载)创建一个任务,该任务在任何提供的任务完成时完成。 |
8 | public void Wait()
|
阻止当前线程,直到当前任务完成。 |
9 | public bool Wait(int millisecondsTimeout)
|
(重载)阻止当前线程,直到当前任务完成或等待超时。 |
10 | public bool Wait(TimeSpan timeout)
|
(重载)阻止当前线程,直到当前任务完成或等待超时。 |
11 | public static Task FromResult<TResult>(TResult result)
|
创建一个已完成的任务,并返回指定的结果。 |
12 | public void ContinueWith(Action<Task> continuationAction)
|
创建一个新的任务,该任务在当前任务完成时运行指定的操作。 |
13 | public Task ContinueWith(Func<Task, TResult> continuationFunction)
|
(重载)创建一个新的任务,该任务在当前任务完成时运行指定的函数,并返回一个带有返回值的任务对象。 |
14 | public Task ContinueWith(Func<Task, TResult> continuationFunction, CancellationToken cancellationToken)
|
(重载)创建一个新的任务,该任务在当前任务完成时运行指定的函数,并返回一个带有返回值的任务对象,同时可以通过指定的 CancellationToken 请求取消操作。 |
15 | public Task ContinueWith(Func<Task, TResult> continuationFunction, TaskContinuationOptions continuationOptions)
|
(重载)创建一个新的任务,该任务在当前任务完成时运行指定的函数,并返回一个带有返回值的任务对象,并提供选项以控制新任务的行为。 |
在使用 Task 类创建线程时,可以使用 Start
方法或 Run
方法来启动线程,二者有以下区别:
-
启动方式:
Start
方法会在新线程上执行任务,而Run
方法则是在当前线程上同步执行任务。 -
线程数量:
Start
方法会创建新线程并在其中执行任务,而Run
方法只会在当前线程上执行任务,不会创建新线程。 -
调用次数:
Run
方法可以被多次执行,而Start
方法只能被执行一次,因为线程不能被重复启动。 -
作用:
Run
方法存放任务代码,而Start
方法同时创建线程并执行任务。
需要注意的是,使用 Start
方法启动的线程可能会在当前线程之后才开始执行,因为它需要等待 CPU 时间片的分配。而使用 Run
方法则会立即执行任务,直到任务执行完毕才会继续执行下面的代码。
此外,使用 Task.Run
方法也可以创建新线程并在其中执行任务,与 Task.Start
方法类似。但是 Task.Run
方法会返回一个 Task 对象,该对象可以用于等待任务执行完毕或者监视任务执行状态。
以下是一个使用 Task
类的简单示例代码,该示例创建了一个异步任务来计算数组中所有元素的平均值:
using System;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
// 定义整数数组
int[] arr = { 1, 2, 3, 4, 5 };
// 创建一个Task实例,计算整数数组的平均值
Task<double> task = Task.Run(() => {
double sum = 0;
foreach (int num in arr) {
sum += num;
}
return sum / arr.Length;
});
// 输出提示信息
Console.WriteLine($"Calculating average of array...");
// 输出Task实例返回的结果(平均值)
Console.WriteLine($"Average: {task.Result}");
}
}
在上面的代码中,首先定义了一个整数数组 arr
,然后创建了一个 Task
对象,该对象使用 Task.Run()
方法执行一个无参数 Lambda 表达式。Lambda 表达式计算 arr
中所有元素的平均值,然后将结果作为 Task<double>
对象的返回值。
在执行任务期间,Main()
方法可以继续执行其他操作。当需要获取任务的结果时,可以使用 Task.Result
属性,该属性将等待任务完成并返回其结果。
上述代码运行后将输出以下内容:
Calculating average of array…
Average: 3
需要注意的是,如果任务执行过程中发生了异常,则可以通过 Task.Exception
属性获取异常信息。可以通过 Task.IsFaulted
属性来检查任务是否失败。
6、Timer 类
Timer
类允许我们在一段时间间隔内重复执行某些代码,可以使用 Timer
类的构造函数创建计时器。
- 在初始化
Timer
实例时,可以指定一个回调函数来执行代码,以及第一次回调的等待时间和回调间隔时间。然后,Timer
类将在指定的时间间隔内重复执行回调函数。 - 除此之外,
Timer
类还有一些可用于更改回调时间或释放资源的方法。
下表列出了 Timer
类的创建语法,以及一些常用的方法:
序号 | 语法格式 | 描述 |
---|---|---|
1 | public Timer(TimerCallback callback, object state, int dueTime, int period)
|
初始化一个新实例的Timer类,并设置回调方法、传递对象、第一次回调等待时间和回调间隔时间。 |
2 | public void Change(int dueTime, int period)
|
更改Timer的第一次回调等待时间和回调间隔时间。 |
3 | public bool Change(int dueTime, int period, bool firstTime)
|
(重载)更改Timer的第一次回调等待时间和回调间隔时间,并决定是否更改第一次回调等待时间。 |
4 | public void Change(TimeSpan dueTime, TimeSpan period)
|
(重载)更改Timer的第一次回调等待时间和回调间隔时间。 |
5 | public bool Change(TimeSpan dueTime, TimeSpan period, bool firstTime)
|
(重载)更改Timer的第一次回调等待时间和回调间隔时间,并决定是否更改第一次回调等待时间。 |
6 | public void Dispose()
|
释放Timer类占用的所有资源。 |
7 | public bool Dispose(WaitHandle notifyObject)
|
(重载)释放Timer类占用的所有资源,并在释放之前等待异步操作完成。 |
8 | public static Timer Synchronized(Timer timer)
|
返回一个包装的Timer对象,该对象在多线程环境中执行回调方法时使用指定的Timer对象的同步上下文。 |
9 | public bool WaitForDispose(WaitHandle waitHandle)
|
等待Timer类释放所有资源,并在等待时使用指定的WaitHandle对象。 |
下面是一个使用 Timer
类的示例代码:
using System;
using System.Threading;
class Program
{
static void Main()
{
// 创建 TimerCallback 委托对象,指向 PrintTime 方法
TimerCallback callback = new TimerCallback(PrintTime);
// 创建 Timer 对象并启动定时器,第一个参数是回调函数,第二个参数传递给回调函数的状态信息,第三个参数是时间间隔,第四个参数是定时器第一次执行的延迟时间
Timer timer = new Timer(callback, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
// 等待用户按下任意键终止定时器
Console.WriteLine("Press any key to stop the timer");
Console.ReadKey();
// 销毁定时器
timer.Dispose();
Console.WriteLine("Timer stopped");
}
// 定义回调函数 PrintTime
static void PrintTime(object state)
{
Console.WriteLine(DateTime.Now.ToString("hh:mm:ss"));
}
}
该程序使用 Timer
类每秒打印当前时间。在 Main
方法中,首先创建一个 TimerCallback
委托,该委托指向一个名为 PrintTime
的方法。
然后,创建一个 Timer
实例,并传入 callback
和两个 TimeSpan
参数。第一个参数表示多长时间后开始计时,TimeSpan.Zero
表示立即开始计时。第二个参数表示每隔多长时间执行一次回调函数,TimeSpan.FromSeconds(1)
表示每秒执行一次回调函数。
在程序运行时,通过 Console.ReadKey() 暂停程序,等待用户按下任意键。一旦用户按下任意键,程序将释放 timer 并停止计时。
7、线程同步的相关类
在使用线程时,需要注意线程同步和线程安全问题,避免出现线程间数据竞争和死锁等问题。可以使用 Monitor
、Mutex
、Semaphore
等类创建同步对象来确保线程安全。
7.1 Monitor 类
Monitor 是 C# 中最基本的同步对象,是一个内置的互斥锁定对象,由 System.Threading.Monitor
类实现。Monitor 对象用于限制在同一时间内只有一个线程可以访问共享资源。
- Monitor 的主要作用是提供一个独占锁来确保线程安全。使得在同一时刻只有一个线程可以进入代码块,进而保证了代码块的互斥性。当一个线程进入被 Monitor 锁定的代码块时,其他线程将被阻塞,直到该线程退出代码块。
下列是常用的一些方法:文章来源地址https://www.toymoban.com/news/detail-658579.html
序号 | 方法 | 描述 |
---|---|---|
1 | Enter(object obj) | 获取指定对象的互斥锁,如果对象已经被其他线程锁定,则当前线程会阻塞直到对象被释放。 |
2 | TryEnter(object obj, int timeout) | 获取指定对象的互斥锁,如果对象已经被其他线程锁定,则当前线程会阻塞,但是最多阻塞指定的超时时间,超时后该方法将返回 false。 |
3 | Exit(object obj) | 释放指定对象的互斥锁,如果当前线程并没有获取到该对象的互斥锁,则会抛出 SynchronizationLockException 异常。 |
7.2 Mutex 类
Mutex 是一种系统级别的内核对象,由 System.Threading.Mutex
类实现。Mutex 对象是一种可命名的同步对象,可以跨进程共享。它可以保护多个线程同时访问共享资源,即多线程编程中的互斥锁。
- Mutex 与 Monitor 的最大区别在于,Mutex 可以跨进程共享,而 Monitor 只能在同一个进程内的线程间共享。Mutex 还可以防止饥饿状态的发生,即等待较长时间但一直未被允许进入临界区的线程不会无限期地等待下去。
下列是常用的一些方法:
序号 | 方法 | 描述 |
---|---|---|
1 | WaitOne() | 请求获取 Mutex 对象的所有权,如果该 Mutex 对象已被其他线程占用,则当前线程将被阻塞直到 Mutex 对象被释放。 |
2 | WaitOne(int millisecondsTimeout) | 请求获取 Mutex 对象的所有权,如果该 Mutex 对象已被其他线程占用,则当前线程将被阻塞,但最多等待指定的超时时间,超时后该方法将返回 false。 |
3 | ReleaseMutex() | 释放 Mutex 对象的所有权,如果当前线程没有获取到该 Mutex 对象的所有权,则会抛出一个异常。 |
7.3 Semaphore 类
Semaphore 也是一种同步对象,由 System.Threading.Semaphore
类实现。Semaphore 对象也是用于保护多个线程同时访问一组资源,控制对某个共享资源的访问数量。Semaphore 的使用场景包括线程池和限制并发请求等。文章来源:https://www.toymoban.com/news/detail-658579.html
- Semaphore 类中有一个计数器,初始值表示同时可访问该资源的线程数量。每当有一个线程访问该资源时,计数器的值就会减一。当计数器的值为 0 时,后续访问该资源的线程将会被阻塞,直到有一个线程释放该资源,计数器的值才会加一。
下列是常用的一些方法:
序号 | 方法 | 描述 |
---|---|---|
1 | WaitOne() | 请求获取 Semaphore 对象的访问令牌,如果当前 Semaphore 对象已被其他线程占用,则当前线程将被阻塞直到 Semaphore 对象被释放。 |
2 | WaitOne(int millisecondsTimeout) | 请求获取 Semaphore 对象的访问令牌,如果当前 Semaphore 对象已被其他线程占用,则当前线程将被阻塞,但最多等待指定的超时时间,超时后该方法将返回 false。 |
3 | Release() | 释放 Semaphore 对象的一个访问令牌,如果当前线程没有获取到该 Semaphore 对象的访问令牌,则会抛出一个异常。 |
到了这里,关于unity的C#学习——多线程编程(线程的生命周期、创建与管理)与线程相关类的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!