<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      【.NET】聊聊 IChangeToken 接口

      由于兩個月的奮戰(zhàn),導(dǎo)致很久沒更新了。就是上回老周說的那個產(chǎn)線和機械手搬貨的項目,好不容易等到工廠放假了,我就偷偷樂了。當(dāng)然也過年了,老周先給大伙伴們拜年了,P話不多講,就祝大家身體健康、生活愉快。其實生活和健康是密不可分的,想活得好,就得健康。包括身體健康、思想健康、心理健康、精神健康。不能以為我無病無痛就很健康,你起碼要全方位健康。

      不管你的工作是什么,忙或者不忙,報酬高或低,但是,人,總得活,總得過日子。咱們最好多給自己點福利,多整點可以自娛自樂的東西,這就是生活。下棋、打游戲、繪畫、書法、釣魚、飆車、嗩吶……不管玩點啥,只要積極正向的就好,可以大大降低得抑郁癥、高血壓的機率;可以減少70%無意義的煩惱;可以降低跳樓風(fēng)險;在這個禮崩樂壞的社會環(huán)境中,可以抵御精神污染……總之,益處是大大的有。

      然后老周再說一件事,一月份的時候常去工廠調(diào)試,也認(rèn)識了機械臂廠商派的技術(shù)支持——吳大工程師。由于工廠所處地段非常繁華,因此每次出差,午飯只能在附近一家四川小吃店解決。畢竟這方圓百十里也僅此一家。不去那里吃飯除非自帶面包蹲馬路邊啃,工廠不供食也不供午休場所。剛開始幾次出差還真的像個傻子似的蹲馬路邊午休。后來去多了,直接鉆進(jìn)工廠的會議室睡午覺。

      有一天吃午飯時,吳老師說:你說什么樣的人編程水平最高?

      我直接從潛意識深處回答他:我做一個排序,僅供參考。編程水平從高到低排行:

      1、黑客。雖然大家都說黑客一代不如一代,但目前來說,這群人還是最強的;

      2、純粹技術(shù)愛好者;

      3、著名開源項目貢獻(xiàn)者。畢竟拿不出手的代碼也不好意思與人分享;

      4、做過許多項目的一線開發(fā)者。我強調(diào)的項目數(shù)量多,而不是長年只維護(hù)一個項目的。只有數(shù)量多你學(xué)到的才多;

      5、社區(qū)貢獻(xiàn)較多者,這個和3差不多。不過,老周認(rèn)為的社區(qū)貢獻(xiàn)就是不僅提供代碼,還提供文檔、思路、技巧等;

      6、剛?cè)肟拥A(chǔ)較好的開發(fā)者;

      7、培訓(xùn)機構(gòu)的吹牛專業(yè)戶;

      8、大學(xué)老師/教授;

      9、短視頻平臺上的磚家、成宮人士;

      10、剛學(xué)會寫 main 函數(shù)的小朋友。

      ==========================================================================================================

      下面進(jìn)入主題,咱們今天聊聊 IChangeToken。它的主要功能是提供更改通知。比如你的配置源發(fā)生改變了,要通知配置的使用者重新加載。你可能會疑惑,這貨跟使用事件有啥區(qū)別?這個老周也不好下結(jié)論,應(yīng)該是為異步代碼準(zhǔn)備的吧。

      下面是 IChangeToken 接口的成員:

      bool HasChanged { get; }
      bool ActiveChangeCallbacks { get; }
      IDisposable RegisterChangeCallback(Action<object?> callback, object? state);

      這個 Change Token 思路很清奇,實際功能類似事件,就是更改通知。咱們可以了解一下其原理,但如果你覺得太繞,不想了解也沒關(guān)系的。在自定義配置源時,咱們是不需要自己寫 Change Token 的,框架已有現(xiàn)成的。我們只要知道要觸發(fā)更改通知時調(diào)用相關(guān)成員就行。

      如果你想看源碼的話,老周可以告你哪些文件(github 項目是 dotnet\runtime):

      1、runtime-main\src\libraries\Common\src\Extensions\ChangeCallbackRegistrar.cs:這個主要是 UnsafeRegisterChangeCallback 方法,用于注冊回調(diào)委托;

      2、runtime-main\src\libraries\Microsoft.Extensions.Primitives\src\ChangeToken.cs:這個類主要是提供靜態(tài)的輔助方法,用于注冊回調(diào)委托。它的好處是可以循環(huán)——注冊回調(diào)后,觸發(fā)后委托被調(diào)用;調(diào)用完又自動重新注冊,使得 Change Token 可以多次觸發(fā);

      3、runtime-main\src\libraries\Microsoft.Extensions.Primitives\src\CancellationChangeToken.cs:這個類是真正實現(xiàn) IChangeToken 接口的;

      4、runtime-main\src\libraries\Microsoft.Extensions.Configuration\src\ConfigurationReloadToken.cs:這個也是實現(xiàn) IChangeToken 接口,而且它才是咱們今天的主角,該類就是為重新加載配置數(shù)據(jù)而提供的。調(diào)用它的 OnReload 方法可以觸發(fā)更改通知。

      看了上面這些,你可能更疑惑了。啥原理?為啥 Token 只能觸發(fā)一次?為何要重新注冊回調(diào)?

      咱們用一個簡單例子演練一下。

      static void Main(string[] args)
      {
          CancellationTokenSource cs = new();
          // 這里獲取token
          CancellationToken token = cs.Token;
          // token 可以注冊回調(diào)
          token.Register(() =>
          {
              Console.WriteLine("你按下了【K】鍵");
          });
          // 啟動一個新task
          Task myTask = Task.Run(() =>
          {
              // 等待輸入,如果按下【K】鍵,就讓CancellationTokenSource取消
              ConsoleKeyInfo keyInfo;
              while(true)
              {
                  keyInfo = Console.ReadKey(true);
                  if(keyInfo.Key == ConsoleKey.K)
                  {
                      // 取消
                      cs.Cancel();
                      break;
                  }
              }
          });
          // 主線程等待任務(wù)完成
          Task.WaitAll(myTask);
      }

      CancellationTokenSource 類表示一個取消任務(wù)的標(biāo)記,訪問它的 Token 屬性可以獲得一個 CancellationToken 結(jié)構(gòu)體實例,可以檢索它的 IsCancellationRequested 屬性以明確是否有取消請求(有則true,無則false)。

      還有更重要的,CancellationToken 結(jié)構(gòu)體的 Register 方法可以注冊一個委托作為回調(diào),當(dāng)收到取消請求后會觸發(fā)這個委托。對的,這個就是 Change Token 靈魂所在了。一旦回調(diào)被觸發(fā)后,CancellationTokenSource 就處于取消狀態(tài)了,你無法再次觸發(fā),除非重置或重新實例化。這就是回調(diào)只能觸發(fā)一次的原因。

      下面,咱們完成一個簡單的演示——用數(shù)據(jù)庫做配置源。在 SQL Server 里面隨便建個數(shù)據(jù)庫,然后添加一個表,名為 tb_configdata。它有四個字段:

      CREATE TABLE [dbo].[tb_configdata](
          [ID] [int] NOT NULL,
          [config_key] [nvarchar](15) NOT NULL,
          [config_value] [nvarchar](30) NOT NULL,
          [remark] [nvarchar](50) NULL,
       CONSTRAINT [PK_tb_configdata] PRIMARY KEY CLUSTERED 
      (
          [ID] ASC,
          [config_key] ASC
      )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
      ) ON [PRIMARY]
      GO

      ID和config_key設(shè)為主鍵,config_value 是配置的值,remark 是備注。備注字段其實可以不用,但實際應(yīng)用的時候,可以用來給配置項寫點注釋。

      然后,在程序里面咱們用到 EF Core,故要先生成與表對應(yīng)的實體類。這里老周就不用工具了,直接手寫更有效率。

      // 實體類
      public class MyConfigData
      {
          public int ID { get; set; }
          public string ConfigKey { get; set; } = string.Empty;
          public string ConfigValue { get; set; } = string.Empty;
          public string? Remark { get; set; }
      }
      
      // 數(shù)據(jù)庫上下文對象
      public class DemoConfigDBContext : DbContext
      {
          public DbSet<MyConfigData> ConfigData => Set<MyConfigData>();
      
          protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
          {
              optionsBuilder.UseSqlServer("Data Source=DEV-PC\\SQLTEST;Initial Catalog=Demo;Integrated Security=True;Connect Timeout=30;Encrypt=True;Trust Server Certificate=True;Application Intent=ReadWrite;Multi Subnet Failover=False");
          }
      
          protected override void OnModelCreating(ModelBuilder modelbd)
          {
              modelbd.Entity<MyConfigData>()
                  .ToTable("tb_configdata")
                  .HasKey(c => new { c.ID, c.ConfigKey });
              modelbd.Entity<MyConfigData>()
                  .Property(c => c.ConfigKey)
                  .HasColumnName("config_key");
              modelbd.Entity<MyConfigData>()
                  .Property(c => c.ConfigValue)
                  .HasColumnName("config_value");
              modelbd.Entity<MyConfigData>()
                  .Property(c => c.Remark)
                  .HasColumnName("remark");
          }
      }

      上述代碼的情況特殊,實體類的名稱和成員名稱與數(shù)據(jù)表并不一致,所以在重寫 OnModelCreating 方法時,需要進(jìn)行映射。

      1、ToTable("tb_configdata") 告訴 EF 實體類對應(yīng)的數(shù)據(jù)表是 tb_configdata;

      2、HasKey(c => new { c.ID, c.ConfigKey }):表明該實體有兩個主鍵——ID和ConfigKey。這里指定的是實體類的屬性,而不是數(shù)據(jù)表的字段名,因為后面咱們會進(jìn)行列映射;

      3、HasColumnName("config_key"):告訴 EF,實體的 ConfigKey 屬性對應(yīng)的是數(shù)據(jù)表中 config_key。后面的幾個屬性的道理一樣,都是列映射。

      做映射就類似于填坑,如果你不想挖坑,那就直接讓實體類名與表名一樣,屬性名與表字段(列)一樣,這樣就省事多了。不過,在實際使用中真沒有那么美好。很多時候數(shù)據(jù)庫是小李負(fù)責(zé)的,人家早就建好了,存儲過程都寫了幾萬個了。后面前臺程序是老張來開發(fā),對老張來說,要么把實體的命名與數(shù)據(jù)庫的一致,要么就做一下映射。多數(shù)情況下是要映射的,畢竟很多時候數(shù)據(jù)庫對象的命名都比較奇葩。尤其有上千個表的時候,為了看得順眼,很多人喜歡這樣給數(shù)據(jù)表命名:ta_XXX、ta_YYY、tb_ZZZ、tc_FFF、tx_PPP、ty_EEE、tz_WWW。還有這樣命名的:m1_Report、m2_ReportDetails…… m105_TMD、m106_WNM、m107_DOUBI。

      這種命名用在實體類上面確實很不優(yōu)雅,所以映射就很必要了。

      此處咱們不用直接實現(xiàn) IConfigurationProvider 接口,而是從 ConfigurationProvider 類派生就行了。自定義配置源的東東老周以前寫過,只是當(dāng)時沒有實現(xiàn)更改通知。

      public class MyConfigurationProvider : ConfigurationProvider, IDisposable
      {
          private System.Threading.Timer theTimer;
      
          public MyConfigurationProvider()
          {
              theTimer = new Timer(OnTimer, null, 100, 10000);
          }
      
          private void OnTimer(object? state)
          {
              // 先調(diào)用Load方法,然后用OnReload觸發(fā)更新通知
              Load();
              OnReload();
          }
      
          public void Dispose()
          {
              theTimer?.Change(0, 0);
              theTimer?.Dispose();
          }
      
          public override void Load()
          {
              // 先讀取一下
              using DemoConfigDBContext dbctx = new();
              // 如果無數(shù)據(jù),先初始化
              if(dbctx.ConfigData.Count() == 0)
              {
                  InitData(dbctx.ConfigData);
              }
              // 加載數(shù)據(jù)
              Data = dbctx.ConfigData.ToDictionary(k => k.ConfigKey, k => (string?)k.ConfigValue);
      
              // 本地函數(shù)
              void InitData(DbSet<MyConfigData> set)
              {
                  int _id = 1;
                  set.Add(new()
                  {
                      ID = _id,
                      ConfigKey = "page_size",
                      ConfigValue = "25"
                  });
                  _id += 1;
                  set.Add(new()
                  {
                      ID = _id,
                      ConfigKey = "format",
                      ConfigValue = "xml"
                  });
                  _id += 1;
                  set.Add(new()
                  {
                      ID = _id,
                      ConfigKey = "limited_height",
                      ConfigValue = "1450"
                  });
                  _id += 1;
                  set.Add(new()
                  {
                      ID = _id,
                      ConfigKey = "msg_lead",
                      ConfigValue = "TDXA_"
                  });
                  // 保存數(shù)據(jù)
                  dbctx.SaveChanges();
              }
          }
      
      }

      由于老周不知道怎么監(jiān)控數(shù)據(jù)庫更新,最簡單的辦法就是用定時器循環(huán)檢查。重點是重寫 Load 方法,完成加載配置的邏輯。Load 方法覆寫后不需要調(diào)用 base 的 Load 方法,因為基類的方法是空的,調(diào)用了也沒毛用。

      在 Timer 對象調(diào)用的方法(OnTimer)中,先調(diào)用 Load 方法,再調(diào)用 OnReload 方法。這樣就可以在加載數(shù)據(jù)后觸發(fā)更改通知。

      然后實現(xiàn) IConfigurationSource 接口,提供 MyConfigurationProvider 實例。

      public class MyConfigurationSource : IConfigurationSource
      {
          public IConfigurationProvider Build(IConfigurationBuilder builder)
          {
              return new MyConfigurationProvider();
          }
      }

      默認(rèn)的配置源有JSON文件、命令行、環(huán)境變量等,為了排除干擾,便于查看效果,在 Main 方法中咱們先把配置源列表清空,再添加咱們自定義的配置源。

      var builder = WebApplication.CreateBuilder(args);
      // 清空配置源
      builder.Configuration.Sources.Clear();
      // 添加配置源到Sources
      builder.Configuration.Sources.Add(new MyConfigurationSource());
      var app = builder.Build();

      最后,可以做個簡單測試,直接注入 Mini-API 中讀取配置。

      app.MapGet("/", (IConfiguration config) =>
      {
          StringBuilder bd = new();
          foreach(var kp in config.AsEnumerable())
          {
              bd.AppendLine($"{kp.Key} = {kp.Value}");
          }
          return bd.ToString();
      });

      運行效果如下:

      這時候咱們到數(shù)據(jù)庫里把配置值改一下。

      update tb_configdata
          set config_value = N'55'
          where config_key = N'page_size'
      
      update tb_configdata
          set config_value = N'1900'
          where config_key = N'limited_height'

      接著回應(yīng)用程序的頁面,刷新一下,配置值已更新。

      這里你可能會有個疑問:連接字符串硬編碼了不太好,要不寫在配置文件中,可是,寫在JSON文件中咱們怎么獲取呢?畢竟 ConfigurationProvider 不使用依賴注入。

      IConfigurationSource 不是有個 Build 方法嗎?Build 方法不是有個參數(shù)是 IConfigurationBuilder 嗎?用它,用它,狠狠地用它。

      public class MyConfigurationSource : IConfigurationSource
      {
          public IConfigurationProvider Build(IConfigurationBuilder builder)
          {
              // 此處可以臨時build一個配置樹,就能獲取到JSON配置文件里面的連接字符串了
              var config = builder.Build();
              string connStr = config["ConnectionStrings:test"]!;
              return new MyConfigurationProvider(connStr);
          }
      }

      前面定義的一些類也要改一下。

      先是 MyConfigurationProvider 的構(gòu)造函數(shù)。

      public class MyConfigurationProvider : ConfigurationProvider, IDisposable
      {
          private System.Threading.Timer theTimer;
          private string connectString;
      
          public MyConfigurationProvider(string cnnstr)
          {
              connectString = cnnstr;
              ……
          }
      
          ……
      }

      DemoConfigDBContext 類是連接字符串的最終使用者,所以也要改一下。

      public class DemoConfigDBContext : DbContext
      {
          private string connStr;
      
          public DemoConfigDBContext(string connectionString)
          {
              connStr = connectionString;
          }
      
          ……
      
          protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
          {
              optionsBuilder.UseSqlServer(connStr);
          }
      }

      在appsettings.json 文件中配置連接字符串。

      {
        "Logging": {
          ……
        },
        "AllowedHosts": "*",
        "ConnectionStrings": {
          "test": "Data Source=DEV-PC\\SQLTEST;Initial Catalog=Demo;Integrated Security=True;Connect Timeout=30;Encrypt=True;Trust Server Certificate=True;Application Intent=ReadWrite;Multi Subnet Failover=False"
        }
      }

      回到 Main 方法,咱們還得加上 JSON 配置源。

      var builder = WebApplication.CreateBuilder(args);
      // 清空配置源
      builder.Configuration.Sources.Clear();
      // 添加配置源到Sources
      builder.Configuration.AddJsonFile("appsettings.json");
      builder.Configuration.Sources.Add(new MyConfigurationSource());
      var app = builder.Build();

      其他的不變。

      -----------------------------------------------------------------------------------------------------

      接下來,咱們弄個一對多的例子。邏輯是這樣的:啟動程序顯示主窗口,接著創(chuàng)建五個子窗口。主窗口上有個大大的按鈕,點擊后,五個子窗口會收到通知。大概就這個樣子:

      子窗口名為 TextForm,代碼如下:

      internal class TestForm : Form
      {
          private IDisposable _changeTokenReg;
          private TextBox _txtMsg;
          public TestForm(Func<IChangeToken?> getToken)
          {
              // 初始化子級控件
              _txtMsg = new()
              {
                  Dock = DockStyle.Fill,
                  Margin = new Padding(5),
                  Multiline = true,
                  ScrollBars = ScrollBars.Vertical
              };
              Controls.Add(_txtMsg);
      
              _changeTokenReg = ChangeToken.OnChange(getToken, OnCallback);
          }
      
          // 回調(diào)方法
          void OnCallback()
          {
              DateTime curtime = DateTime.Now;
              string str = $"{curtime.ToLongTimeString()} 新年快樂\r\n";
              _txtMsg.BeginInvoke(() =>
              {
                  _txtMsg.AppendText(str);
              });
          }
      
          protected override void Dispose(bool disposing)
          {
              // 釋放對象
              if (disposing)
              {
                  _changeTokenReg?.Dispose();
              }
              base.Dispose(disposing);
          }
      }

      窗口上只放了一個文本框。上面代碼中,使用了 ChangeToken.OnChange 靜態(tài)方法,為 Change Token 注冊回調(diào)委托,本例中回調(diào)委托綁定的是 OnCallback 方法,也就是說:當(dāng) Change Token 觸發(fā)后會在文本框中追加文本。OnChange 靜態(tài)方法有兩個重載:

      // 咱們示例中用的是這個版本
      static IDisposable OnChange(Func<IChangeToken?> changeTokenProducer, Action changeTokenConsumer);
      // 這是另一個重載
      static IDisposable OnChange<TState>(Func<IChangeToken?> changeTokenProducer, Action<TState> changeTokenConsumer, TState state);

      上述例子用的是第一個,其實里面調(diào)用的也是第二個重載,只是把咱們傳遞的 OnCallback 方法當(dāng)作 TState 傳進(jìn)去了。

      請大伙伴暫時記住 changeTokenProducer 和 changeTokenConsumer 這兩參數(shù)。changeTokenProducer 也是一個委托,返回 IChangeToken。用的時候一定要注意,每次觸發(fā)之前,Change Token 要先創(chuàng)建新實例。注意是先創(chuàng)建新實例再觸發(fā),否則會導(dǎo)致無限。盡管內(nèi)部會判斷 HasChanged 屬性,可問題是這個判斷是在注冊回調(diào)之后的。這個是跟 Change Token 的清奇邏輯有關(guān),咱們看看 OnChage 的源代碼就明白了。

       public static IDisposable OnChange<TState>(Func<IChangeToken?> changeTokenProducer, Action<TState> changeTokenConsumer, TState state)
       {
           if (changeTokenProducer is null)
           {
               ThrowHelper.ThrowArgumentNullException(ExceptionArgument.changeTokenProducer);
           }
           if (changeTokenConsumer is null)
           {
               ThrowHelper.ThrowArgumentNullException(ExceptionArgument.changeTokenConsumer);
           }
      
           return new ChangeTokenRegistration<TState>(changeTokenProducer, changeTokenConsumer, state);
       }

      簡單來說,就是返回一個 ChangeTokenRegistration 實例,這是個私有類,咱們是訪問不到的,以 IDisposable 接口公開。其中,它有兩個方法是遞歸調(diào)用的:

      private void OnChangeTokenFired()
      {
          // The order here is important. We need to take the token and then apply our changes BEFORE
          // registering. This prevents us from possible having two change updates to process concurrently.
          //
          // If the token changes after we take the token, then we'll process the update immediately upon
          // registering the callback.
          IChangeToken? token = _changeTokenProducer();
      
          try
          {
              _changeTokenConsumer(_state);
          }
          finally
          {
              // We always want to ensure the callback is registered
              RegisterChangeTokenCallback(token);
          }
      }
      
      private void RegisterChangeTokenCallback(IChangeToken? token)
      {
          if (token is null)
          {
              return;
          }
          IDisposable registraton = token.RegisterChangeCallback(s => ((ChangeTokenRegistration<TState>?)s)!.OnChangeTokenFired(), this);
          if (token.HasChanged && token.ActiveChangeCallbacks)
          {
              registraton?.Dispose();
              return;
          }
          SetDisposable(registraton);
      }

      在 ChangeTokenRegistration 類的構(gòu)造函數(shù)中,先調(diào)用 RegisterChangeTokenCallback 方法,開始了整個遞歸套娃的過程。在 RegisterChangeTokenCallback 方法中,為 token 注冊的回調(diào)就是調(diào)用 OnChangeTokenFired 方法。

      而 OnChangeTokenFired 方法中,是先獲取新的 Change Token,再觸發(fā)舊 token。最后,又調(diào)用 RegisterChangeTokenCallback 方法,實現(xiàn)了無限套娃的邏輯。

      因此,咱們在用的時候,必須先創(chuàng)建新的 Change Token 實例,然后再調(diào)用 RegisterChangeTokenCallback 實例的 Cancel 方法。不然這無限套娃會一直進(jìn)行到棧溢出,除非你提前把 ChangeTokenRegistration 實例 Dispose 掉(由 OnChange 靜態(tài)方法返回)。可是那樣的話,你就不能多次接收更改了。

      下面就是主窗口部分,也是最危險的部分——必須按照咱們上面分析的順序進(jìn)行,不然會 Stack Overflow。

      public partial class Form1 : Form
      {
          private CancellationTokenSource _cancelTkSource;
          private CancellationChangeToken _changeToken;
          public Form1()
          {
              InitializeComponent();
              _cancelTkSource = new CancellationTokenSource();
              _changeToken = new(_cancelTkSource.Token);
              button1.Click += OnButton1Click;
              button2.Click += OnButton2Click;
          }
      
          private void OnButton2Click(object? sender, EventArgs e)
          {
              for(int t= 0; t < 5; t++)
              {
                  TestForm frm = new(GetChangeToken);
                  frm.Text = "窗口" + (t + 1);
                  frm.Size = new Size(300, 240);
                  frm.StartPosition = FormStartPosition.CenterParent;
                  frm.Show(this);
              }
          }
      
          // 這個地方就是觸發(fā)token了,所以要先換上新的實例
          private void OnButton1Click(object? sender, EventArgs e)
          {
              // 先創(chuàng)建新的實例
              var oldsource = Interlocked.Exchange(ref _cancelTkSource, new CancellationTokenSource());
              Interlocked.Exchange(ref _changeToken, new CancellationChangeToken(_cancelTkSource.Token));
              // 只要CancellationTokenSource一取消,其他客戶端會收到通知
              oldsource.Cancel();
          }
      
          // 這個方法傳遞給 TestForm 構(gòu)造函數(shù),再傳給 OnChange 靜態(tài)方法
          public IChangeToken? GetChangeToken()
          {
              return _changeToken;
          }
      }

      按鈕1的單擊事件處理方法就是觸發(fā)點,所以,CancellationTokenSource、CancellationChangeToken 要先換成新的實例,然后再用舊的實例去 Cancel。這里用 Interlocked 類會好一些,畢竟要考慮異步的情況,雖然咱這里都是在UI線程上傳遞的,但還是遵守這個習(xí)慣好一些。

      這樣處理就能避免棧溢出了。運行后,先打開五個子窗口(多點擊一次就能創(chuàng)建十個子窗口)。接著點擊大大按鈕,五個子窗口就能收到通知了。

       

      好了,這次就聊到這兒了。

      posted @ 2024-02-11 12:29  東邪獨孤  閱讀(4970)  評論(5)    收藏  舉報
      主站蜘蛛池模板: 色噜噜亚洲精品中文字幕| 91中文字幕一区二区| 久久波多野结衣av| 九九日本黄色精品视频| 亚洲精品无码久久一线| 亚洲成av人最新无码不卡短片| 免费午夜无码片在线观看影院| 国产美女被遭强高潮免费一视频 | 无遮高潮国产免费观看| 国产在线观看免费观看| 国产小视频一区二区三区| 亚洲 日本 欧洲 欧美 视频| 日韩少妇人妻vs中文字幕| 日韩国产成人精品视频| 丝袜美腿一区二区三区| 在线精品自拍亚洲第一区| 亚洲熟妇av综合一区二区| 猫咪AV成人永久网站在线观看| 日韩有码中文在线观看| 色偷偷女人的天堂亚洲网| 无码中文字幕热热久久| 麻豆精品一区二区三区蜜桃 | 精品人妻系列无码一区二区三区| 色综合国产一区二区三区| 国产在线国偷精品免费看| 亚洲最大在线精品| 平凉市| 建瓯市| h动态图男女啪啪27报gif| 欧美性XXXX极品HD欧美风情| 少妇人妻偷人精品系列| 精品久久久久久无码人妻蜜桃| 一本大道无码av天堂| 高清破外女出血AV毛片| 亚洲综合色一区二区三区| 亚洲AV无码久久精品日韩| 国产精品va无码一区二区| 亚洲中文字幕久久精品品| 国产91丝袜在线播放动漫| 国产成人免费永久在线平台| 亚洲乱理伦片在线观看中字|