プログラミングにおいて、特定の処理を一定時間停止させてx秒後に実行したい、または一定の間隔で処理を行いたいといった要求は頻繁に発生します。ユーザーインターフェース(UI)の操作中に画面更新のための間隔を空ける、定期的なバックグラウンド処理を行う、外部 API へのリクエスト間隔を調整するなど、さまざまなシーンで「ディレイ(遅延)処理」は重要な役割を担います。
C# ではこれらのディレイ処理を実現するための方法が複数用意されており、その採用方法はアプリケーションの種類や要件に大きく左右されます。この記事では、C#における代表的なディレイ処理の手法として、Thread.Sleep、Task.Delay、Timerクラスの活用方法を、その違いや注意点、実際のコード例を交えて解説します。
同期的なディレイ処理 ~ Thread.Sleep の利用
もっとも基本的な待機処理としてよく知られているのがThread.Sleepメソッドです。このメソッドは、指定した時間だけ現在のスレッドをブロック(=待機状態)します。
簡単なサンプルコードは以下のとおりです。
using System;
using System.Threading;
class Program
{
    static void Main()
    {
        Console.WriteLine("処理開始");
        // 3000ミリ秒(3秒)待機
        Thread.Sleep(3000);
        Console.WriteLine("3秒後に処理再開");
    }
}
上記コードでは、10行目のThread.Sleep(3000);によって、プログラムの実行が3秒間停止します。この方法は理解しやすく、単純なコンソールアプリケーションなど短期間で終了する処理には有効です。
しかし、注意点として、Thread.Sleepはその間スレッド全体を占有してしまうため、ユーザーインターフェース(UI)のあるアプリ(例えば Windows Forms や WPF のアプリケーション)に組み込むとアプリケーションが「固まってしまう」原因となります。つまり、メインスレッドがブロックされるために、ユーザー操作や画面の更新が行えなくなってしまうのです。
また、マルチスレッド環境であっても無駄なリソースの占有を招く恐れがあるため、非同期処理が求められる現代のアプリケーション開発においては、より柔軟なディレイ処理を実現する方法が必要となります。
このため、Thread.Sleepの使用は単純なアプリケーションや個人的なアプリケーション、一時的な確認などにとどめておくようにしましょう。
非同期的なディレイ処理 ~ Task.Delay と async/await
非同期プログラミングが一般的となった現在、待機中にスレッドをブロックしない実装方法として利用されるのが、Task.Delay と async/await の組み合わせです。この方法は、UIスレッドやバックグラウンドスレッドを解放しつつ、一定期間後に処理を再開することができます。
以下はTask.Delayを利用したディレイ処理のサンプルコードです。
using System;
using System.Threading.Tasks;
class Program
{
    static async Task Main()
    {
        Console.WriteLine("処理開始");
        // 非同期に3000ミリ秒待機
        await Task.Delay(3000);
        Console.WriteLine("3秒後に処理再開");
    }
}
このコードでは、10行目のawait Task.Delay(3000);によって、3秒間の待機状態に入りますが、その間も他のタスク(UI操作や別のバックグラウンド処理)がスムーズに行えるようになります。async/await パターンを利用することで、コードの可読性や保守性も向上し、従来のコールバック地獄やイベント駆動型の制御フローから解放されます。
利点と実践上の注意点
- 利点
- スレッドブロックを回避し、サーバー負荷やUIの応答性向上に寄与する
 - 他の非同期処理とシームレスに統合可能
 - コードの非同期部分がシンプルになり、直感的に理解できる
 
 - 注意点
- 非同期メソッド内で例外が発生した場合、適切に捕捉・処理しないとアプリケーション全体が不安定になりかねない
 - コンテキスト(SynchronizationContext)の扱いに注意。特に、コンソールアプリケーションと UI アプリケーションでは、await 後にどのスレッドで再開するかが変わる可能性があるため、特定のスレッド(UIスレッドなど)で処理を再開する必要がある場合には、
ConfigureAwait(false)でメインスレッドに戻らないように制御するなどを検討する必要があります 
 
タイマーを用いたディレイ処理 ~ Timerクラスの利用
一定時間ごとに定期的に処理を実行したい場合、Timerクラスが有効です。C#では、いくつかのTimerクラスが提供されています。それぞれの特徴を理解し、シーンに応じて使い分けることが重要です。
System.Threading.Timer の利用
System.Threading.Timerは、スレッドプール上でコールバックを実行する軽量タイマーです。
using System;
using System.Threading;
class Program
{
    static Timer _timer;
    static void Main()
    {
        // 1000ミリ秒後に開始、以降1000ミリ秒ごとにコールバック実行
        _timer = new Timer(TimerCallback, null, 1000, 1000);
        Console.WriteLine("タイマー開始。Enterキーで終了します...");
        Console.ReadLine();
    }
    static void TimerCallback(object state)
    {
        Console.WriteLine($"タイマー実行: {DateTime.Now}");
    }
}
このタイマーは、指定した初期遅延(1秒)後、1秒ごとにコールバックに指定したTimerCallbackメソッドを呼び出します。UIスレッドとは独立して実行されるため、UIアプリケーションの定期処理などに適していますが、スレッドセーフな操作が必要となることには十分注意してください。
System.Timers.Timer の利用
System.Timers.Timerは、バックグラウンドのスレッド(スレッドプール上)でElapsedイベントを発生させるタイマーです。コンソールアプリケーションなど、UIスレッドに依存しない環境での利用に適しています。
using System;
using System.Timers;
namespace TimersSample
{
    class Program
    {
        // Timerのインスタンス
        private static Timer _timer;
        static void Main(string[] args)
        {
            // Timerを初期化:1000ミリ秒(1秒)ごとにElapsedイベントを発生
            _timer = new Timer(1000);
            _timer.Elapsed += OnTimedEvent;
            _timer.AutoReset = true; // 繰り返し処理を行うためにtrueに設定
            _timer.Enabled = true;   // タイマーを開始
            Console.WriteLine("System.Timers.Timer を開始しました。Enterキーを押すと終了します...");
            Console.ReadLine();
            // タイマーを停止してリソースを解放
            _timer.Stop();
            _timer.Dispose();
        }
        // Elapsedイベントハンドラー
        private static void OnTimedEvent(object sender, ElapsedEventArgs e)
        {
            Console.WriteLine($"Elapsed: {e.SignalTime:HH:mm:ss}");
        }
    }
}
ポイント
AutoResetプロパティAutoResetをtrueに設定すると、指定した間隔でイベントが継続的に発生します。
- バックグラウンドスレッドでの動作
System.Timers.Timerはスレッドプール上で動作するため、UIスレッドのブロックを防ぎます。ただし、UI要素を操作する場合はスレッドセーフな処理が必要です。
 
DispatcherTimer の利用
DispatcherTimerは主に WPF や UWP のアプリケーションで使用され、UIスレッド上で動作します。これにより、UI更新が容易に行えるため、UI制御を伴う定期処理に適しています。
以下は、シンプルな WPF のウィンドウにLabelを配置して、そこで時刻を更新する例です。
XAMLのサンプル
<Window x:Class="DispatcherTimerSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DispatcherTimerサンプル" Height="200" Width="300">
    <Grid>
        <Label x:Name="TimeLabel" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="24"/>
    </Grid>
</Window>
C# (MainWindow.xaml.cs) のサンプルコード
using System;
using System.Windows;
using System.Windows.Threading;
namespace DispatcherTimerSample
{
    public partial class MainWindow : Window
    {
        private DispatcherTimer _dispatcherTimer;
        public MainWindow()
        {
            InitializeComponent();
            // DispatcherTimer の初期化と設定
            _dispatcherTimer = new DispatcherTimer();
            _dispatcherTimer.Interval = TimeSpan.FromSeconds(1); // 1秒ごとにTickイベント発火
            _dispatcherTimer.Tick += DispatcherTimer_Tick;
            _dispatcherTimer.Start(); // タイマー開始
        }
        // Tickイベントハンドラー
        private void DispatcherTimer_Tick(object sender, EventArgs e)
        {
            // UI上のLabelに現在時刻を表示
            TimeLabel.Content = DateTime.Now.ToString("HH:mm:ss");
        }
    }
}
ポイント
- UIスレッド上での動作
DispatcherTimerは WPF のディスパッチャー(UIスレッド)上で動作するため、UI更新に直接アクセスできます。
 IntervalプロパティIntervalに設定した時間ごとにTickイベントが発生するため、定期的なUI更新などに適しています。
