この記事では主に、C# でのタイマーの使用とリエントランシーの問題の解決に関する関連知識を紹介します。とても良い参考値です
★前書き
長いことブログを書いていなかったライブライターを開きます。本当に怠け者すぎる。早速、このブログのテーマであるタイマーに直接行きましょう。なぜこれを書いているかというと、数日前に友人から「ハッカー」ガジェットを作るように誘われたからです。その機能は非常に単純で、クリップボードの内容を自動的に取得してメールを送信する必要があります。タイマーを使用してクリップボードの内容をループしますが、電子メールを送信する機能のため、C# を使用した SmtpClient は以前に同様の電子メール送信機能を作成したことがあります。 NetEase を使用できましたが、今は使用できません。何が起こったのかわからないので、あきらめなければなりませんでした。 Timer を使用しているときに、これまで考えもしなかった問題、つまり再入の問題に遭遇しました。
★はじめに
まず、ここで言うタイマーとはSystem.Timers.timerのことを指します。公式の紹介文はここにあり、以下に抜粋します:
Timer コンポーネントは、アプリケーションで Elapsed イベントが発生する定期的な間隔を指定できるサーバーベースのタイマーです。このイベントは、一般的な処理を提供するために処理できます。 たとえば、1 日 24 時間、年中無休で稼働する必要がある重要なサーバーがあるとします。 タイマーを使用してサーバーを定期的にチェックし、システムが稼働していることを確認するサービスを作成できます。 システムが応答しなくなった場合、サービスはサーバーの再起動を試行するか、管理者に通知することができます。 サーバーベースのタイマーは、マルチスレッド環境のセカンダリ スレッドで使用するように設計されています。 サーバー タイマーはスレッド間を移動して発生した Elapsed イベントを処理できるため、Windows タイマーよりも正確な時間にイベントを発生させることができます。
他のタイマーとの違いを知りたい場合は、ここで詳しく紹介されているので、これ以上は言いません(実際、他にもたくさんあるとは知りませんでした)。では、このタイマーを使用するとどのようなメリットがあるのでしょうか?その主な理由は、.NET スレッド プールを通じて実装され、軽量で正確なタイミングを持ち、アプリケーションやメッセージに対する特別な要件がないためです。
★使用方法
以下は、このタイマーの使用方法の簡単な紹介です。実際には、テストのために Microsoft が提供したサンプルを使用し、コードを直接実行しました。動作は以下の通り、タイミングはかなり正確です:
//Timer不要声明成局部变量,否则会被GC回收 private static System.Timers.Timer aTimer; public static void Main() { //实例化Timer类,设置间隔时间为10000毫秒; aTimer = new System.Timers.Timer(10000); //注册计时器的事件 aTimer.Elapsed += new ElapsedEventHandler(OnTimedEvent); //设置时间间隔为2秒(2000毫秒),覆盖构造函数设置的间隔 aTimer.Interval = 2000; //设置是执行一次(false)还是一直执行(true),默认为true aTimer.AutoReset = true; //开始计时 aTimer.Enabled = true; Console.WriteLine("按任意键退出程序。"); Console.ReadLine(); } //指定Timer触发的事件 private static void OnTimedEvent(object source, ElapsedEventArgs e) { Console.WriteLine("触发的事件发生在: {0}", e.SignalTime); }
★
リエントランシー問題の再現と分析リエントランシーとは何ですか?これはマルチスレッド プログラミングに関する概念です。プログラム内で複数のスレッドが同時に実行されると、同じメソッドが複数のプロセスによって同時に呼び出される可能性があります。このメソッドにスレッドセーフでないコードが含まれている場合、メソッドの再入によりデータの不整合が発生します。タイマー メソッドの再入とは、1 つのタイマーの処理が完了する前に、時間が経過すると、他のタイマーが処理のためにメソッドに入り続けることを指します。以下は、リエントラント問題の発生を示しています (再現性はあまり良くないかもしれませんが、問題を簡単に説明することもできます):
/* 按任意键退出程序。 触发的事件发生在: 2014/12/26 星期五 23:08:51 触发的事件发生在: 2014/12/26 星期五 23:08:53 触发的事件发生在: 2014/12/26 星期五 23:08:55 触发的事件发生在: 2014/12/26 星期五 23:08:57 触发的事件发生在: 2014/12/26 星期五 23:08:59 */上記の出力は次のように感じますか?結果が奇妙です。最初、スレッド 1 の出力は 1 ですが、これは問題ありません。その後、2 秒後にスレッド 1 が 1 ずつ増加して 2 を出力します。なぜ、スレッド 2 の出力が途中で表示されるのでしょうか。さらに奇妙なのは、スレッド 2 の出力は最初は 1 でしたが、1 ずつ増加すると 3 になったことです。実際、これはリエントランシーによって引き起こされる問題です。心配しないでください。分析後に原因がわかります。
まず、タイマーが計時を開始した後、スレッド 1 の実行メソッドが開始され、スレッド 1 が初めて出力するとき、設定された計時間隔が 1 であるため、スレッド 1 は 2 秒間スリープしません。スレッド 1 が 1 秒間スリープした後、スレッド 2 はスレッド 1 が実行中であるかスリープ中であるかを気にせず、スレッド 2 の実行メソッドを再び開始します。眠っていて、それ自体は増加しません。さらに 1 秒が経過し、同時に 2 つのイベントが発生し、スレッド 1 が休止状態を通過し、自動インクリメント出力が 2 になりました。また、タイマーはスレッド 3 の出力も同時に開始しました。スレッド 1 が自動インクリメントされた後の値は 2 でした。さらに 1 秒後、スレッド 2 はすでに 2 であったため、自動インクリメント後の出力は 3 になりました。問題は、1 つの Timer によって開始されたスレッドの処理が完了していないときに、別の Timer が処理のためにメソッドに入り続けることです。
では、この問題を解決するにはどうすればよいでしょうか?さまざまなシナリオに適応するために 3 つの解決策を 1 つずつ見てみましょう。ただし、より安全な最後の解決策をお勧めします。
★
再入可能問題の解決策1、使用lock(Object)的方法来防止重入,表示一个Timer处理正在执行,下一个Timer发生的时候发现上一个没有执行完就等待执行,适用重入很少出现的场景(具体也没研究过,可能比较占内存吧)。
代码跟上面差不多,在触发的方法中加入lock,这样当线程2进入触发的方法中,发现已经被锁,会等待锁中的代码处理完在执行,代码如下:
private static object locko = new object(); /// <summary> /// System.Timers.Timer的回调方法 /// </summary> /// <param name="sender"></param> /// <param name="args"></param> private static void TimersTimerHandler(object sender, EventArgs args) { int t = ++num; lock (locko) { Console.WriteLine(string.Format("线程{0}输出:{1}, 输出时间:{2}", t, outPut.ToString(), DateTime.Now)); System.Threading.Thread.Sleep(2000); outPut++; Console.WriteLine(string.Format("线程{0}自增1后输出:{1},输出时间:{2}", t, outPut.ToString(), DateTime.Now)); } }
执行结果:
2、设置一个标志,表示一个Timer处理正在执行,下一个Timer发生的时候发现上一个没有执行完就放弃(注意这里是放弃,而不是等待哦,看看执行结果就明白啥意思了)执行,适用重入经常出现的场景。代码如下:
private static int inTimer = 0; /// <summary> /// System.Timers.Timer的回调方法 /// </summary> /// <param name="sender"></param> /// <param name="args"></param> private static void TimersTimerHandler(object sender, EventArgs args) { int t = ++num; if (inTimer == 0) { inTimer = 1; Console.WriteLine(string.Format("线程{0}输出:{1}, 输出时间:{2}", t, outPut.ToString(), DateTime.Now)); System.Threading.Thread.Sleep(2000); outPut++; Console.WriteLine(string.Format("线程{0}自增1后输出:{1},输出时间:{2}", t, outPut.ToString(), DateTime.Now)); inTimer = 0; } }
执行结果:
3、在多线程下给inTimer赋值不够安全,Interlocked.Exchange提供了一种轻量级的线程安全的给对象赋值的方法(感觉比较高上大,也是比较推荐的一种方法),执行结果与方法2一样,也是放弃执行。Interlocked.Exchange用法参考这里。
private static int inTimer = 0; /// <summary> /// System.Timers.Timer的回调方法 /// </summary> /// <param name="sender"></param> /// <param name="args"></param> private static void TimersTimerHandler(object sender, EventArgs args) { int t = ++num; if (Interlocked.Exchange(ref inTimer, 1) == 0) { Console.WriteLine(string.Format("线程{0}输出:{1}, 输出时间:{2}", t, outPut.ToString(), DateTime.Now)); System.Threading.Thread.Sleep(2000); outPut++; Console.WriteLine(string.Format("线程{0}自增1后输出:{1},输出时间:{2}", t, outPut.ToString(), DateTime.Now)); Interlocked.Exchange(ref inTimer, 0); } }
执行结果:
★总结
终于码完字了,真心不容易啊。写博客是个挺耗精力的事情,真心佩服那些大牛们笔耕不辍,致敬!在这里稍微总结一下,timer是一个使用挺简单的类,拿来即用,这里主要总结了使用timer时重入问题的解决,以前也没思考过这个问题,解决方案也挺简单,在这里列出了三种,不知道还有没有其他的方式。这里的解决方案同时也适用多线程的重入问题。
以上がC# でのタイマーの使用法と再入可能の問題の解決方法の詳細な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。