一、基础概念
1. 、线程和进程
进程:是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。
线程:是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发。
是不是有点抽象?没关系,我们拿王者荣耀来举个例子,比如我现在开了一把游戏,我这把游戏就可以看成是一个进程,而在这把游戏里的英雄,小兵,野怪等局内的元素就可以看作一个个线程。所以,多线程的概念也就呼之欲出了:一个进程里可以有多个线程就叫多线程。
需要注意的是,进程和线程还有下面这些特点:
1、线程在进程下进行
(单独的英雄角色、野怪、小兵肯定不能运行)
2、进程之间不会相互影响,主线程结束将会导致整个进程结束
(你和你朋友没人开一把游戏,你超神了对他那局游戏也起不到任何影响,水晶(主线程)推了游戏就结束了)
3、不同的进程数据很难共享
(两局游戏之间很难有联系,你无法在你的那局游戏里看到另外一局游戏的数据,除非王者出bug(串麦!!!))
4、同进程下的不同线程之间数据很容易共享
(同一局游戏里你可以看到队友和对手状态,血量,装备)
5、进程使用内存地址可以限定使用量
(三排、五排房间满了以后就没法再邀请玩家进如,只能有玩家退出房间后才能进入)
2 、线程的生命周期
线程的生命周期可以分为以下几个阶段:
创建(Created):线程对象被创建,但尚未开始执行。
就绪(Ready):线程已经准备好执行,但还没有被调度执行。
运行(Running):线程正在执行其任务。
阻塞(Blocked):线程被阻塞,暂时停止执行,等待某个条件满足或者等待某个资源可用。
终止(Terminated):线程执行完毕或者被强制终止,结束其生命周期。
在正常情况下,线程的生命周期是从创建开始,经过就绪、运行和阻塞等状态,最终结束于终止状态。然而,线程的生命周期也可能被一些特殊情况中断或改变,例如线程被中断、线程异常终止等
需要注意的是,线程的生命周期是由操作系统的调度器控制的,具体的状态转换和调度行为可能因操作系统的不同而有所差异。在C#中,可以使用Thread类来创建和管理线程,通过调用Thread类的方法和属性可以控制线程的生命周期。
3、 线程同步和互斥
线程同步:线程同步是指多个线程之间协调和同步执行的机制。由于多个线程同时访问共享资源可能会导致数据不一致或竞态条件等问题,因此需要使用线程同步来确保线程之间的顺序和正确性。
线程互斥:线程互斥是指多个线程对共享资源的访问进行协调和控制,以避免并发访问导致的数据不一致或竞态条件等问题。
*C#提供了多种机制来实现线程互斥,其中最常用的是互斥锁(Mutex)和监视器(Monitor)。
二、多线程的使用
1 、Thread类
1.1、常用属性
- Name:获取或设置线程的名称。可以用于标识和区分不同的线程。
- IsAlive:获取一个值,指示线程是否处于活动状态。
- IsBackground:获取或设置一个值,指示线程是否为后台线程。后台线程在主线程结束时会自动终止。
- Priority:获取或设置线程的优先级。可以设置为ThreadPriority枚举中的一个值,如ThreadPriority.Lowest(最低)、ThreadPriority.BelowNormal(低于正常)、ThreadPriority.Normal(正常)、ThreadPriority.AboveNormal(高于正常)、ThreadPriority.Highest(最高)。
- ThreadState:获取线程的当前状态,返回一个ThreadState枚举中的值,如ThreadState.Running、ThreadState.Stopped、ThreadState.WaitSleepJoin等。
- CurrentThread:获取当前正在执行的线程的Thread对象。
- ManagedThreadId:获取线程的唯一标识符。
- ApartmentState:获取或设置线程的单元状态。可以设置为
- ApartmentState枚举中的一个值,如ApartmentState.STA(单线程单元状态)或ApartmentState.MTA(多线程单元状态)。
1.2、常用方法
(1). start():启动线程,使其进入可运行状态。
(2). run():定义线程的执行逻辑,需要在子类中重写。
(3). sleep(long millis):使当前线程休眠指定的毫秒数。
(4). join():等待该线程执行完毕。
(5). interrupt():中断线程,给线程发送一个中断信号。
(6). isInterrupted():判断线程是否被中断。
(7). getName():获取线程的名称。
(8). setName(String name):设置线程的名称。
(9). isAlive():判断线程是否还活着。
(10). yield():暂停当前正在执行的线程,让其他线程有机会执行。
(11). setPriority(int priority):设置线程的优先级。
(12). getPriority():获取线程的优先级。
(13). currentThread():获取当前正在执行的线程对象。
(14). getState():获取线程的状态。
(15). setDaemon(boolean on):设置线程是否为守护线程。
*这些方法可以通过Thread类的实例对象调用,用于控制线程的执行和状态。
using System;
using System.Threading;
class Program
{
static void Main()
{
// 创建两个线程
Thread thread1 = new Thread(PrintThreadId);
Thread thread2 = new Thread(PrintThreadId);
// 启动线程
thread1.Start();
thread2.Start();
// 等待所有线程完成
thread1.Join();
thread2.Join();
Console.WriteLine("所有线程已完成。");
}
// 线程要执行的函数
static void PrintThreadId()
{
Console.WriteLine($"线程ID: {Thread.CurrentThread.ManagedThreadId}");
}
}
2 、ThreadPool
2.1、ThreadPool类的使用方法
QueueUserWorkItem:将工作项添加到线程池的队列中,以便由线程池中的线程执行。
ThreadPool.QueueUserWorkItem(DoWork, data);
GetMaxThreads:获取线程池允许的最大工作线程数和最大异步 I/O 线程数。
int maxWorkerThreads, maxIOThreads;
ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxIOThreads);
GetMinThreads:获取线程池的最小工作线程数和最小异步 I/O 线程数。
int minWorkerThreads, minIOThreads;
ThreadPool.GetMinThreads(out minWorkerThreads, out minIOThreads);
SetMaxThreads:设置线程池允许的最大工作线程数和最大异步 I/O 线程数。
ThreadPool.SetMaxThreads(maxWorkerThreads, maxIOThreads);
SetMinThreads:设置线程池的最小工作线程数和最小异步 I/O 线程数。
ThreadPool.SetMinThreads(minWorkerThreads, minIOThreads);
GetAvailableThreads:获取线程池中可用的工作线程数和可用的异步 I/O 线程数。
int availableWorkerThreads, availableIOThreads;
ThreadPool.GetAvailableThreads(out availableWorkerThreads, out availableIOThreads);
UnsafeQueueUserWorkItem:与 QueueUserWorkItem 类似,但不会捕获异常。
ThreadPool.UnsafeQueueUserWorkItem(DoWork, data);
2.2、如何使用线程池来管理和复用线程资源
使用线程池来管理和复用线程资源,以提高多线程程序的性能和效率。以下是使用线程池的步骤:
1.使用ThreadPool类的静态方法QueueUserWorkItem来将工作项添加到线程池中。例如:
ThreadPool.QueueUserWorkItem(DoWork, data);
*其中,DoWork是一个方法,用于执行具体的工作任务,data是传递给工作方法的参数。
2.定义工作方法,该方法会在线程池中的线程上执行。例如:
private static void DoWork(object state)
{
// 执行具体的工作任务
}
3.可以使用WaitHandle类的实现类(如ManualResetEvent、AutoResetEvent)来等待线程池中的工作项完成。例如:
ManualResetEvent waitHandle = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem(DoWork, waitHandle);
// 等待工作项完成
waitHandle.WaitOne();
4.可以使用ThreadPool.GetAvailableThreads方法获取线程池中可用的线程数量。例如:
int workerThreads;
int completionPortThreads;
ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);
5.可以使用ThreadPool.SetMaxThreads方法设置线程池中的最大线程数量。例如:
int workerThreads = 100;
int completionPortThreads = 100;
ThreadPool.SetMaxThreads(workerThreads, completionPortThreads);
通过设置最大线程数量,可以控制线程池中的线程数量,避免线程过多导致系统资源耗尽。
使用线程池可以方便地管理和复用线程资源,避免频繁地创建和销毁线程,提高多线程程序的性能和效率。但需要注意,线程池中的线程是共享的,如果某个工作项执行时间过长,可能会影响其他工作项的执行。因此,在使用线程池时,需要合理安排工作项的执行顺序和时间,避免长时间占用线程池中的线程。
2.3、如何提交任务
使用 ThreadPool 类的 QueueUserWorkItem 方法来提交任务到线程池。以下是一个示例:
// 定义一个方法来执行任务
void DoWork(object state)
{
// 执行任务的代码
Console.WriteLine("Task executed: " + state.ToString());
}
// 提交任务到线程池
ThreadPool.QueueUserWorkItem(DoWork, "Task 1");
ThreadPool.QueueUserWorkItem(DoWork, "Task 2");
ThreadPool.QueueUserWorkItem(DoWork, "Task 3");
在上面的示例中,我们定义了一个名为 DoWork 的方法来执行任务。然后,我们使用 ThreadPool 的 QueueUserWorkItem 方法将任务提交到线程池。每个任务都会在线程池中的一个可用线程上执行。
在 QueueUserWorkItem 方法中,第一个参数是要执行的方法,第二个参数是传递给方法的参数。在上面的示例中,我们将字符串作为参数传递给 DoWork 方法。
当任务被提交到线程池时,线程池会自动选择一个可用的线程来执行任务。任务的执行顺序和线程的分配是由线程池来管理的。
2.4、如何设置最大线程数
使用 ThreadPool 类的 SetMaxThreads 方法来设置线程池的最大线程数。以下是一个示例:
// 设置线程池的最大线程数
int workerThreads;
int completionPortThreads;
ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads);
ThreadPool.SetMaxThreads(10, completionPortThreads);
在上面的示例中,我们首先使用 ThreadPool 的 GetMaxThreads 方法获取当前线程池的最大线程数。然后,我们使用 ThreadPool 的 SetMaxThreads 方法来设置新的最大线程数。在这里,我们将最大线程数设置为 10,而保持 completionPortThreads 不变。
需要注意的是,SetMaxThreads 方法的第一个参数是工作线程的最大数目,第二个参数是异步 I/O 线程的最大数目。在大多数情况下,我们只需要设置工作线程的最大数目,而将异步 I/O 线程的最大数目保持不变。
请注意,设置线程池的最大线程数是一个全局设置,会影响整个应用程序中使用线程池的所有任务。因此,需要谨慎设置最大线程数,以避免过多的线程导致性能问题。
3、 Task类
3.1、常用属性
- IsCompleted:一个只读属性,表示任务是否已完成。
- IsCanceled:一个只读属性,表示任务是否已取消。
- IsFaulted:一个只读属性,表示任务是否执行失败。
- Status:一个只读属性,表示任务的当前状态(如Running、WaitingForActivation、WaitingForChildrenToComplete、RanToCompletion或Canceled)。
- Exception:一个只读属性,表示任务执行期间发生的异常(如果有)。
- Result:一个只读属性,表示任务的结果(如果任务已完成)。
3.2、常用方法
- Start():启动任务,使其开始执行。
- Wait():等待任务完成。
- Wait(TimeSpan):等待任务完成或直到指定的时间间隔过去。
- WaitAll(Task[]):等待一组任务全部完成。
- WaitAny(Task[]):等待一组任务中任意一个完成。
- ContinueWith(Action<Task, Task>):在任务完成后,以指定的委托继续执行另一个任务。
- Delay(TimeSpan):使当前线程在指定的时间间隔后继续执行。
- WhenAll(Task[]):返回一个新的Task,该任务将在所有指定的任务完成后完成。
- WhenAny(Task[]):返回一个新的Task,该任务将在任意指定的任务完成后完成。
- ConfigureAwait(bool):配置当前任务的Await操作,指定是否捕捉原始Synchronization Context。
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// 创建两个任务
Task task1 = Task.Run(() => PrintTaskId("Task 1"));
Task task2 = Task.Run(() => PrintTaskId("Task 2"));
// 等待所有任务完成
task1.Wait();
task2.Wait();
Console.WriteLine("所有任务已完成。");
}
// 任务要执行的函数
static void PrintTaskId(string taskName)
{
Console.WriteLine($"任务ID: {Task.CurrentId} - 任务名称: {taskName}");
}
}
4 、异步方法(async/await)
async
和await
关键字被用来简化异步编程。使用async
关键字声明一个方法是异步的,而await
关键字用来等待一个异步操作完成。下面是一个使用async
和await
的简单示例:
假设我们有一个模拟的网络请求,使用HttpClient类:
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
// 模拟网络请求
HttpClient client = new HttpClient();
string url = "http://www.example.com";
string result = await client.GetStringAsync(url);
Console.WriteLine(result);
}
}
在这个例子中,我们创建了一个HttpClient实例并使用它来发送一个GET请求。GetStringAsync
方法是一个异步方法,它会立即返回,而实际的网络请求将在后台异步执行。然后我们使用await
关键字等待这个异步操作完成,并获取返回的结果。这个例子中的Main
方法也是异步的,因为它被声明为async
。
三、线程同步和互斥
线程同步是指多个线程之间协调和同步执行的机制。由于多个线程同时访问共享资源可能会导致数据不一致或竞态条件等问题,因此需要使用线程同步来确保线程之间的顺序和正确性。
线程互斥是指多个线程对共享资源的访问进行协调和控制,以避免并发访问导致的数据不一致或竞态条件等问题。
C#提供了多种机制来实现线程互斥,其中最常用的是互斥锁(Mutex)和监视器(Monitor)。
1 、使用锁(lock)机制实现线程同步
lock关键字用于在代码块中创建一个互斥锁,确保只有一个线程可以进入该代码块。当一个线程进入lock代码块时,其他线程将被阻塞,直到该线程退出lock代码块。
using System;
using System.Threading;
public class Counter
{
private int count = 0;
private readonly object lockObject = new object();
public void Increment()
{
lock (lockObject)
{
count++;
Console.WriteLine($"Count after increment: {count}");
}
}
}
public class Program
{
public static void Main()
{
Counter counter = new Counter();
Thread t1 = new Thread(new ThreadStart(delegate { counter.Increment(); }));
Thread t2 = new Thread(new ThreadStart(delegate { counter.Increment(); }));
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
}
2 、Monitor类
Monitor类提供了一些静态方法,用于实现线程同步和互斥。其中最常用的方法是Enter和Exit方法,用于在代码块中获取和释放锁。
using System;
using System.Threading;
public class Counter
{
private int count = 0;
private readonly object lockObject = new object();
public void Increment()
{
Monitor.Enter(lockObject);
try
{
count++;
Console.WriteLine($"Count after increment: {count}");
}
finally
{
Monitor.Exit(lockObject);
}
}
}
public class Program
{
public static void Main()
{
Counter counter = new Counter();
Thread t1 = new Thread(new ThreadStart(delegate { counter.Increment(); }));
Thread t2 = new Thread(new ThreadStart(delegate { counter.Increment(); }));
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
}
3、 信号量(Semaphore)和互斥体(Mutex)
信号量用于控制同时访问某个资源的线程数量。
using System;
using System.Threading;
public class SemaphoreExample
{
private static Semaphore semaphore = new Semaphore(initialCount: 1, maximumCount: 1);
public static void Main()
{
Thread t1 = new Thread(new ThreadStart(ThreadTask));
Thread t2 = new Thread(new ThreadStart(ThreadTask));
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
private static void ThreadTask()
{
Console.WriteLine($"Waiting for semaphore...");
semaphore.WaitOne(); // 等待获取信号量
Console.WriteLine($"Semaphore acquired by thread {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000); // 模拟耗时操作
Console.WriteLine($"Releasing semaphore...");
semaphore.Release(); // 释放信号量
}
}
Mutex类是一个系统级别的互斥锁,可以用于跨进程的线程同步。Mutex类提供了WaitOne和ReleaseMutex方法,用于获取和释放锁。
using System;
using System.Threading;
public class MutexExample
{
private static Mutex mutex = new Mutex();
public static void Main()
{
Thread t1 = new Thread(new ThreadStart(ThreadTask));
Thread t2 = new Thread(new ThreadStart(ThreadTask));
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
private static void ThreadTask()
{
Console.WriteLine($"Waiting for mutex...");
mutex.WaitOne(); // 等待获取互斥体
Console.WriteLine($"Mutex acquired by thread {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000); // 模拟耗时操作
Console.WriteLine($"Releasing mutex...");
mutex.ReleaseMutex(); // 释放互斥体
}
}
四、任务并行库(TPL)
任务并行库(Task Parallel Library,TPL)是一个强大的工具,用于简化多线程和并发编程。TPL 基于 .NET Framework 中的 System.Threading.Tasks 命名空间,它提供了一组类和接口,使开发人员能够更容易地创建和管理并行任务。
以下是使用 TPL 进行多线程编程的一些基本概念和示例:
1. 创建 Task
使用 Task
类来创建一个新任务。例如:
Task task = new Task(new Action(() =>
{
// 执行一些工作
Console.WriteLine("Task is running on a separate thread.");
}));
2. 异步方法(Async/Await)
使用 async
和 await
关键字来异步执行方法。这可以避免显式创建和管理任务。例如:
public async Task MyAsyncMethod()
{
// 使用 await 调用异步方法
await SomeAsyncOperation();
Console.WriteLine("This runs after SomeAsyncOperation()");
}
3. Task Parallelism
使用 Parallel
类来执行多个任务。例如,使用 Parallel.For
或 Parallel.ForEach
来并行处理集合中的元素:
int[] numbers = { 1, 2, 3, 4, 5 };
Parallel.For(0, numbers.Length, i =>
{
// 对 numbers[i] 进行操作
int square = numbers[i] * numbers[i];
Console.WriteLine($"Processing {square} on thread {Task.CurrentId}");
});
4. 数据并行(Parallel LINQ,PLINQ)
PLINQ 是 LINQ 的扩展,允许开发人员使用 LINQ 语法编写并行查询:
var query = numbers.AsParallel().Select(n => n * n); // 使用 AsParallel() 来启用并行执行。
foreach (var square in query)
{
Console.WriteLine(square); // 结果按任意顺序打印。
}
5. 任务调度器(TaskScheduler)和任务上下文(TaskContext)
TPL 支持多种任务调度器,包括默认的 TaskScheduler.Default
和 TaskScheduler.Current
。在某些情况下,可能需要自定义任务调度器或保留任务的上下文信息。例如:
TaskContext context = TaskContext.Capture(); // 捕获当前任务的上下文信息。
Task task = Task.Factory.StartNew(() => ProcessWithContext(context)); // 使用捕获的上下文信息创建新任务。
注意:过度使用并行和并发可能导致资源竞争、死锁和其他并发问题。在编写并行代码时,请务必小心并确保正确地同步访问共享资源。
五、异步编程
异步编程是一种处理长时间运行操作的方法,这些操作不需要用户交互,但可能阻止UI线程或阻止其他任务的执行。异步编程允许应用程序继续执行其他任务,而不需要等待当前操作完成。
异步编程通常使用async
和await
关键字来实现。async
关键字用于声明一个方法将异步执行,而await
关键字用于等待一个异步操作完成。
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("开始异步读取文件...");
string content = await ReadFileAsync("test.txt");
Console.WriteLine("文件内容: " + content);
Console.WriteLine("异步读取文件结束。");
}
static async Task<string> ReadFileAsync(string path)
{
await Task.Run(() =>
{
// 模拟耗时操作
System.Threading.Thread.Sleep(5000);
});
return File.ReadAllText(path);
}
}
在上面的例子中,Main
方法使用async
关键字声明为异步,并调用ReadFileAsync
方法来异步读取文件。ReadFileAsync
方法内部使用Task.Run
来在后台线程上执行耗时操作(模拟读取文件),并使用await
等待该任务完成。当文件读取完成后,方法返回文件内容。
使用异步编程可以显著提高应用程序的性能和响应能力,因为它允许应用程序在等待操作完成时继续执行其他任务。此外,使用异步编程还可以避免UI线程阻塞,提高用户体验。
六、取消任务和异常处理
取消任务
当使用Task
类或async/await
模式创建异步任务时,可以使用CancellationToken
来请求取消任务。CancellationToken
是一个包含取消令牌的对象,可以用来请求取消任务。
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
// 创建一个CancellationTokenSource,它将生成一个取消令牌。
using (var cts = new CancellationTokenSource())
{
// 创建一个CancellationToken,它将与取消令牌关联。
var token = cts.Token;
// 启动一个异步任务,并将取消令牌传递给它。
Task task = Task.Run(() => LongRunningOperation(token), token);
// 等待用户输入来决定是否取消任务。
Console.WriteLine("按Enter键来取消任务,或者等待任务完成...");
Console.ReadLine();
// 如果用户输入了Enter键,则请求取消任务。
if (token.CanBeCanceled)
{
Console.WriteLine("取消任务...");
token.ThrowIfCancellationRequested();
}
}
}
static void LongRunningOperation(CancellationToken token)
{
// 检查是否请求了取消,如果是,则抛出一个异常。
if (token.IsCancellationRequested)
{
throw new OperationCanceledException("任务被取消。");
}
// 执行长时间运行的操作...
}
}
在上面的例子中,LongRunningOperation
方法在执行长时间运行的操作之前检查是否请求了取消。如果请求了取消,该方法会抛出一个OperationCanceledException
异常。
异常处理
处理多线程中的异常与处理常规异常类似,但是需要确保正确地捕获和处理所有可能的异常。你可以使用try/catch
块来捕获异常,并采取适当的行动来处理它们。还可以使用Task
的ContinueWith
方法或async/await
结构中的catch
块来捕获异步任务的异常。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
try
{
await LongRunningOperationAsync();
}
catch (Exception ex)
{
Console.WriteLine("发生异常: " + ex.Message);
}
}
static async Task LongRunningOperationAsync()
{
// 模拟长时间运行的操作...
await Task.Run(() => SimulateLongRunningOperation());
}
static void SimulateLongRunningOperation()
{
// 执行长时间运行的操作...可能会抛出异常...
}
}
七、并行LINQ
使用LINQ(Language Integrated Query)可以方便地并行处理集合中的元素。通过使用Parallel
类和LINQ的AsParallel
扩展方法,可以很容易地实现并行版本的查询操作。
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
var numbers = new int[] { 1, 2, 3, 4, 5 };
// 使用LINQ的AsParallel方法并行处理集合中的元素
var parallelQuery = from number in numbers.AsParallel()
where IsPrime(number)
select number;
// 执行查询并处理结果
foreach (var prime in parallelQuery)
{
Console.WriteLine($"Prime number: {prime}");
}
}
static bool IsPrime(int number)
{
// 检查数字是否为素数的逻辑
return number > 1 && Enumerable.Range(2, (number - 1)).All(n => number % n != 0);
}
}
在上面的例子中,numbers.AsParallel()
将numbers
数组转换为并行可枚举的集合,然后可以使用标准的LINQ查询操作符(如where
和select
)对其进行操作。注意,这里的IsPrime
方法也应该是线程安全的,或者需要在每个线程上调用它时进行适当的同步。
使用并行LINQ (Parallel LINQ
) 可以提高对大型数据集的查询性能,但需要注意线程安全和资源竞争的问题。
八、线程安全
线程安全是指对共享数据的访问控制,以避免并发线程之间的冲突和不一致状态。线程安全问题通常发生在多个线程同时访问和修改同一资源时。
要实现线程安全,可以使用以下几种方法:
- 互斥锁(Mutex):通过
System.Threading.Mutex
类可以同步对共享资源的访问。这个类可以用来实现跨进程的同步,但它比Monitor
更难以使用和调试。 - 监视器(Monitor):使用
System.Threading.Monitor
类来锁定特定代码块,确保一次只有一个线程可以执行这些代码。 - 锁语句(lock):使用
lock
关键字来保护代码块,确保同一时间只有一个线程可以执行被保护的代码。 - 原子操作:某些操作在单个指令中完成,无法被中断或被其他线程干扰,因此是线程安全的。例如,
Interlocked
类提供了一些原子操作方法。 - 读写锁:
ReaderWriterLock
或ReaderWriterLockSlim
类用于同步对共享数据的读取和写入。它们允许同时有多个读取者,但只允许一个写入者。 - 避免共享状态:通过设计将数据局部化到单个线程,可以避免线程安全问题。例如,使用局部变量或使用线程局部存储(Thread Local Storage, TLS)。
- 避免死锁:通过设计算法和使用适当的同步原语,避免出现死锁的情况。死锁是指两个或多个线程永久地等待对方释放资源。
using System;
using System.Threading;
class Program
{
static void Main()
{
// 创建一个互斥锁实例
Mutex mutex = new Mutex();
// 启动两个线程,它们将访问共享资源并需要同步访问
Thread thread1 = new Thread(new ThreadStart(ThreadSafeMethod));
Thread thread2 = new Thread(new ThreadStart(ThreadSafeMethod));
thread1.Start(mutex); // 传递互斥锁对象给线程
thread2.Start(mutex); // 传递互斥锁对象给线程
thread1.Join();
thread2.Join();
}
static void ThreadSafeMethod(object mutexObj)
{
// 获取互斥锁对象
Mutex mutex = (Mutex)mutexObj;
try
{
// 请求互斥锁的拥有权,如果当前没有其他线程拥有该锁,则当前线程将获得它并继续执行被保护的代码块。
mutex.WaitOne(); // 阻塞当前线程直到获得互斥锁的拥有权。
// 在这里编写访问共享资源的代码...
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} inside the critical section.");
// 释放互斥锁的拥有权,允许其他线程获得它。
mutex.ReleaseMutex();
}
catch (AbandonedMutexException) { } // 互斥锁已被其他线程释放,这是一个正常情况。
}
}
在这个例子中,我们使用了Mutex
类来同步对共享资源的访问。每个线程在进入临界区之前必须获取互斥锁的拥有权,并在退出临界区时释放它。这样可以确保任何时候只有一个线程可以访问共享资源。