线程(Thread)是进程中的基本执行单元,是操作系统分配CPU时间的基本单位,一个进程可以包含若干个线程,在进程入口执行的第一个线程被视为这个进程的主线程。在.NET应用程序中,都是以Main()方法作为入口的,当调用此方法时系统就会自动创建一个主线程。线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的。CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。
多线程的意义:
1、CPU运行速度太快,硬件处理速度跟不上,所以操作系统进行分时间片管理。这样,从宏观角度来说是多线程并发的,因为CPU速度太快,察觉不到,看起来是同一时刻执行了不同的操作。但是从微观角度来讲,同一时刻只能有一个线程在处理。
2、目前电脑都是多核多CPU的,一个CPU在同一时刻只能运行一个线程,但是多个CPU在同一时刻就可以运行多个线程。
多线程优点:
(1)可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。(牺牲空间计算资源,来换取时间)
然而,多线程虽然有很多优点,但是也必须认识到多线程可能存在影响系统性能的不利方面,才能正确使用线程。不利方面主要有如下几点:
(1)线程也是程序,所以线程需要占用内存,线程越多,占用内存也越多。(占内存)
(2)多线程需要协调和管理,所以需要占用CPU时间以便跟踪线程。(占CPU多)
(3)线程之间对共享资源的访问会相互影响,必须解决争用共享资源的问题。(资源共享问题)
(4)线程太多会导致控制太复杂,最终可能造成很多程序缺陷。(复杂线程容易产生BUG)
何时多线程使用(不要单纯为了使用而使用):
(1)当主线程试图执行冗长的操作,但系统会卡界面,体验非常不好,这时候可以开辟一个新线程,来处理这项冗长的工作。
(2)当请求别的数据库服务器、业务服务器等,可以开辟一个新线程,让主线程继续干别的事。
(3)利用多线程拆分复杂运算,提高计算速度。
同步&异步
线性执行,从上往下依次执行,同步方法执行慢,消耗的计算机资源少。
线程和线程之间,不再线型执行,多个线程总的耗时少,执行快,消耗的计算机资源多,各线程执行是无序的。
c#中的多线程:Thread/ThreadPool/Task 都是C#语言在操作计算机的资源时封装的帮助类库。
.net最早的多线程处理方式
开启新线程1
ParameterizedThreadStart parameterizedThreadStart = new ParameterizedThreadStart((oInstacnce) =>
{
Debug.WriteLine($"ParameterizedThreadStart--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString(" HH:mm:ss.fff")}");
});
Thread thread = new Thread(parameterizedThreadStart);
thread.Start();
开启新线程2
ThreadStart threadStart = new ThreadStart(() =>
{
this.DoSomething("张三");
});
Thread thread = new Thread(threadStart);
thread.Start();
线程的停止等待
Thread thread = new Thread(() =>
{
this.DoSomething("张三", 5000);
});
thread.Start();
//thread.Suspend(); //表示线程暂停,现在已弃用,NetCore平台已经不支持
//thread.Resume(); //线程恢复执行,弃用,NetCore平台已经不支持
//thread.Abort(); //线程停止,子线程对外抛出了一个异常,线程是无法从外部去终止的
//Thread.ResetAbort();//停止的线程继续去执行
//thread.ThreadState
//根据线程状态ThreadState判断实现线程间歇性休息
//while (thread.ThreadState != System.Threading.ThreadState.Stopped)
//{
// Thread.Sleep(500); //当前休息500ms,不消耗计算机资源的
//}
thread.Join();//主线程等待,直到当前线程执行完毕
//thread.Join(500);//主线程等待500毫秒,不管当前线程执行是否完毕,都继续往后执行
//thread.Join(new TimeSpan(500*10000));//主线程等待500毫秒,不管当前线程执行是否完毕,都继续往后执行
//TimeSpan 单位100纳秒 1毫秒=10000*100纳秒
后台线程&前台线程
Thread thread = new Thread(() =>
{
this.DoSomething("张三");
});
thread.Start();
thread.IsBackground = true;//后台线程,界面关闭,线程也就随之消失
thread.IsBackground = false;//前台线程,界面关闭,线程会等待执行完才结束
thread.Start();
跨线程操作主线程UI
Thread thread = new Thread(() =>
{
for (int i = 0; i <= 5; i++)
{
Thread.Sleep(500);
textBox1.Invoke(new Action(()=> textBox1.Text = i.ToString()));
}
});
thread.Start();
线程的优先级(提高优先级 = 提高了被优先执行的概率)
Thread thread = new Thread(() =>
{
this.DoSomething("张三");
});
// 线程的优先级最高
thread.Priority = ThreadPriority.Highest;
Thread thread1 = new Thread(() =>
{
this.DoSomething("张三");
});
// 线程的优先级最低
thread1.Priority = ThreadPriority.Lowest;
thread.Start();
thread1.Start();//线程开启后,根据优先级,来执行
数据槽
为了解决多线程竞用共享资源的问题,引入数据槽的概念,即将数据存放到线程的环境块中,使该数据只能单一线程访问。
在主线程上设置槽位,使该数据只能被主线程读取,其它线程无法访问
AllocateNamedDataSlot命名槽位
var d = Thread.AllocateNamedDataSlot("userName");
Thread.SetData(d, "张三");
//声明一个子线程
var t1 = new Thread(() =>
{
Debug.WriteLine($"子线程中读取数据:{Thread.GetData(d)}");
});
t1.Start();
//主线程中读取数据
Debug.WriteLine($"主线程中读取数据:{Thread.GetData(d)}");
AllocateDataSlot未命名槽位
var d = Thread.AllocateDataSlot();
Thread.SetData(d, "李四");
//声明一个子线程
var t1 = new Thread(() =>
{
Debug.WriteLine($"子线程中读取数据:{Thread.GetData(d)}");
});
t1.Start();
//主线程中读取数据
Debug.WriteLine($"主线程中读取数据:{Thread.GetData(d)}");
主线程中读取数据:张三
子线程中读取数据:
主线程中读取数据:李四
子线程中读取数据:
在主线程中给ThreadStatic特性标注的变量赋值,则只有主线程能访问该变量
变量标记特性
[ThreadStatic]
private static string Age = string.Empty;
线程访问变量
Age = "小女子年方28";
//声明一个子线程
var t1 = new Thread(() =>
{
Debug.WriteLine($"子线程中读取数据:{Age}");
});
t1.Start();
//主线程中读取数据
Debug.WriteLine($"主线程中读取数据:{Age}");
主线程中读取数据:小女子年方28
子线程中读取数据:
在主线程中声明ThreadLocal变量,并对其赋值,则只有主线程能访问该变量
程序访问
ThreadLocal tlocalSex = new ThreadLocal();
tlocalSex.Value = "女博士";
//声明一个子线程
var t1 = new Thread(() =>
{
Debug.WriteLine($"子线程中读取数据:{tlocalSex.Value}");
});
t1.Start();
//主线程中读取数据
Debug.WriteLine($"主线程中读取数据:{tlocalSex.Value}");
主线程中读取数据:女博士
子线程中读取数据:
内存栅栏(屏障)
在多线程编程中,内存屏障是一种非常重要的同步机制。多个线程同时访问同一份数据时,会出现线程安全性问题,需要使用内存屏障来保证线程之间的数据同步。内存屏障可以保证内存操作的有序性和可见性,避免因为指令重排序、缓存一致性等问题导致的线程安全性问题,以此提高程序的正确性和稳定性。
内存屏障可以分为四类:读屏障、写屏障、全屏障和加入屏障。每种屏障都有其特定的作用,需要深入理解这些屏障的原理和使用方法。
读屏障:保证对于一个指令进行的读操作,只有在读操作完成后,才能进行后续的读操作。
写屏障:保证对于一个指令进行的写操作,只有在写操作完成后,才能进行后续的写操作。
全屏障:保证所有的读写操作都完成后,才能进行后续的读写操作。
加入屏障:保证指令加入执行队列之前的操作顺序和加入执行队列之后的操作顺序一致。
.NET Framework2.0时代,出现了一个线程池ThreadPool,是一种池化思想,如果需要使用线程,就可以直接到线程池中去获取直接使用,如果使用完毕,在自动的回放到线程池去;
1、优点
不需要程序员对线程的数量管控,提高性能,防止滥用,去掉了很多在Thread中没有必要的Api
2、线程池如何分配一个线程
QueueUserWorkItem方法,将方法排入队列以便开启异步线程,它有两个重载。
1、QueueUserWorkItem(WaitCallback callBack),WaitCallback是一个有一个object类型参数且无返回值的委托。
2、QueueUserWorkItem(WaitCallback callBack, object state),WaitCallback是一个有一个object类型参数且无返回值的委托,state即WaitCallback中需要的参数, 不推荐这么使用,存在拆箱装箱的转换问题,影响性能。
//无参数
ThreadPool.QueueUserWorkItem(o =>this.DoSomething("张三"));
//一个参数
ThreadPool.QueueUserWorkItem(o => this.DoSomething("张三"), "12345");
(1)定义一个监听ManualResetEvent
(2)通过ManualResetEvent.WaitOne等待
(3)等到ManualResetEvent.Set方法执行了,主线程等待的这个WaitOne()就继续往后执行
ManualResetEvent resetEvent = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem(o =>
{
this.DoSomething(o.ToString());
resetEvent.Set();
}, "张三");
resetEvent.WaitOne();
如果通过SetMinThreads/SetMaxThreads来设置线程的数量,不建议大家去这样控制线程数量,这个数量访问是在当前进程中是全局的,错误配置可能影响程序的正常运行
{
//线程池中的工作线程数
int workerThreads = 4;
//线程池中异步 I/O 线程的数目
int completionPortThreads = 4;
//设置最小数量
ThreadPool.SetMinThreads(workerThreads, completionPortThreads);
}
{
int workerThreads = 8;
int completionPortThreads = 8;
//设置最大数量
ThreadPool.SetMaxThreads(workerThreads, completionPortThreads);
}
{
ThreadPool.GetMinThreads(out int workerThreads, out int completionPortThreads);
Debug.WriteLine($"当前进程最小的工作线程数量:{workerThreads}");
Debug.WriteLine($"当前进程最小的IO线程数量:{completionPortThreads}");
}
{
ThreadPool.GetMaxThreads(out int workerThreads, out int completionPortThreads);
Debug.WriteLine($"当前进程最大的工作线程数量:{workerThreads}");
Debug.WriteLine($"当前进程最大的IO线程数量:{completionPortThreads}");
}
5、扩展一个定时器功能
(1)RegisterWaitForSingleObject类,但是不常用.(涉及到定时任务,建议使用Quartz.Net)
(2)System.threading命名空间下的Thread类,通过查看源码,构造函数中有四个参数
第一个是object参数的委托
第二个是委托需要的值
第三个是调用 callback 之前延迟的时间量(以毫秒为单位)
第四个是 调用 callback 的时间间隔(以毫秒为单位)
//每隔3s开启一个线程执行业务逻辑
ThreadPool.RegisterWaitForSingleObject(new AutoResetEvent(true), new WaitOrTimerCallback((obj, b) => this.DoSomething("张三")), "hello world", 3000, false);
//效果类似于Timer定时器:2秒后开启该线程,然后每隔3s调用一次
System.Threading.Timer timer = new System.Threading.Timer((n) => this.DoSomething("李四"), "1", 2000, 3000);
Task出现之前,微软的多线程处理方式有:Thread→ThreadPool→委托的异步调用,虽然也可以基本业务需要的多线程场景,但它们在多个线程的等待处理方面、资源占用方面、线程延续和阻塞方面、线程的取消方面等都显得比较笨拙,在面对复杂的业务场景下,显得有点捉襟见肘了。正是在这种背景下,Task应运而生。
*Task是微软在.Net 4.0时代推出来的,也是微软极力推荐的一种多线程的处理方式,Task看起来像一个Thread,实际上,它是在ThreadPool的基础上进行的封装,Task的控制和扩展性很强,在线程的延续、阻塞、取消、超时等方面远胜于Thread和ThreadPool。
1、Task开启线程
Task task = new Task(() =>
{
this.DoSomething("张三");
});
task.Start();
Task task2 = new Task(() =>
{
return DateTime.Now.Year;
});
task2.Start();
int result = task2.Result;
Debug.WriteLine($"result:{result}");
Task.Run(() =>
{
this.DoSomething("张三");
});
Task task2 = Task.Run(() =>
{
return DateTime.Now.Year;
});
int result = task2.Result;
Debug.WriteLine($"result:{result}");
TaskFactory factory = Task.Factory;
factory.StartNew(() =>
{
this.DoSomething("张三");
});
Task task2 = factory.StartNew(() =>
{
return DateTime.Now.Year;
});
int result = task2.Result;
Debug.WriteLine($"result:{result}");
Task task = new Task(() =>
{
this.DoSomething("张三");
});
task.RunSynchronously();
Task task2 = new Task(() =>
{
return DateTime.Now.Year;
});
task2.RunSynchronously();
int result = task2.Result;
Debug.WriteLine($"result:{result}");
2、线程等待
在 C# 中,你可以使用 Task
和相关的方法来实现线程的等待。下面是几种常见的线程等待的方法:
1、使用 Task.Wait()
方法:Task.Wait()
方法会阻塞当前线程,直到任务完成。例如:
Task task = SomeAsyncMethod();
task.Wait(); // 阻塞当前线程直到任务完成
2、使用 Task.WaitAll()
方法:Task.WaitAll()
方法可以等待多个任务完成。它会阻塞当前线程,直到所有任务都完成。例如:
Task[] tasks = { task1, task2, task3 };
Task.WaitAll(tasks); // 阻塞当前线程直到所有任务都完成
3、使用 Task.WhenAll()
方法和 await
关键字:Task.WhenAll()
方法返回一个新的任务,该任务在所有输入任务都完成时完成。你可以使用 await
关键字来异步等待这个任务。例如:
Task[] tasks = { task1, task2, task3 };
await Task.WhenAll(tasks); // 异步等待所有任务都完成
以上方法适用于等待 Task
对象完成,这些 Task
对象可以表示异步操作。请根据你的具体需求选择适当的等待方法,并根据需要进行错误处理和超时控制。
3、多线程异常捕获
在 C# 中,可以使用 try-catch
块来捕获并处理多线程 Task
执行过程中抛出的异常。下面是几种常见的方法:
1、使用 Task.Exception
属性:在 Task
对象上可以访问 Exception
属性,该属性包含了任务执行过程中抛出的异常。你可以在 catch
块中检查 Task.Exception
属性来捕获异常。例如:
try
{
Task task = SomeAsyncMethod();
await task;
}
catch (Exception ex)
{
// 处理异常
Console.WriteLine("异常信息:" + ex.Message);
}
2、使用 Task.Wait()
方法和 AggregateException
:当使用 Task.Wait()
方法阻塞线程时,可以通过 AggregateException
类来访问任务执行过程中的所有异常。AggregateException
类是一个包装了一个或多个异常的异常类型。例如:
try
{
Task task = SomeAsyncMethod();
task.Wait();
}
catch (AggregateException ex)
{
// 处理异常
foreach (var innerException in ex.InnerExceptions)
{
Console.WriteLine("异常信息:" + innerException.Message);
}
}
3、使用 Task.ContinueWith()
方法和 TaskStatus.Faulted
:可以使用 Task.ContinueWith()
方法来在任务完成后执行后续操作,并检查任务的状态是否为 Faulted
。如果任务的状态为 Faulted
,则可以通过 Task.Exception
属性来获取异常。例如:
Task task = SomeAsyncMethod();
task.ContinueWith(t =>
{
if (t.Status == TaskStatus.Faulted)
{
// 处理异常
Exception ex = t.Exception;
Console.WriteLine("异常信息:" + ex.Message);
}
});
通过以上方法,你可以在多线程的 Task
执行过程中捕获和处理异常,以便进行适当的错误处理和异常信息记录。请根据你的实际需求选择适当的异常处理方法。
4、线程取消
在 C# 中,可以使用 CancellationToken
和 Task
的取消机制来取消任务的执行。下面是一个示例:
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
// 创建一个 CancellationTokenSource 对象用于取消任务
CancellationTokenSource cts = new CancellationTokenSource();
// 获取一个 CancellationToken 对象
CancellationToken token = cts.Token;
// 创建一个异步任务
Task task = Task.Run(async () =>
{
while (!token.IsCancellationRequested)
{
// 模拟任务的执行
Console.WriteLine("任务正在执行...");
await Task.Delay(1000); // 假设任务需要一秒钟的执行时间
}
// 检查任务是否被取消
token.ThrowIfCancellationRequested();
});
// 等待一段时间后取消任务
await Task.Delay(5000); // 假设等待五秒钟后取消任务
cts.Cancel();
try
{
// 等待任务完成,并处理可能的取消异常
await task;
}
catch (TaskCanceledException)
{
Console.WriteLine("任务已取消");
}
}
}
在上面的示例中,我们首先创建了一个 CancellationTokenSource
对象 cts
,用于取消任务。然后,我们通过 cts.Token
获取一个 CancellationToken
对象 token
,用于监视任务的取消请求。
接下来,我们创建了一个异步任务,并在任务的执行过程中周期性地检查 token.IsCancellationRequested
属性,以判断是否收到取消请求。如果收到取消请求,任务会抛出 TaskCanceledException
异常。
在 Main
方法中,我们等待一段时间后调用 cts.Cancel()
来请求取消任务。然后,我们使用 try-catch
块来捕获可能的 TaskCanceledException
异常,以处理任务的取消情况。
通过使用 CancellationToken
和适当的取消机制,你可以在需要时取消正在执行的任务,并进行相应的处理。请根据你的实际需求,调整示例代码中的时间间隔和取消逻辑。
5、线程安全
线程安全:一段业务逻辑,单线程执行和多线程执行后的结果如果完全一致,是线程安全的,否则就是线程不安全的
在 C# 中,线程安全是指多个线程同时访问共享资源时,不会发生意外的数据竞争或不一致的情况。线程安全的代码能够正确地处理并发访问,保证数据的一致性和正确性。
下面是一些常见的实现线程安全的方法和技术:
lock
关键字,你可以在代码块中创建一个临界区,只允许一个线程进入该临界区,从而保护共享资源的访问。ConcurrentDictionary
、ConcurrentQueue
、ConcurrentStack
等。这些集合类使用了内部的同步机制,可以安全地在多线程环境中进行操作,避免数据竞争和不一致的问题。Interlocked
类,它提供了一些原子操作方法,如原子的增加、减少、交换等,用于保证线程安全。以上是一些常见的方法和技术来实现线程安全。根据具体的场景和需求,选择适当的方法来确保代码在多线程环境中的安全性和正确性。同时,进行适当的测试和验证,以确保代码的正确性和性能。
Parallel
类是 C# 中用于并行编程的一个工具类,它提供了一些方便的方法来执行并行任务。Parallel
类使用了任务并行库(Task Parallel Library,TPL)来实现并行计算,简化了多线程编程的复杂性。
Parallel
类提供了以下常用的方法:
1、Parallel.For
:用于在一个范围内并行执行循环迭代。它接受起始索引、结束索引和一个委托作为参数,委托表示每个迭代要执行的操作。
Parallel.For(0, 10, i =>
{
// 执行循环迭代中的操作
Console.WriteLine(i);
});
2、Parallel.ForEach
:用于并行地迭代集合中的元素。它接受一个集合和一个委托作为参数,委托表示对每个元素要执行的操作。
List numbers = new List { 1, 2, 3, 4, 5 };
Parallel.ForEach(numbers, number =>
{
// 处理集合中的每个元素
Console.WriteLine(number);
});
3、Parallel.Invoke
:用于并行地执行多个操作。它接受多个委托作为参数,每个委托表示一个要执行的操作。
Parallel.Invoke(
() => Console.WriteLine("操作1"),
() => Console.WriteLine("操作2"),
() => Console.WriteLine("操作3")
);
使用 Parallel
类可以很方便地进行并行计算,它会根据当前系统的资源情况自动进行任务的分配和调度。在使用 Parallel
类时,需要注意保证并行执行的操作是线程安全的,避免出现数据竞争和不一致的问题。
另外,Parallel
类还提供了其他一些方法和选项,如 ParallelOptions
类来配置并行选项,以及 Parallel.ForAll
方法用于并行地迭代数组。你可以根据具体的需求和场景选择合适的方法来进行并行计算。
在 C# 中,await 和 async 是用于异步编程的关键字。它们结合起来可以让你以更简洁和直观的方式编写异步代码,避免了传统的回调地狱和复杂的线程管理。
async 关键字用于修饰方法,表示该方法是一个异步方法。异步方法可以在执行过程中暂停,并允许其他代码继续执行,直到异步操作完成或达到某个等待点。
await 关键字用于等待一个异步操作的完成,并允许程序流程在等待期间继续执行其他操作。await 关键字必须出现在异步方法中,并且后面跟着一个返回一个任务(Task)或任务结果(Task)的表达式。
public async Task GetDataAsync()
{
// 模拟耗时的异步操作
await Task.Delay(2000);
return "异步操作完成";
}
public async void MyMethod()
{
Console.WriteLine("开始异步操作");
string result = await GetDataAsync();
Console.WriteLine(result);
}
在上面的示例中,GetDataAsync()
方法是一个异步方法,它使用 await Task.Delay(2000)
来模拟一个耗时的异步操作。在 MyMethod()
方法中,我们可以使用 await
来等待 GetDataAsync()
方法的完成,并在异步操作完成后打印结果。
需要注意的是,异步方法的返回类型通常是 Task
或 Task
,表示异步操作的结果。如果异步方法没有返回值,可以使用 Task
类型作为返回类型,或者使用 void
返回类型。
异步方法的好处是可以使你的代码更加响应和高效,特别是在处理 I/O 密集型操作时。通过使用 await
和 async
,你可以在等待异步操作完成的同时,不阻塞其他线程或任务的执行,提高程序的并发性和吞吐量。
然而,在使用 await
和 async
时,需要注意以下几点:
Task.Delay
代替 Thread.Sleep
。try/catch
块来捕获异常,或者使用 Task.Exception
属性获取异常信息。Task.WhenAll
或 Task.WhenAny
来等待多个异步操作的完成。这只是异步编程的基本概念和用法,还有更多的高级技术和最佳实践可以应用于异步编程。如果你想深入了解更多关于异步编程的内容,建议查阅相关的文档和教程。
await/async原理
如果给方法加上Async,在底层会生成一个状态机,一个对象在不同的状态可以执行的不同的行为
(1)实例化状态机
(2)把状态机实例交给一个build去执行
(3)整理线程的上下文
(4)stateMachine.MoveNext();
(5)MoveNext如何执行,先获取一个状态,继续往后执行
(6)如果有异常,抛出异常,把状态重置为-2
(7)如果没有异常,把状态重置重置为-2
(8)SetResult();把结果包裹成一个Task
await/async应用场景
计算机的计算任务可以分成两类,计算密集型任务和IO密集型任务,async/await和Task相比,降低了线程使用数量,性能相当,不能提高计算速度,优势就是在同等硬件基础上系统的吞吐率更高,对计算密集型任务没有优势,IO密集型计算有优势,常见的IO密集型任务有: