C#多线程基本使用小结
作者:小威编程
线程是并发编程的基础概念之一。在现代应用程序中,我们通常需要执行多个任务并行处理,以提高性能。C# 提供了多种并发编程工具,如Thread
、Task
、异步编程和Parallel
等。
Thread 类
Thread
类是最基本的线程实现方法。使用Thread
类,我们可以创建并管理独立的线程来执行任务。
基本使用Thread
创建一个新的实例对象,将一个方法直接给Thread类,并使用实例对象启动线程运行。
static void Test() { Console.WriteLine("Test事件开始执行"); Thread.Sleep(1000); Console.WriteLine("Test事件睡眠之后打印数据"); Console.WriteLine("Test事件id: " + Thread.CurrentThread.ManagedThreadId); } static void Main(string[] args) { #region //主线程id Console.WriteLine("主线程id: " + Thread.CurrentThread.ManagedThreadId); #endregion #region //传递一个方法给线程,执行方法 Thread t = new Thread(Test); t.Start(); #endregion }
线程数据传输
传递单个参数
Thread
提供了一个 Start(object parameter)
方法,可以在启动线程时传递一个参数。
static int Downlod(object obj) { string str = obj as string; Console.WriteLine(str); } static void Main(string[] args) { Thread t1 = new Thread(Downlod); //这里传递的参数是字符串 t1.Start("数据传递方法执行 ,数据传递方法id: "+Thread.CurrentThread.ManagedThreadId); }
这种方式适用于需要传递单个参数的情况。
使用类的实例方法传递多个参数
先new一个类的实例,给实例字段赋值,调用类的实例,通过实例调用方法,构造函数接收参数,然后在线程中调用该实例的方法。
static void Main(string[] args) { // 使用 DownloadTool 实例化并传递数据 DownloadTool downloadTool = new DownloadTool("www.baidu.com", "这是下载链接哦"); Thread thread = new Thread(downloadTool.Download); thread.Start(); } public class DownloadTool { private string url; private string message; public DownloadTool(string url, string message) { this.url = url; this.message = message; } public void Download() { Console.WriteLine("下载链接: " + url); Console.WriteLine("提示信息: " + message); Console.WriteLine("Download 线程 ID: " + Thread.CurrentThread.ManagedThreadId); } }
当 thread
线程启动时,它会执行 downloadTool.Download()
方法,输出传递的数据。
线程优先级
在 C# 中,可以使用 Thread.Priority
属性来设置线程的优先级。线程优先级决定了操作系统在多线程环境中调度线程的顺序,但并不保证高优先级的线程总是比低优先级的线程更早或更频繁地执行。
线程优先级级别
C# 提供了五个线程优先级级别,定义在 ThreadPriority
枚举中:
- Lowest:最低优先级。操作系统尽可能少地调度这个优先级的线程。
- BelowNormal:低于正常的优先级。优先级比 Normal 低,但高于 Lowest。
- Normal:默认优先级,大多数线程默认的优先级。适用于一般用途。
- AboveNormal:高于正常的优先级。操作系统更倾向于调度这个优先级的线程。
- Highest:最高优先级。操作系统尽可能多地调度这个优先级的线程。
internal class Program { static void A() { int i = 0; while (true) { i++; Console.WriteLine($"A 输出第{i}"); Thread.Sleep(1000); } } static void B() { int i = 0; while (true) { i++; Console.WriteLine($"B 输出第{i}"); Thread.Sleep(1000); } } static void Main(string[] args) { //在C#中,线程的优先级可以通过Thread.Priority属性来设置和获取。 // Lowest: 线程的优先级是最低的。在系统中存在其他活动线程时,此优先级的线程很少得到执行。 //BelowNormal: 线程的优先级低于正常线程。 //Normal: 线程的优先级是普通的,这是线程的默认优先级。 //AboveNormal: 线程的优先级高于正常线程。 //Highest: 线程的优先级是最高的。此优先级的线程会尽量优先于其他所有优先级的线程执行。 Thread a = new Thread(A); Thread b = new Thread(B); a.Priority = ThreadPriority.Highest; a.Start(); b.Priority = ThreadPriority.Lowest; b.Start(); Console.WriteLine("按任意键停止线程..."); Console.ReadKey(); a.Join(); b.Join(); Console.WriteLine("线程已停止"); } }
A
被设置为最高优先级ThreadPriority.Highest
。B
被设置为最低优先级ThreadPriority.Lowest
。
注意事项
- 优先级不是绝对控制:操作系统可能会忽略优先级设置,特别是在资源有限的系统中。高优先级线程不一定会一直执行,也不能阻止低优先级线程的执行。
- 使用优先级的适用场景:设置线程优先级可能适用于实时系统(例如,某些任务需要优先处理)。但是,大多数应用程序通常可以使用默认的
Normal
优先级。 - 避免使用过多的高优先级线程:如果所有线程都被设置为
Highest
,系统的整体性能可能会下降,甚至导致线程争用 CPU 资源的情况。 - CPU 密集型任务:在 CPU 密集型任务中,优先级可能会对性能产生较大影响,因为优先级高的线程可能会占用更多的 CPU 时间。
线程优先级的最佳实践
- 默认使用
Normal
优先级,除非有特殊原因。 - 避免滥用
Highest
优先级,因为它会对系统资源产生影响。 - 在 I/O 密集型的线程中,优先级通常不会有显著差异,因为这些线程在等待 I/O 操作完成时,CPU 会调度其他线程。
通过合理设置线程优先级,可以帮助操作系统更好地调度线程,以满足应用程序的需求。 但通常在 .NET 应用程序中,多数情况下使用默认的 Normal
优先级就足够了。
线程池
线程池 (ThreadPool
) 是一种高效的管理和调度线程的方式。线程池自动管理线程的创建、重用和销毁,从而减少了手动创建和管理线程的开销。
为什么使用线程池
- 性能更高:线程池会重用现有的线程,减少了创建和销毁线程的开销。
- 自动管理:线程池会根据系统负载动态调整线程数量。
- 避免线程资源不足:线程池限制了同时运行的线程数,避免了线程过多导致的资源耗尽问题。
基本使用 ThreadPool.QueueUserWorkItem
方法将任务排入线程池队列。
using System; using System.Threading; class Program { static void Main(string[] args) { // 将任务排入线程池 ThreadPool.QueueUserWorkItem(DoWork, "任务 1"); ThreadPool.QueueUserWorkItem(DoWork, "任务 2"); Console.WriteLine("主线程完成"); Thread.Sleep(3000); // 等待线程池中的任务完成 } static void DoWork(object state) { string taskName = (string)state; Console.WriteLine($"{taskName} 开始执行 - 线程ID: {Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(1000); // 模拟耗时操作 Console.WriteLine($"{taskName} 执行完成 - 线程ID: {Thread.CurrentThread.ManagedThreadId}"); } }
运行结果QueueUserWorkItem自动分配线程来执行任务。
QueueUserWorkItem
方法将任务排入线程池。它接收一个委托(即方法)和一个可选的状态对象(传递给方法的数据)。
DoWork
方法接受一个参数 state
,这是从 QueueUserWorkItem
传递的。
Thread.Sleep(3000)
确保主线程不会立即退出,使得线程池中的任务有机会完成。
使用带返回值的线程池任务
C# 中的 ThreadPool
通常不直接支持返回值。如果需要获得任务结果,可以使用 Task
,因为 Task
本质上也是线程池的一部分。Task
更适合于带返回值的异步操作。这里使用 Task.Run
来代替 ThreadPool
:
static void Main(string[] args) { //使用tesk多线程 int a= Task.Run(() => { int a = Dowload(); return a; }).Result; Task<int> task = Task<int>.Run(()=>{ int a = Dowload(); return a; }); //初始化一个CancellationTokenSource实例 CancellationTokenSource source = new CancellationTokenSource(); //task.Start(); task.Wait(1000); source.Cancel(); int result = task.Result; Console.WriteLine(result); Console.WriteLine($"tesk返回值{a}"); } static int Dowload() { int a = 0; for (int i = 0; i < 10; i++) { a= a + i + 1; } int? id= Task.CurrentId; Console.WriteLine("Current thread ID: " + id); return a; }
线程池的限制
- 任务运行时间过长:线程池中的线程本质上是共享资源,如果某个任务运行时间太长,将会占用线程池中的线程,导致其他任务无法及时执行。
- 不适合实时系统:线程池中的任务调度是由系统管理的,无法保证精确的实时性。
- 有限的线程数量:在高并发场景中,如果线程池中的线程全部被占用,新的任务将会等待,直到有线程可用。
线程池总结
线程池是一种高效的并发处理方式,适合于大多数轻量级的后台任务。在现代 C# 编程中,建议使用 Task
和 async/await
进行异步操作,因为它们能简化代码,并且使用底层的线程池来管理线程。如果需要精确控制线程的执行,通常建议使用手动管理的 Thread
等。
线程锁
使用多线程,在多线程编程中,如果多个线程同时访问和修改共享资源(如全局变量、文件、数据库等),可能会导致数据不一致或竞争条件。为了避免这种情况,多线程锁通过控制对共享资源的访问来保证线程安全性。
资源冲突示例:
static void Main(string[] args) { //调用方法循环创建新的线程来执行方法 StateObject state = new StateObject(); for (int i = 0; i < 30; i++) { Thread thread = new Thread(state.ChangState); thread.Start(); } Console.ReadKey(); } //一个StateObject类 public class StateObject { private int state = 5; //里面有个状态改变方法,当状态等于5的时候进入到方法中,然后state+1 打印的应该是6 和线程id public void ChangState() { if (state == 5) { state++; Console.WriteLine($"state:{state} 线程id:" + Thread.CurrentThread.ManagedThreadId); } state = 5; } }
运行结果:
因为资源冲突的原因,有一些线程执行的时候,可能另外一个线程没有执行完,另外一个线程就已经进入到方法里面了。因为有修改的操作导致State状态混乱,资源冲突。这时候我们就需要一个东西来维护,让线程执行指定方法时一个一个的执行,不会冲突。
简单使用lock锁
1.创建一个private readonly object lockObject = new object(); // 锁对象
2.使用lock (lockObject){
//业务代码,执行到有修改的操作的代码
} // 使用锁对象来确保线程安全
static void Main(string[] args) { StateObject state = new StateObject(); for (int i = 0; i < 100; i++) { Thread thread = new Thread(state.ChangState); thread.Start(); } Console.ReadKey(); } public class StateObject { private object _lock = new object(); private int state = 5; public void ChangState() { //使用锁保证每次执行方法的都是一个线程,防止资源冲突 lock (_lock) { if (state == 5) { state++; Console.WriteLine($"state:{state} 线程id:" + Thread.CurrentThread.ManagedThreadId); } state = 5; } } }
这样运行起来就能把每个线程操作隔离开,保证state的状态不会冲突。
为什么要创建一个 object
对象作为锁?
- 专用性:创建一个专用的锁对象(如
private readonly object lockObject = new object();
),可以确保锁定的是特定的同步逻辑,而不是其他对象。这有助于避免意外的锁冲突或死锁。 - 避免使用其他共享对象:虽然可以使用任意的引用类型对象作为锁对象(包括
this
或Type
对象),但这可能会带来不必要的风险,尤其是在public
方法或对象中,这样可能会导致意外的锁定冲突。
常见问题死锁:
锁并不是万能,也不是有锁就是最好,要看情况使用,锁也会产生问题,常见的问题就是死锁等问题。
演示死锁:
thread1方法1 的锁里面嵌套锁住了thread2的锁,thread2方法2的锁里面嵌套锁住了thread1的锁,这种锁与锁嵌套使用,就是容易出问题。导致线程锁死程序无法动弹。
static void Main(string[] args) { DeadlockExample deadlockExample = new DeadlockExample(); Thread t1 = new Thread(deadlockExample.Thread1); Thread t2 = new Thread(deadlockExample.Thread2); t1.Start(); t2.Start(); t1.Join(); t2.Join(); Console.ReadKey(); } } public class DeadlockExample { private static object lock1 = new object(); private static object lock2 = new object(); public void Thread1() { lock (lock1) { Console.WriteLine("线程1:已获取锁1,正在等待锁2。。。"); Thread.Sleep(100); // 模拟某些工作 lock (lock2) { Console.WriteLine("线程1:获得锁2"); } } } public void Thread2() { lock (lock2) { Console.WriteLine("线程2:已获取锁2,正在等待锁1。。。"); Thread.Sleep(100); // 模拟某些工作 lock (lock1) { Console.WriteLine("线程2:获得锁1"); } } } }
运行结果:
死锁发生在两个或多个线程相互等待对方持有的资源,导致所有线程都无法继续执行。
总结:
多线程锁在 C# 中主要用于解决以下问题:
- 竞态条件:通过锁机制防止多个线程同时访问和修改共享资源,确保数据一致性。
- 死锁:防止多个线程相互等待资源,通过锁的顺序或者避免嵌套锁来解决。
- 资源饥饿:确保每个线程都能获取资源,使用
Monitor.TryEnter
等机制防止无限等待。 - 读写锁:允许多个线程并发读取资源,但写入时互斥,适合读多写少的场景。
C# 提供了多种锁机制,开发者可以根据应用场景选择合适的锁类型。
如果不想使用 lock
关键字,C# 还提供了其他锁机制,比如 Mutex
、Semaphore
、Monitor
等
到此这篇关于C#多线程基本使用和探讨的文章就介绍到这了,更多相关C#多线程使用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!