多线程编程是一个很重要的技术点对于初学者来说是比较难掌握的。要让程序高效率、稳健运行多线程编程技术是必需掌握牢固。本篇文章会提供一个使用多线程的完整项目示例自定义计时器。要启用一个新线程既可以使用Thread这个类在System.Threading命名空间下也可以使用Task类在System.Threading.Tasks命名空间下后者是在.NET 4.0推出配合async和await两关键字简化了多线程编程可以直接在同一个方法内实现异步代码并在异步代码后面继续编写同步代码内容这种多线程编写方式是官方推荐的。使用Thread类编写多线程代码的示例如下new Thread(() { }).Start();使用Task类编写多线程的示例如下await Task.Run(() { });多线程编程就涉及到资源访问问题如果处理不好就容易出现各种莫名其妙的错误。要解决资源跨线程访问问题那就会涉及到锁lock的用法在访问之前先锁定各线程共同约定的锁定对象再访问具体资源。示例如下lock (lockObj) { list.Add(item); }注意这边是给lockObj加锁如果其它线程是直接访问list那是错误的其它线程也是需要先给lockObj加锁才能访问list。如果只是简单的一个对象有跨线程访问需求那也可以直接给这个对象加锁而不需要单独定义锁对象示例如下lock (list) { list.Add(item); }加了锁之后其它线程就需要等待前一个线程跳出锁定代码块后才允许加锁。其它线程要访问同样的资源如果存在多个锁定对象时一定要特别注意给各个锁定对象加锁时不要造成死锁。还有一个需要注意的是锁内执行的代码应该是简单高效的而不是长时间锁定如果需要长时间执行一些操作也允许被其它线程访问那就应该定义标识指示被访问的对象是否正在执行中如果是就直接跳过示例如下ClassA current null; lock (list) { foreach (var item in list) { if (item.IsExecuting) continue; current item; current.IsExecuting true; break; } } if (current null) return; try { //这边可以执行一些长时间操作又不会阻止其它线程访问list对象 } finally { lock (list) { current.isExecuting false; } }当然也可以不用定义额外属性也可以实现避免正在执行的任务被其它线程操作示例如下ClassA current null; lock (queueList) { queueList.TryDequeue(out current); } if (current null) return; try { //这边可以执行一些长时间操作又不会阻止其它线程访问queueList对象 } finally { lock (queueList) { queueList.Enqueue(current); } }两种方法都可以实现多线程安全访问对象具体采用何种方法根据实际情况去选择。一定要用try…finally结构在finally块中释放占用的资源这样可以确定try块中内容执行出错也能正确释放占用的资源。前面讲解了多线程的使用方法以及注意事项。掌握了知识点那就应该多练习并掌握牢固。下面我们来看具体完整案例利用多线程技术实现自定义计时器。.NET框架已经实现了多种计时器可以随时取用本示例属于重复造轮子在实际编程中直接利用现有的实现可以实现高效编程和程序的可靠性。还有平常我们都会引入其它开发者提供的各种实现包大大缩短软件开发时间以及提高可维护性。当然第三方包也是有可能有问题的在引用第三方包后是需要进行测试。要实现自定义计时器首先要先定义一个专门的计时器类把相关的代码实现写在这个类里面实现高内聚、低耦合方便提供给其它项目使用。甚至把相关的基础类编译成一个程序集管理好版本统一维护这样可以避免重复修改功能和修复BUG。在本文中自定义计时器命名为CustomTimer主要的属性有Interval、IsRunning、IsStopping、主要的事件有Tick、Stopped主要的方法有Start、Run、Stop。其中Run是内部方法甚至可以使用匿名方法实现。Interval用来标识计时器每次触发Tick事件的间隔。IsRunning用来指示当前计时器是否正在运行中。正在运行中的计时器需要做一些限制比如禁止设置Interval属性。IsStopping用来指示当前计时器是否正在停止中。计时器在触发了Tick事件时可能会执行一些比较长时间的任务而这些任务可能没办法立即停止需要等待执行完毕。通过这个属性可以告诉计时器要准备停止了不能再触发Tick事件。Tick用来定时触发事件通知订阅方可以执行操作。Stopped用来触发停止事件通知订阅方已停止当前计时器。可以安全处理其它操作。Start用来启动计时器在启动时会检查计时器是否已经正在工作如果是就报错避免重复启动导致逻辑混乱。此方法会创建一个新线程用来专门执行Run方法避免阻塞调用线程。这个是计时器最核心部分计时器必需有自己的线程与其它线程并行运行。Run用来循环计时达到Interval时间间隔时触发Tick事件。Stop用来停止计时器此方法只是标记IsStopping属性为true实际停止需要靠Run方法监听IsStopping属性检测到为true时跳出循环结束计时并触发Stopped事件。本篇文章的自定义计时器的多线程实现仍以传统方式Thread类实现完整代码如下在Visual Studio 2022 Current编译通过using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Dayoukema.CustomTimerDemo { /// summary /// 自定义计时器 /// /summary public class CustomTimer { private readonly object _lockObj new(); private System.Threading.Thread? _thread null; private int _interval 1; /// summary /// 时间间隔单位为秒不能小于1也不能大于3600 /// /summary public int Interval { get { return _interval; } set { if (value 1) throw new ArgumentOutOfRangeException(nameof(value), 值不能小于1); if (value 3600) throw new ArgumentOutOfRangeException(nameof(value), 值不能大于3600); lock (_lockObj) { if (IsRunning) throw new InvalidOperationException(计时器运行中); _interval value; } } } /// summary /// 是否运行中 /// /summary public bool IsRunning { get; private set; } /// summary /// 是否停止中 /// /summary public bool IsStopping { get; private set; } /// summary /// 滴答事件 /// /summary public event EventHandlerEventArgs? Tick; /// summary /// 停止事件 /// /summary public event EventHandlerEventArgs? Stopped; /// summary /// 启动可以通过滴答事件接收定时通知 /// /summary /// exception crefInvalidOperationException/exception public void Start() { lock (_lockObj) { if (IsRunning) throw new InvalidOperationException(计时器已启动运行); IsRunning true; } _thread new System.Threading.Thread(Run); _thread.Start(); } /// summary /// 计时线程运行处理 /// /summary private void Run() { while (true) { for (var s 0; s _interval; s) { System.Threading.Thread.Sleep(1000); if (IsStopping) break; } if (IsStopping) break; Tick?.Invoke(this, EventArgs.Empty); } _thread null; lock (_lockObj) { IsRunning false; IsStopping false; } Stopped?.Invoke(this, EventArgs.Empty); } /// summary /// 停止调用此方法并不会立即停止计时线程可以通过停止事件接收通知 /// /summary /// exception crefInvalidOperationException/exception public void Stop() { lock (_lockObj) { if (!IsRunning) throw new InvalidOperationException(计时器未运行); if (IsStopping) throw new InvalidOperationException(计时器已正在停止中); IsStopping true; } } } }实现了自定义计时器那就需要使用。本自定义计时器功能比较简单使用方法也简单自定义计时器本身可以在不同项目类型中使用比如在Windows服务。为方便演示本文提供WPF窗口使用自定义计时器示例在Visual Studio 2022 Current编译通过。WPF窗口设计部分Window x:ClassDayoukema.CustomTimerDemo.MainWindow xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml xmlns:dhttp://schemas.microsoft.com/expression/blend/2008 xmlns:mchttp://schemas.openxmlformats.org/markup-compatibility/2006 xmlns:localclr-namespace:Dayoukema.CustomTimerDemo mc:Ignorabled Title自定义计时器示例 Height350 Width500 WindowStartupLocationCenterScreen LoadedWindow_Loaded ClosingWindow_Closing Grid Margin5 Grid.RowDefinitions RowDefinition/RowDefinition RowDefinition Height5/RowDefinition RowDefinition/RowDefinition /Grid.RowDefinitions TextBlock x:NametbTimer FontSize100 HorizontalAlignmentCenter VerticalAlignmentCenter/TextBlock Grid Grid.Row2 Grid.ColumnDefinitions ColumnDefinition/ColumnDefinition ColumnDefinition Width5/ColumnDefinition ColumnDefinition/ColumnDefinition /Grid.ColumnDefinitions Button x:NamebtnStart ClickbtnStart_Click启动/Button Button x:NamebtnStop Grid.Column2 IsEnabledFalse ClickbtnStop_Click停止/Button /Grid /Grid /WindowWPF代码逻辑部分using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace Dayoukema.CustomTimerDemo { /// summary /// Interaction logic for MainWindow.xaml /// /summary public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private string _title string.Empty; private CustomTimer _timer new(); private TimeSpan _ts TimeSpan.Zero; private void Window_Loaded(object sender, RoutedEventArgs e) { _title Title; _timer new CustomTimer(); _timer.Tick _timer_Tick; _timer.Stopped _timer_Stopped; Render(); } private void Render() { tbTimer.Text _ts.ToString(hh\\:mm\\:ss); } private void _timer_Tick(object? sender, EventArgs e) { _ts _ts.Add(TimeSpan.FromSeconds(1)); Dispatcher.Invoke(delegate { Render(); }); } private void _timer_Stopped(object? sender, EventArgs e) { Dispatcher.Invoke(delegate { Title ${_title}已停止; btnStart.IsEnabled true; }); } private void btnStart_Click(object sender, RoutedEventArgs e) { Title ${_title}运行中; _ts TimeSpan.Zero; Render(); _timer.Start(); btnStart.IsEnabled false; btnStop.IsEnabled true; } private void btnStop_Click(object sender, RoutedEventArgs e) { _timer.Stop(); btnStop.IsEnabled false; } private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) { if (_timer.IsRunning) { e.Cancel true; MessageBox.Show(this, 计时器正在运行中请先停止后再关闭窗口, 关闭窗口提示, MessageBoxButton.OK, MessageBoxImage.Error); } } } }以上就是本篇文章的全部内容如有错误欢迎指正。
【C#】多线程编程之自定义计时器
发布时间:2026/7/12 3:02:55
多线程编程是一个很重要的技术点对于初学者来说是比较难掌握的。要让程序高效率、稳健运行多线程编程技术是必需掌握牢固。本篇文章会提供一个使用多线程的完整项目示例自定义计时器。要启用一个新线程既可以使用Thread这个类在System.Threading命名空间下也可以使用Task类在System.Threading.Tasks命名空间下后者是在.NET 4.0推出配合async和await两关键字简化了多线程编程可以直接在同一个方法内实现异步代码并在异步代码后面继续编写同步代码内容这种多线程编写方式是官方推荐的。使用Thread类编写多线程代码的示例如下new Thread(() { }).Start();使用Task类编写多线程的示例如下await Task.Run(() { });多线程编程就涉及到资源访问问题如果处理不好就容易出现各种莫名其妙的错误。要解决资源跨线程访问问题那就会涉及到锁lock的用法在访问之前先锁定各线程共同约定的锁定对象再访问具体资源。示例如下lock (lockObj) { list.Add(item); }注意这边是给lockObj加锁如果其它线程是直接访问list那是错误的其它线程也是需要先给lockObj加锁才能访问list。如果只是简单的一个对象有跨线程访问需求那也可以直接给这个对象加锁而不需要单独定义锁对象示例如下lock (list) { list.Add(item); }加了锁之后其它线程就需要等待前一个线程跳出锁定代码块后才允许加锁。其它线程要访问同样的资源如果存在多个锁定对象时一定要特别注意给各个锁定对象加锁时不要造成死锁。还有一个需要注意的是锁内执行的代码应该是简单高效的而不是长时间锁定如果需要长时间执行一些操作也允许被其它线程访问那就应该定义标识指示被访问的对象是否正在执行中如果是就直接跳过示例如下ClassA current null; lock (list) { foreach (var item in list) { if (item.IsExecuting) continue; current item; current.IsExecuting true; break; } } if (current null) return; try { //这边可以执行一些长时间操作又不会阻止其它线程访问list对象 } finally { lock (list) { current.isExecuting false; } }当然也可以不用定义额外属性也可以实现避免正在执行的任务被其它线程操作示例如下ClassA current null; lock (queueList) { queueList.TryDequeue(out current); } if (current null) return; try { //这边可以执行一些长时间操作又不会阻止其它线程访问queueList对象 } finally { lock (queueList) { queueList.Enqueue(current); } }两种方法都可以实现多线程安全访问对象具体采用何种方法根据实际情况去选择。一定要用try…finally结构在finally块中释放占用的资源这样可以确定try块中内容执行出错也能正确释放占用的资源。前面讲解了多线程的使用方法以及注意事项。掌握了知识点那就应该多练习并掌握牢固。下面我们来看具体完整案例利用多线程技术实现自定义计时器。.NET框架已经实现了多种计时器可以随时取用本示例属于重复造轮子在实际编程中直接利用现有的实现可以实现高效编程和程序的可靠性。还有平常我们都会引入其它开发者提供的各种实现包大大缩短软件开发时间以及提高可维护性。当然第三方包也是有可能有问题的在引用第三方包后是需要进行测试。要实现自定义计时器首先要先定义一个专门的计时器类把相关的代码实现写在这个类里面实现高内聚、低耦合方便提供给其它项目使用。甚至把相关的基础类编译成一个程序集管理好版本统一维护这样可以避免重复修改功能和修复BUG。在本文中自定义计时器命名为CustomTimer主要的属性有Interval、IsRunning、IsStopping、主要的事件有Tick、Stopped主要的方法有Start、Run、Stop。其中Run是内部方法甚至可以使用匿名方法实现。Interval用来标识计时器每次触发Tick事件的间隔。IsRunning用来指示当前计时器是否正在运行中。正在运行中的计时器需要做一些限制比如禁止设置Interval属性。IsStopping用来指示当前计时器是否正在停止中。计时器在触发了Tick事件时可能会执行一些比较长时间的任务而这些任务可能没办法立即停止需要等待执行完毕。通过这个属性可以告诉计时器要准备停止了不能再触发Tick事件。Tick用来定时触发事件通知订阅方可以执行操作。Stopped用来触发停止事件通知订阅方已停止当前计时器。可以安全处理其它操作。Start用来启动计时器在启动时会检查计时器是否已经正在工作如果是就报错避免重复启动导致逻辑混乱。此方法会创建一个新线程用来专门执行Run方法避免阻塞调用线程。这个是计时器最核心部分计时器必需有自己的线程与其它线程并行运行。Run用来循环计时达到Interval时间间隔时触发Tick事件。Stop用来停止计时器此方法只是标记IsStopping属性为true实际停止需要靠Run方法监听IsStopping属性检测到为true时跳出循环结束计时并触发Stopped事件。本篇文章的自定义计时器的多线程实现仍以传统方式Thread类实现完整代码如下在Visual Studio 2022 Current编译通过using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Dayoukema.CustomTimerDemo { /// summary /// 自定义计时器 /// /summary public class CustomTimer { private readonly object _lockObj new(); private System.Threading.Thread? _thread null; private int _interval 1; /// summary /// 时间间隔单位为秒不能小于1也不能大于3600 /// /summary public int Interval { get { return _interval; } set { if (value 1) throw new ArgumentOutOfRangeException(nameof(value), 值不能小于1); if (value 3600) throw new ArgumentOutOfRangeException(nameof(value), 值不能大于3600); lock (_lockObj) { if (IsRunning) throw new InvalidOperationException(计时器运行中); _interval value; } } } /// summary /// 是否运行中 /// /summary public bool IsRunning { get; private set; } /// summary /// 是否停止中 /// /summary public bool IsStopping { get; private set; } /// summary /// 滴答事件 /// /summary public event EventHandlerEventArgs? Tick; /// summary /// 停止事件 /// /summary public event EventHandlerEventArgs? Stopped; /// summary /// 启动可以通过滴答事件接收定时通知 /// /summary /// exception crefInvalidOperationException/exception public void Start() { lock (_lockObj) { if (IsRunning) throw new InvalidOperationException(计时器已启动运行); IsRunning true; } _thread new System.Threading.Thread(Run); _thread.Start(); } /// summary /// 计时线程运行处理 /// /summary private void Run() { while (true) { for (var s 0; s _interval; s) { System.Threading.Thread.Sleep(1000); if (IsStopping) break; } if (IsStopping) break; Tick?.Invoke(this, EventArgs.Empty); } _thread null; lock (_lockObj) { IsRunning false; IsStopping false; } Stopped?.Invoke(this, EventArgs.Empty); } /// summary /// 停止调用此方法并不会立即停止计时线程可以通过停止事件接收通知 /// /summary /// exception crefInvalidOperationException/exception public void Stop() { lock (_lockObj) { if (!IsRunning) throw new InvalidOperationException(计时器未运行); if (IsStopping) throw new InvalidOperationException(计时器已正在停止中); IsStopping true; } } } }实现了自定义计时器那就需要使用。本自定义计时器功能比较简单使用方法也简单自定义计时器本身可以在不同项目类型中使用比如在Windows服务。为方便演示本文提供WPF窗口使用自定义计时器示例在Visual Studio 2022 Current编译通过。WPF窗口设计部分Window x:ClassDayoukema.CustomTimerDemo.MainWindow xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml xmlns:dhttp://schemas.microsoft.com/expression/blend/2008 xmlns:mchttp://schemas.openxmlformats.org/markup-compatibility/2006 xmlns:localclr-namespace:Dayoukema.CustomTimerDemo mc:Ignorabled Title自定义计时器示例 Height350 Width500 WindowStartupLocationCenterScreen LoadedWindow_Loaded ClosingWindow_Closing Grid Margin5 Grid.RowDefinitions RowDefinition/RowDefinition RowDefinition Height5/RowDefinition RowDefinition/RowDefinition /Grid.RowDefinitions TextBlock x:NametbTimer FontSize100 HorizontalAlignmentCenter VerticalAlignmentCenter/TextBlock Grid Grid.Row2 Grid.ColumnDefinitions ColumnDefinition/ColumnDefinition ColumnDefinition Width5/ColumnDefinition ColumnDefinition/ColumnDefinition /Grid.ColumnDefinitions Button x:NamebtnStart ClickbtnStart_Click启动/Button Button x:NamebtnStop Grid.Column2 IsEnabledFalse ClickbtnStop_Click停止/Button /Grid /Grid /WindowWPF代码逻辑部分using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace Dayoukema.CustomTimerDemo { /// summary /// Interaction logic for MainWindow.xaml /// /summary public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private string _title string.Empty; private CustomTimer _timer new(); private TimeSpan _ts TimeSpan.Zero; private void Window_Loaded(object sender, RoutedEventArgs e) { _title Title; _timer new CustomTimer(); _timer.Tick _timer_Tick; _timer.Stopped _timer_Stopped; Render(); } private void Render() { tbTimer.Text _ts.ToString(hh\\:mm\\:ss); } private void _timer_Tick(object? sender, EventArgs e) { _ts _ts.Add(TimeSpan.FromSeconds(1)); Dispatcher.Invoke(delegate { Render(); }); } private void _timer_Stopped(object? sender, EventArgs e) { Dispatcher.Invoke(delegate { Title ${_title}已停止; btnStart.IsEnabled true; }); } private void btnStart_Click(object sender, RoutedEventArgs e) { Title ${_title}运行中; _ts TimeSpan.Zero; Render(); _timer.Start(); btnStart.IsEnabled false; btnStop.IsEnabled true; } private void btnStop_Click(object sender, RoutedEventArgs e) { _timer.Stop(); btnStop.IsEnabled false; } private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) { if (_timer.IsRunning) { e.Cancel true; MessageBox.Show(this, 计时器正在运行中请先停止后再关闭窗口, 关闭窗口提示, MessageBoxButton.OK, MessageBoxImage.Error); } } } }以上就是本篇文章的全部内容如有错误欢迎指正。