ユースケース別ディレイ処理の使い分け
ここで、具体的なユースケースに応じたディレイ処理の選択基準を整理してみましょう。
| ユースケース | 推奨するディレイ処理方法 | 特記事項・注意点 | 
|---|---|---|
| コンソールアプリケーションの簡単な待機処理 | Thread.Sleep | 簡単なものは問題ないが、ブロックによるリソース占有に注意 | 
| UIアプリケーションでの非同期待機 | Task.Delay (async/await) | UIスレッドをブロックしないため、応答性が確保される | 
| 定期的なタイマー処理 | System.Threading.Timer | マルチスレッド環境での実行となるため、スレッドセーフな設計が必要 | 
| WPFなどでUIの更新を伴う定期処理 | DispatcherTimer | UIスレッドで動作するため、UI操作が容易となる | 
このように、アプリケーションの種類や要求される応答性を踏まえ、適切なディレイ処理を選択することが、品質の高いコードを書くための鍵となります。
ディレイ処理を導入する際の注意点
ディレイ処理を導入するとき、プログラム全体の設計にどのような影響が及ぶかを理解しておくことが重要です。特に以下の点に十分留意してください。
- スレッドブロックの回避
同期的な待機処理は、シンプルな場合もありますが、特に UI やサーバーサイドのマルチスレッドアプリケーションでは、スレッドがブロックされることで全体のパフォーマンスに悪影響を与えかねません。非同期処理の積極的な採用を検討しましょう。 - 例外処理の設計
非同期メソッド内で例外が発生した際、適切にキャッチしないとアプリケーションが予期せず終了するリスクがあります。try-catch文を利用したエラーハンドリングや、例外伝播に関する設計が必要です。 - パフォーマンスとリソース管理
適切な待機処理を用いることで、CPU やメモリの無駄な消費を防ぐことができます。特にサーバーアプリケーションにおいては、同時に多くのリクエストをさばくために、非同期処理が重要な役割を果たします。 CancellationTokenによるキャンセル処理
長時間実行する非同期タスクに対しては、ユーザーの操作に応じてキャンセル可能な設計が重要です。Task.Delayも、以下のようにCancellationTokenを渡すことで、待機中にタスクをキャンセルすることが可能です。
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
    static async Task Main()
    {
        using (CancellationTokenSource cts = new CancellationTokenSource())
        {
            // 例:5秒後にキャンセルを実行する
            cts.CancelAfter(5000);
            try
            {
                Console.WriteLine("キャンセル可能な待機開始");
                await Task.Delay(10000, cts.Token);
                Console.WriteLine("待機完了");
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("待機はキャンセルされました");
            }
        }
    }
}
まとめ
本記事では、C# におけるディレイ処理の基本的な方法と、各手法のメリット・デメリット、具体的な実装例について解説しました。
実際の開発においては、これらディレイ処理の概念を踏まえ、アプリケーションの要求に最適な実装方法を選択してください。また、非同期処理やタイマーの使い方は、プログラム全体の設計に大きな影響を及ぼすため、十分にテストを行い、例外処理やキャンセル処理を実装するなど、堅牢な設計を心がけることが重要です。
プログラムの細部にわたる最適化は、コードの可読性と保守性を損なわずに実現するのが理想です。今後も C# の進化とともに、新たな非同期処理の手法やライブラリが登場することが予想されます。常に最新の情報にアンテナを張り、より洗練されたアプローチを取り入れるよう努力することが、堅牢かつ快適なアプリケーション開発への道となるでしょう!

  
  
  
  
