线程
参考文章 https://blog.gkarch.com/threading/part2.html#locking-and-atomicity
线程间同步是指在多线程环境下,保证共享资源的安全和一致性的机制。 C# 中提供了多种方式实现线 程间同步。例如:
- lock 语句:使用一个对象作为锁,保证一次只有一个线程可以进入临界区;
- Interlocked 类:提供了原子操作,如递增、递减、交换和读取值;
- Monitor 类:提供了锁定对象、等待信号和通知信号的方法;
- Mutex 类:提供了跨进程的互斥锁,可以用来同步不同进程中的线程;
- Semaphore 类:提供了一个计数器,限制同时访问共享资源的线程数;
- AutoResetEvent 和 ManualResetEvent 类:提供了信号量,可以用来通知或等待其他线程的状态变 化;
线程池
https://www.cnblogs.com/eventhorizon/p/15316955.html#3-%E9%81%BF%E5%85%8D%E9%A5%A5%E9%A5%BF%E6%9C%BA%E5%88%B6starvation-avoidance https://threads.whuanle.cn/2.thread_sync/10.spinwait.html https://k8s.whuanle.cn/1.basic/1.docker.html https://www.whuanle.cn/ https://www.cnblogs.com/whuanle/p/13675952.html#%E5%85%A8%E5%B1%80%E5%BC%82%E5%B8%B8%E6%8B%A6%E6%88%AA%E5%99%A8 https://www.albahari.com/ https://blog.gkarch.com/topic/threading.html https://xiaolincoding.com/ https://www.cnblogs.com/xiaolipro/p/16891311.html
Task.Yield
Task.Yield 申请空闲线程
static async Task Process()
{
//向空闲的线程池申请一个线程
await Task.Yield();
//之后的代码运行在新的线程
var tcs = new TaskCompletionSource<bool>();
//在开启一个新线程
Task.Run(() =>
{
Thread.Sleep(1000);
tcs.SetResult(true);
});
//等待返回的结果
tcs.Task.Wait();
}
同步方法里等待异步方法可能会照成线程饥饿状态
因为同步方法会进行等待
ExecutionContext vs SynchronizationContext 执行上下文和同步上下文
ExecutionContext
ExecutionContext 是一个表示线程执行上下文的对象。它包含了线程的状态信息、堆栈帧、线程本地 存储等相关数据。每个线程都有自己的 ExecutionContext 对象,用于跟踪和管理线程的执行状态 ExecutionContext 是一个重要的概念,它用于跟踪和管理线程的执行上下文。ExecutionContext 提 供了一种机制,使得线程能够在执行过程中共享上下文信息,并且能够在不同的线程之间传递上下文 。
Task is running on thread 1primary 子 Task Task is running on thread 7 task1 在子 Task 线程里,使用指定的执行上下文启动一个方法 这里获取到了 parentContext 里的 AsyncLocal Task is running on thread 7 primary
var al=new AsyncLocal<string> {
Value = "primary"
};
//对当前线程的执行上下文拍快照
var parentContext = ExecutionContext.Capture();
Console.WriteLine($"Task is running on thread
{Thread.CurrentThread.ManagedThreadId}"+al.Value);
//在Task 任务里会开启一个新的 ExecutionContext
await Task.Run(() => {
al.Value = "task1";
Console.WriteLine($"Task is running on thread
{Thread.CurrentThread.ManagedThreadId}"+al.Value);
ExecutionContext.Run(parentContext, _ => {
Console.WriteLine($"Task is running on thread
{Thread.CurrentThread.ManagedThreadId}"+al.Value);
}, null);
});
SynchronizationContext
同步上下文是为了在不同的线程中,在其中一个线程发送消息给其他线程,比如给 UI 线程发消息
Task 的 await async 理解
Task 的异步是一种基于状态机实现方式,编译器碰到 await 会把代码编译成一个代码块,表示一种状 态。Task 的任务调度器会调度空闲线程去处理每一个状态。当一个状态完成后,调度器调度一个空闲 线程去处理下一个任务,这样一个接一个处理,这背后是靠 Task 调度器进行安排
AsyncLocal 及 ThreadLocal
AsyncLocal
asyncLocalData.Value = 1;
threadLocalData.Value = 2;
//id:1
Console.WriteLine("当前线程Id"+Thread.CurrentThread.ManagedThreadId);
//1
Console.WriteLine($"AsyncLocal in Task: {asyncLocalData.Value}");
//2
Console.WriteLine($"ThreadLocal in Task: {threadLocalData.Value}");
await Task.Run(() =>
{
//id4
Console.WriteLine("当前线程Id"+Thread.CurrentThread.ManagedThreadId);
//1
Console.WriteLine($"AsyncLocal in Task: {asyncLocalData.Value}");
//0
Console.WriteLine($"ThreadLocal in Task: {threadLocalData.Value}");
});
//id 4
Console.WriteLine("当前线程Id"+Thread.CurrentThread.ManagedThreadId);
//1
Console.WriteLine($"AsyncLocal in Main: {asyncLocalData.Value}");
//0
Console.WriteLine($"ThreadLocal in Main: {threadLocalData.Value}");
如下代码执行结果:
当前线程 Id1 AsyncLocal in Task: 1 ThreadLocal in Task: 2 当前线程 Id7 AsyncLocal in Task: 3 ThreadLocal in Task: 4 当前线程 Id8 AsyncLocal in Task: 5 ThreadLocal in Task: 6 当前线 程 Id8 AsyncLocal in Main: 1 ThreadLocal in Main: 6
最为奇怪的是在 当前线程 Id8 里,第一次进入 task 时 AsyncLocal in Task: 5 ThreadLocal in Task: 6 执行完 task 后 AsyncLocal in Main: 1 ThreadLocal in Main: 6
ThreadLocal 里的变量在同一线程是一样,但是 AsyncLocal 有变成最初的值了说明在每个 task 里是 对 AsyncLocal 值的一次拷贝,不会对 AsyncLocal 最初的值产生影响故在最后一次退出 task 后,显 示的是最初的 1.
AsyncLocal 对于引用类型是浅拷贝,故在所有 task 里使用的是一个引用对象
AsyncLocal 是为了配合 Task await 流程。
var asyncLocalData = new AsyncLocal<int>();
var threadLocalData = new ThreadLocal<int>();
asyncLocalData.Value = 1;
threadLocalData.Value = 2;
Console.WriteLine("当前线程Id"+Thread.CurrentThread.ManagedThreadId);
Console.WriteLine($"AsyncLocal in Task: {asyncLocalData.Value}");
Console.WriteLine($"ThreadLocal in Task: {threadLocalData.Value}");
await Task.Run(() =>
{
asyncLocalData.Value = 3;
threadLocalData.Value = 4;
Console.WriteLine("当前线程Id"+Thread.CurrentThread.ManagedThreadId);
Console.WriteLine($"AsyncLocal in Task: {asyncLocalData.Value}");
Console.WriteLine($"ThreadLocal in Task: {threadLocalData.Value}");
});
await Task.Run(() =>
{
asyncLocalData.Value = 5;
threadLocalData.Value = 6;
Console.WriteLine("当前线程Id"+Thread.CurrentThread.ManagedThreadId);
Console.WriteLine($"AsyncLocal in Task: {asyncLocalData.Value}");
Console.WriteLine($"ThreadLocal in Task: {threadLocalData.Value}");
});
Console.WriteLine("当前线程Id"+Thread.CurrentThread.ManagedThreadId);
Console.WriteLine($"AsyncLocal in Main: {asyncLocalData.Value}");
Console.WriteLine($"ThreadLocal in Main: {threadLocalData.Value}");
嵌套的 Task 也符合上面对 AsyncLocal 的结论,AsyncLocal 是按照状态机的流程工作而不是线程工 作
当前线程 Id1 AsyncLocal in Task: 1 ThreadLocal in Task: 2
主 Tak 流程 当前线程 Id7 AsyncLocal in Task: 3 ThreadLocal in Task: 4
嵌套 Task 的流程 当前线程 Id4 AsyncLocal in Task: 5 ThreadLocal in Task: 6
回到主 Tak 流程 当前线程 Id4 AsyncLocal in Task: 3 ThreadLocal in Task: 6
回到主流程 当前线程 Id4 AsyncLocal in Main: 1 ThreadLocal in Main: 6
var asyncLocalData = new AsyncLocal<int>();
var threadLocalData = new ThreadLocal<int>();
asyncLocalData.Value = 1;
threadLocalData.Value = 2;
Console.WriteLine("当前线程Id"+Thread.CurrentThread.ManagedThreadId);
Console.WriteLine($"AsyncLocal in Task: {asyncLocalData.Value}");
Console.WriteLine($"ThreadLocal in Task: {threadLocalData.Value}");
await Task.Run(async () =>
{
asyncLocalData.Value = 3;
threadLocalData.Value = 4;
Console.WriteLine("当前线程Id"+Thread.CurrentThread.ManagedThreadId);
Console.WriteLine($"AsyncLocal in Task: {asyncLocalData.Value}");
Console.WriteLine($"ThreadLocal in Task: {threadLocalData.Value}");
await Task.Run(() =>
{
asyncLocalData.Value = 5;
threadLocalData.Value = 6;
Console.WriteLine("当前线程Id"+Thread.CurrentThread.ManagedThreadId);
Console.WriteLine($"AsyncLocal in Task: {asyncLocalData.Value}");
Console.WriteLine($"ThreadLocal in Task: {threadLocalData.Value}");
});
Console.WriteLine("当前线程Id"+Thread.CurrentThread.ManagedThreadId);
Console.WriteLine($"AsyncLocal in Task: {asyncLocalData.Value}");
Console.WriteLine($"ThreadLocal in Task: {threadLocalData.Value}");
});
Console.WriteLine("当前线程Id"+Thread.CurrentThread.ManagedThreadId);
Console.WriteLine($"AsyncLocal in Main: {asyncLocalData.Value}");
Console.WriteLine($"ThreadLocal in Main: {threadLocalData.Value}");
上面的列子在 Task 里修改 AsyncLocal 的值,在回到主流程的时候,修改后的值没有反应到主流程, 如果要在所有流程中传递修改的值,可以传递引用类型
当前线程 Id1 AsyncLocal in Task: primary
子 Task 流程 当前线程 Id7 AsyncLocal in Task: t1 当前线程 Id8 AsyncLocal in Task: t2
主流程,引用类型 当前线程 Id8 AsyncLocal in Main: t2
var asyncLocalData = new AsyncLocal<A> {
Value = new A(){Bar = "primary"}
};
Console.WriteLine("当前线程Id"+Thread.CurrentThread.ManagedThreadId);
Console.WriteLine($"AsyncLocal in Task: {asyncLocalData.Value.Bar}");
await Task.Run(() => {
asyncLocalData.Value.Bar = "t1";
Console.WriteLine("当前线程Id" + Thread.CurrentThread.ManagedThreadId);
Console.WriteLine($"AsyncLocal in Task: {asyncLocalData.Value.Bar}");
});
await Task.Run(() => {
asyncLocalData.Value.Bar = "t2";
Console.WriteLine("当前线程Id" + Thread.CurrentThread.ManagedThreadId);
Console.WriteLine($"AsyncLocal in Task: {asyncLocalData.Value.Bar}");
});
Console.WriteLine("当前线程Id"+Thread.CurrentThread.ManagedThreadId);
Console.WriteLine($"AsyncLocal in Main: {asyncLocalData.Value.Bar}");
class A {
public string Bar { get; set; }
}
使用 AsyncLocal 可以保存当前的对象,输出如下值,这样在多线程环境下就能获取到当前保存的对象 32854180 27252167 43942917
new MyContext();
Func1();
new MyContext();
Func1();
new MyContext();
Func1();
void Func1()
{
//获取当前的 MyContext 对象,这种在多线程里就可以方便的传递 MyContext 对象了
Console.WriteLine(MyContext.Current.GetHashCode());
}
public class MyContext
{
//静态变量
static AsyncLocal<MyContext> _scope = new AsyncLocal<MyContext>();
public MyContext() => _scope.Value = this;
public static MyContext? Current => _scope.Value;
}
Nito.AsyncEx
AsyncEx 库支持异步的各种线程锁,必须配合 using 使用才有效果
AsyncEx 库包含一整套协调原语 :AsyncManualResetEvent、AsyncAutoResetEvent、AsyncConditionVariable、AsyncMonitor、AsyncSemaphore、AsyncCountdownEvent 和 AsyncReaderWriterLock。
AsyncLock 代替 Lock 锁 ,AsyncLock 支持同步和异步的锁,锁定的区域必须要用 using 包围
using static System.Threading.Tasks.Task;
internal class A {
private readonly AsyncLock lockObj = new AsyncLock();
public int amount;
public async Task AddAsync() {
// 常规的 Lock 锁,不能锁 await
// 配合 using 使用,在 using 资源释放的时候,同时也释放锁
// LockAsync 可以传递 CancellationToken
using ( await lockObj.LockAsync()) {
amount++;
await Delay(10).ConfigureAwait(false);
}
}
}
//开启100个线程
public static async Task Main() {
var a = new A();
var tasks = new Task[100];
//开100个线程执行方法
for (var i = 0; i < 100; i++) {
tasks[i] = Run(async () => {
await a.AddAsync();
});
}
await WhenAll(tasks);
// 100
Console.WriteLine("计算的结果:" + a.amount);
}
AsyncReaderWriterLock 代替 ReaderWriterLockSlim
读写锁是为了在 Lock 锁的基础提供性能,原理是写操作会进行锁定,但是多个读操作不会进行锁定
private readonly ReaderWriterLockSlim _readerWriterLockSlim = new ();
private readonly AsyncReaderWriterLock _asyncReaderWriter = new();
// ReaderWriterLockSlim 在异步里不会有效果
public async Task AddAsync() {
_readerWriterLockSlim.EnterWriteLock();
amount++;
await Delay(10).ConfigureAwait(false);
_readerWriterLockSlim.ExitWriteLock();
}
// 用 AsyncReaderWriterLock 才能做到异步的读写锁
public async Task AddAsync() {
using ( await _asyncReaderWriter.WriterLockAsync()) {
amount++;
await Delay(10).ConfigureAwait(false);
}
}
AsyncMonitor 代替 Monitor
Monitor 是 Lock 的底层实现,在一些极端情况下可以使用
AsyncManualResetEvent、AsyncAutoResetEvent 代替 ManualResetEvent,AutoResetEvent
ManualResetEvent,AutoResetEvent 用于线程的互相同步,通过事件的方式
Lock 在 abp 里的一个好例子
Lock 锁住使用的资源,这是一个非常明确的锁定资源的一个例子
public interface ITestCounter
{
int Add(string name, int count);
int Decrement(string name);
int Increment(string name);
int GetValue(string name);
}
public class TestCounter : ITestCounter, ISingletonDependency
{
// 保存资源的字典
private readonly Dictionary<string, int> _values;
public TestCounter()
{
_values = new Dictionary<string, int>();
}
public int Increment(string name)
{
return Add(name, 1);
}
public int Decrement(string name)
{
return Add(name, -1);
}
public int Add(string name, int count)
{
//对资源进行操作,就先锁住资源
lock (_values)
{
var newValue = _values.GetOrDefault(name) + count;
_values[name] = newValue;
return newValue;
}
}
public int GetValue(string name)
{
lock (_values)
{
return _values.GetOrDefault(name);
}
}
}
lock 锁资源的互相等待
如下类型有启动和停止方法,在启动方法后,必须要等待启动方法的回调执行完毕,停止方法才能执行 启动了等待启动执行结束启动回调执行 2023/10/11 15:48:31 停止了
public class DoTask {
private readonly object lockObj = new object();
public async void Start( Func<string,string> callBack) {
lock (lockObj) {
Console.WriteLine("启动了");
}
await Task.Run(() => {
Thread.Sleep(5000);
Console.WriteLine(callBack.Invoke("启动回调执行"));
});
lock (lockObj) {
// Monitor 方法必须用在 lock 内
Monitor.Pulse(lockObj);
}
}
public void Stop() {
Console.WriteLine("等待启动执行结束");
lock (lockObj) {
// Monitor 方法必须用在 lock 内
Monitor.Wait(lockObj);
Console.WriteLine("停止了");
}
}
}
类型系统
class A{
public TProperty F<TProperty>(string value ){
(TProperty)TypeDescriptor.GetConverter(conversionType).
ConvertFromInvariantString(value)
}
public TProperty F<(TProperty)>(object value){
return Convert.ChangeType(value, conversionType, CultureInfo.InvariantCulture);
}
}
Channel
Channel 可以保证消息的顺序性。 Channel 分为限容 Channel(BoundedChannel)和不限容
Channel(UnBundedChannel)。Channel 主要通过自身的静态方法进行实例化: var channel =
Channel.CreateUnbounded
using System.Threading.Channels;
var channel = Channel.CreateUnbounded<int>();
Task.Run(async () =>
{
for (var i = 0; i < 10; i++)
{
await Task.Delay(TimeSpan.FromMilliseconds(200));
Console.WriteLine("发送:" + i);
await channel.Writer.WriteAsync(i);
}
Console.WriteLine("发送结束");
channel.Writer.Complete();
});
Task.Run(async () =>
{
//ReadAllAsync 读取的是一个异步流
await foreach (var item in channel.Reader.ReadAllAsync())
{
Console.WriteLine("收到:"+item);
}
Console.WriteLine("接收结束");
});
Console.ReadKey();
解构 Deconstruct
var foo=new Bar(10,"10");
var (i,j) = foo;
Console.WriteLine(i+":"+j);
public class Bar {
public int Id { get; set; }
public string Name { get; set; }
//顺序赋值
public Bar( int id, string name) => (Id, Name) = (id, name);
//解构 写成方法体也是可以的
public void Deconstruct(out int id, out string name) => (id, name) = (Id, Name);
}
模式匹配
模式匹配 可以简化多重 if ,达到策略模式的效果使用方法上通过 is ,switch 表达式
is
static T MidPoint<T>(IEnumerable<T> sequence) {
// 做类型判断
if (sequence is IList<T> list)
{
return list[list.Count / 2];
}
// 做空检查
if (sequence is null)
{
throw new ArgumentNullException(nameof(sequence), "Sequence can't be null.");
}
int halfLength = sequence.Count() / 2 - 1;
if (halfLength < 0) halfLength = 0;
return sequence.Skip(halfLength).First();
}
switch
static string PoliceHandlerContext(Bar bar) {
var i= bar switch {
Bar.police1=> Handler1(),
Bar.police2=> Handler2(),
_=>"",
};
return i;
}
static string Handler1() {
return nameof(Handler1);
}
static string Handler2() {
return nameof(Handler2);
}
internal enum Bar {
police1,police2
}
ref struck
ref struct 是 C# 7.2 引入的新特性,它允许我们创建一个只能在栈上分配的结构。与普通的 struct 不同,ref struct 不能用作字段、属性或数组的类型,也不能用作泛型参数。它只能用作局部变量、 方法的参数或返回类型。
ref struct 的主要优势是它的实例永远不会分配到托管堆上。这是通过将实例的生命周期限制在创建 它的方法或作用域内来实现的。当方法或作用域结束时,ref struct 的实例将自动被销毁,不需要垃 圾收集器来回收它们。
这种限制确保了 ref struct 的实例不会逃逸到方法或作用域之外,从而避免了对 GC 的压力。这对于 需要高性能和低内存消耗的应用程序非常有用,特别是在处理大量数据或需要频繁创建和销毁对象的情 况下。
普通的 struct 如何做装箱操作或是类的字段,它就会保存在堆内存上
Span 可以统一包装托管和非托管的内存,并提供统一的操作方法
public readonly ref struck Span<T>{
// _reference 是一个指针,指向连续内存的首地址
internal readonly ref T _reference;
/// <summary>The number of elements this Span contains.</summary>
private readonly int _length;
}
为什么我们需要 ref struct 来实现 Span?第一个原因是跨度基本上包含两个字段:
- 第一个字段指向数据本身
- 第二个字段存储包装内存的长度。
为什么非要设计成是只能存在于堆栈呢:
- 对此类结构的写入都不会是原子的。这意味着如果跨度存在于堆上,那么一旦我们修改跨度,我们 就应该始终锁定它们,这样我们就会失去跨度提供的性能优势。或者我们会跳过锁,但这样我们就 会冒超出范围访问和类型安全的风险。而这个问题是顺便一提称为结构撕裂。
- CoreCLR 上的 span 实现在其字段之一中包含托管指针。并且这些托管指针不能是堆对象的字段。
Span 类型表示驻留在托管堆、堆栈甚至非托管内存中的连续内存块,如果创建一个基元类型的数组(使
用 stackalloc 创建),它将在堆栈上分配,并且不需要垃圾回收来管理其生存期。Span
以下是一目了然的 Span
- Value type 值类型
- Low or zero overhead 低或零开销
- High performance 高性能
- Provides memory and type safety 提供内存和类型安全
开发者可以将 Span 与下列任一项一起使用
- Arrays
- Strings
- Native buffers 本地缓冲区
可以转换为 Span
- Arrays
- Pointers 指针
- IntPtr 指针
- stackalloc
- string
托管内存
var managed = new[]{(byte)1,(byte)2,(byte)3,(byte)4,(byte)5};
var spanManaged = new Span<byte>(managed);
foreach (var i in managed) {
Console.WriteLine("托管对象:"+i);
}
非托管堆内存
//堆内存分配 分配5个字节 Handle to Global Memory",意思是分配一个全局内存块的句柄
IntPtr myArray = Marshal.AllocHGlobal(5);
unsafe {
var nativeSpan = new Span<byte>(myArray.ToPointer(), 5);
foreach (var j in nativeSpan) {
Console.WriteLine("origin:"+j);
}
Console.WriteLine("----------");
for (var index = 0; index < nativeSpan.Length; index++) {
nativeSpan[index] = (byte)index;
Console.WriteLine("changed:"+nativeSpan[index]);
var prt= (byte*)myArray;
Console.WriteLine("ptr:"+ *prt );
myArray++;
}
//释放堆内存
Marshal.FreeHGlobal(myArray);
}
非托管栈内存
unsafe {
var ptr= stackalloc byte[5];
var nativeSpan = new Span<byte>(ptr, 5);
//Span<byte> stackSpan = stackalloc byte[5];
foreach (var j in nativeSpan) {
Console.WriteLine("origin:"+j);
}
Console.WriteLine("----------");
Console.WriteLine(ptr[4]);
for (var index = 0; index < nativeSpan.Length; index++) {
nativeSpan[index] = (byte)index;
Console.WriteLine("changed:"+nativeSpan[index]);
Console.WriteLine("ptr:"+ *ptr );
ptr++;
}
}
ReadOnlySpan 提供了只读的 Span
Memory
和 Span 一样是操作连续的内存块,但是 Memory 取消了 ref 的限制
public readonly struct Memory<T> : IEquatable<Memory<T>>
{
private readonly object? _object;
private readonly int _index;
private readonly int _length;
}
指针
C# 中,IntPtr 是一个代表内存位置指针的类型。它被用来存储一个变量或一个对象在内存中的地址 。IntPtr 是一个整数类型,但它的大小与平台有关。在 32 位系统中,IntPtr 的大小为 32 比特(4 字节),在 64 位系统中,它的大小为 64 比特(8 字节)。
IntPtr 通常在处理非托管代码或与其他使用指针的语言相互操作时使用。例如,如果你从动态链接库 (DLL)中调用一个以指针为参数的函数,你可以使用 IntPtr 将一个变量的地址传递给该函数。C# 中 主要用它调用 C++\C 封装的 DLL 库;
{
IntPtr ptr1 = Marshal.AllocHGlobal(sizeof(int));
Marshal.WriteInt32(ptr1, 1);
int nVal1 = Marshal.ReadInt32(ptr1, 0);
Console.WriteLine(nVal1);
Marshal.FreeHGlobal(ptr1);
}
{
string str = "aa";
IntPtr strPtr = Marshal.StringToHGlobalAnsi(str);
string ss = Marshal.PtrToStringAnsi(strPtr);
Marshal.FreeHGlobal(strPtr);
}
TaskCompletionSource
把回调用 TaskCompletionSource 进行包装,就可以返回 Task
IChangeToken 微软扩展的功能
namespace Microsoft.Extensions.Primitives
{
public interface IChangeToken
//通过过一次就赋值为 true
bool HasChanged { get; }
bool ActiveChangeCallbacks { get; }
IDisposable RegisterChangeCallback(Action<object?> callback, object? state);
}
}
ChangeToken.OnChange 消息可以反复通知
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "x.txt");
using var fileProvider = new PhysicalFileProvider(AppDomain.CurrentDomain.BaseDirectory);
// ChangeToken.OnChange 消息可以反复通知
ChangeToken.OnChange(() => {
//之所以可以反复消费原因是提供了 IChangeToken 生产源头,消费完了就在获取一个 IChangeToken
IChangeToken cancellationToken = fileProvider.Watch("x.txt");
return cancellationToken;
}
, obj => {
Console.WriteLine("文件改了,可以反复通知"+obj);
}, "x.txt");
Task.Run(async () => {
while (true) {
using var i = File.AppendText(path);
await i.WriteLineAsync("new");
await Task.Delay(1000);
}
});
Console.ReadKey();
changeToken.RegisterChangeCallback 只通知一次
// IChangeToken 是一次性的消费过就不能在消费了
IChangeToken changeToken = fileProvider.Watch("x.txt");
changeToken.RegisterChangeCallback(obj => {
Console.WriteLine("文件改了,只能通知一次" + obj);
},"");
changeToken.Disposed();