では、C#Windowsサービスの複数のスレッドが同時に実行されないようにするための最良の方法は何だと思いますか(サービスはtimer
イベントでOnElapsed
を使用しています)?
lock()
またはmutex
を使用しますか?
mutex
の概念を理解できないようですが、私の場合はlock()
を使用するとうまくいくようです。
とにかくmutex
の使い方を学ぶのに時間を費やすべきですか?
タイマーをワンショットにして、経過したイベントハンドラーで再初期化します。たとえば、System.Timers.Timer
を使用している場合は、次のように初期化します。
myTimer.Elapsed = timer1Elapsed;
myTimer.Interval = 1000; // every second
myTimer.AutoReset = false; // makes it fire only once
myTimer.Enabled = true;
そして、経過したイベントハンドラー:
void timerElapsed(object source, ElapsedEventArgs e)
{
// do whatever needs to be done
myTimer.Start(); // re-enables the timer
}
これの欠点は、タイマーが1秒間隔で起動しないことです。むしろ、最後のティックの処理が終了してから1秒後に起動します。
タイマーを使用してスレッドを生成しないでください。 1つのスレッドのみを開始します。スレッドが作業サイクルを終了したら、次のサイクルが開始されるまでの残り時間を計算します。この間隔が0または負の場合は、すぐにループバックして新しいサイクルを開始します。正の場合は、ループバックする前にその間隔でスリープします。
これは通常、終了ティックと開始ティックの間の符号なしint減算のint結果を取得することによって行われます。したがって、作業で取得された経過ティックを指定します。これを目的の間隔から引くと、新しい残り時間が得られます。
追加のタイマースレッドは必要ありません。2つのスレッドが同時に実行される可能性はありません。全体的な設計が簡素化され、継続的な作成/開始/終了/破棄、malloc、new()、スタックの割り当て/割り当て解除、GCはありません。
タイマー、ミューテックス、セマフォ、ロックなどを使用する他の設計は、非常に複雑です。余分なスレッドを作成しないほうが簡単で簡単なのに、なぜシンクロで余分なスレッドを止めようとするのですか?
時々、sleep()ループの代わりにタイマーを使用することは本当に悪い考えです。これはその時のように聞こえます。
public void doWorkEvery(int interval)
{
while (true)
{
uint startTicks;
int workTicks, remainingTicks;
startTicks = (uint)Environment.TickCount;
DoSomeWork();
workTicks=(int)((uint)Environment.TickCount-startTicks);
remainingTicks = interval - workTicks;
if (remainingTicks>0) Thread.Sleep(remainingTicks);
}
}
lockの代わりに、Monitor.TryEnter()を使用して、コールバックが別のタイマースレッドによってすでに実行されている場合に返すことができます。
class Program
{
static void Main(string[] args)
{
Timer t = new Timer(TimerCallback, null,0,2000);
Console.ReadKey();
}
static object timerLock = new object();
static void TimerCallback(object state)
{
int tid = Thread.CurrentThread.ManagedThreadId;
bool lockTaken = false;
try
{
lockTaken = Monitor.TryEnter(timerLock);
if (lockTaken)
{
Console.WriteLine("[{0:D02}]: Task started", tid);
Thread.Sleep(3000); // Do the work
Console.WriteLine("[{0:D02}]: Task finished", tid);
}
else
{
Console.WriteLine("[{0:D02}]: Task is already running", tid);
}
}
finally
{
if (lockTaken) Monitor.Exit(timerLock);
}
}
}
2つのスレッド同じプロセス/アプリドメイン内が同時に実行されないようにするだけの場合は、lock
ステートメントで十分です。
ただし、lock
は、クリティカルセクションへのアクセスを待機している間、他のスレッドを離れることに注意してください。locked。それらは中止されたり、リダイレクトされたりすることはありません。彼らはそこに座って、元のスレッドがlock
ブロックの実行を終了して実行できるようになるのを待っています。
mutex
を使用すると、より細かく制御できます。 2番目以降のスレッドをロックするのではなく、完全に停止させたり、プロセス間でスレッドをロックしたりする機能も含まれます。
私はあなたが何をしようとしているのか知っていると思います。コールバックを定期的に実行するタイマー(タイマーの定義)があり、そのコールバックは少しの作業を行います。そのビットの作業は、実際にはタイマー期間よりも時間がかかる可能性があります(たとえば、タイマー期間は500ミリ秒であり、コールバックの特定の呼び出しには500ミリ秒より長くかかる可能性があります)。これは、コールバックを再入可能にする必要があることを意味します。
再入可能になれない場合(そして、これが行われる理由はさまざまです)。私が過去に行ったことは、コールバックの開始時にタイマーをオフにし、最後にタイマーをオンに戻すことです。例えば:
private void timer_Elapsed(object source, ElapsedEventArgs e)
{
timer.Enabled = false;
//... do work
timer.Enabled = true;
}
実際に1つの「スレッド」を次々に実行したい場合は、タイマーを使用することはお勧めしません。 Taskオブジェクトを使用することをお勧めします。例えば
Task.Factory.StartNew(()=>{
// do some work
})
.ContinueWith(t=>{
// do some more work without running at the same time as the previous
});
これらのアプローチのいくつかは素晴らしいと思いますが、少し複雑です。
タイマーがオーバーラップするのを防ぎ、「ELAPSEDをINTERVALごとに1回呼び出す」か、「呼び出し間にINTERVAL遅延を発生させる」かを選択できるラッパークラスを作成しました。
このコードを改善した場合は、ここに更新を投稿してください!
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AllCommander.Diagnostics {
public class SafeTimer : IDisposable {
public enum IntervalStartTime {
ElapsedStart,
ElapsedFinish
}
private System.Timers.Timer InternalTimer;
public bool AutoReset { get; set; }
public bool Enabled {
get {
return InternalTimer.Enabled;
}
set {
if (value) {
Start();
} else {
Stop();
}
}
}
private double __Interval;
public double Interval {
get {
return __Interval;
}
set {
__Interval = value;
InternalTimer.Interval = value;
}
}
/// <summary>
/// Does the internal start ticking at the END of Elapsed or at the Beginning?
/// </summary>
public IntervalStartTime IntervalStartsAt { get; set; }
public event System.Timers.ElapsedEventHandler Elapsed;
public SafeTimer() {
InternalTimer = new System.Timers.Timer();
InternalTimer.AutoReset = false;
InternalTimer.Elapsed += InternalTimer_Elapsed;
AutoReset = true;
Enabled = false;
Interval = 1000;
IntervalStartsAt = IntervalStartTime.ElapsedStart;
}
void InternalTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) {
if (Elapsed != null) {
Elapsed(sender, e);
}
var ElapsedTime = DateTime.Now - e.SignalTime;
if (AutoReset == true) {
//Our default interval will be INTERVAL ms after Elapsed finished.
var NewInterval = Interval;
if (IntervalStartsAt == IntervalStartTime.ElapsedStart) {
//If ElapsedStart is set to TRUE, do some fancy math to determine the new interval.
//If Interval - Elapsed is Positive, then that amount of time is remaining for the interval
//If it is zero or negative, we're behind schedule and should start immediately.
NewInterval = Math.Max(1, Interval - ElapsedTime.TotalMilliseconds);
}
InternalTimer.Interval = NewInterval;
InternalTimer.Start();
}
}
public void Start() {
Start(true);
}
public void Start(bool Immediately) {
var TimerInterval = (Immediately ? 1 : Interval);
InternalTimer.Interval = TimerInterval;
InternalTimer.Start();
}
public void Stop() {
InternalTimer.Stop();
}
#region Dispose Code
//Copied from https://lostechies.com/chrispatterson/2012/11/29/idisposable-done-right/
bool _disposed;
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
~SafeTimer() {
Dispose(false);
}
protected virtual void Dispose(bool disposing) {
if (!_disposed) {
if (disposing) {
InternalTimer.Dispose();
}
// release any unmanaged objects
// set the object references to null
_disposed = true;
}
}
#endregion
}
}