说到线程,很多人会想到timer吧,

接下来我们就来学习一下 timer 吧,摇摇脑袋,清醒一下,接下来开始学习....

一、基本概念

1、什么是进程? 
       当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。一个进程至少有一个主线程。

2、什么是线程? 
       线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数

3、什么是多线程? 
       多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

4、多线程的好处? 
       可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。

5、多线程的不利方面? 
       线程也是程序,所以线程需要占用内存,线程越多占用内存也越多。 
       多线程需要协调和管理,所以 CPU 需要花时间来跟踪线程。 
       线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。 
       线程太多会导致控制太复杂,最终可能造成很多 Bug。

static void Main(string[] args)
{
Thread.CurrentThread.Name = "It's Main Thread";
Console.WriteLine(Thread.CurrentThread.Name + " [Status:" + Thread.CurrentThread.ThreadState + "]");
}

通过 Thread 类的静态属性 CurrentThread 可以获取当前正在执行的线程。不管创建了多少个这个类的实例,但是类的静态属性在内存中只有一个。很容易理解 CurrentThread 为什么是静态的--虽然有多个线程同时存在,但是在某一个时刻,CPU 只能执行其中一个! 
       在.net framework class library 中,所有与多线程机制应用相关的类都是放在 System.Threading 命名空间中。

操纵一个线程

static void Main(string[] args)
{
Console.WriteLine("Thread Start/Stop/Join Sample:"); // 创建一个线程,使之执行 Beta 方法
Thread oThread = new Thread(Beta); // 实际上,Start 方法只是通知 CPU 此线程可以被执行,但具体执行时机则由 CPU 自行决定。
oThread.Start();
while (!oThread.IsAlive)
{
Thread.Sleep();
} oThread.Abort();
oThread.Join();
Console.WriteLine();
Console.WriteLine("Beta has finished");
try
{
Console.WriteLine("Try to restart the Alpha.Beta thread");
oThread.Start();
}
catch (ThreadStateException)
{
Console.WriteLine("ThreadStateException trying to restart Alpha.Beta. ");
Console.WriteLine("Expected since aborted threads cannot be restarted.");
Console.ReadLine();
}
} public static void Beta()
{
while (true)
{
Console.WriteLine("Beta is running in its own thread.");
}
}

试图用 Thread.Start() 方法重新启动线程 oThread,但显然 Abort() 方法带来的后果是不可恢复的终止线程,所以最后程序会抛出 ThreadStateException 异常。

二、线程的优先级

1、当线程之间争夺 CPU 时,CPU 按照线程的优先级给予服务。

2、在 C# 应用程序中,用户可以设定 5 个不同的优先级,由高到低分别是 HighestAboveNormalNormalBelowNormalLowest,在创建线程时如果不指定优先级,那么系统默认为 ThreadPriority.Normal。 
       通过设定线程的优先级,我们可以安排一些相对重要的线程优先执行,例如对用户的响应等等。

static void Main(string[] args)
{
Thread t1 = new Thread(() =>
{
System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
watch.Start();
for (int i = ; i < ; i++)
{
// 模拟耗时工作
var obj = new { name = "XXX", age = };
GC.Collect();
}
watch.Stop();
Console.WriteLine("t1 finished[ {0} ]", watch.ElapsedMilliseconds);
}); Thread t2 = new Thread(() =>
{
System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
watch.Start();
for (int i = ; i < ; i++)
{
var obj = new { name = "XXX", age = };
GC.Collect();
}
watch.Stop();
Console.WriteLine("t2 finished[ {0} ]", watch.ElapsedMilliseconds);
});
t1.Priority = ThreadPriority.AboveNormal;
t2.Priority = ThreadPriority.BelowNormal;
t1.Start();
t2.Start();
}

线程的调度算法非常复杂。记住,优先级高的线程并不一定先执行,但 CPU 会将更多的时间片分给优先级高的线程,因此,在相同任务量的前提下,高优先级线程将会较快的完成任务。

三、Winform 中多线程的应用

1、在 Winform 程序中,一般负责控制UI界面的显示、更新和控件交互的线程为主线程,或 UI 线程。

2、单线程最显著的缺点是,当一个事件发生,程序进行一个耗时的运算动作时,UI 线程会出现假死现象,此时会无视对用户的响应。

下面的代码会模拟一些不同的情况:

void DoSomething()
{
for (int i = ; i < ; i++)
{ }
MessageBox.Show("It's finished.");
} void ShowStr(object obj)
{
var list = obj as List<string>;
if (list != null)
{
foreach (var item in list)
{
MessageBox.Show(item.ToString());
}
}
else
MessageBox.Show("null");
} // UI 单线程,运行时窗体会卡死一段时间
private void btnUI_Click(object sender, EventArgs e)
{
DoSomething();
} // 调用无參函数,此时窗体能响应用户
private void btnThreadA_Click(object sender, EventArgs e)
{
Thread thread = new Thread(DoSomething);
thread.Start();
} // 当所有前台线程都关闭时,后台线程将立即结束运行,无条件的关闭
// 而前台线程运行时,即使关闭 Form 主程序,该线程仍将继续运行,直到计算完毕
private void btnThreadB_Click(object sender, EventArgs e)
{
Thread thread = new Thread(DoSomething);
thread.IsBackground = true;
thread.Start();
} // 调用有参函数
private void btnThreadC_Click(object sender, EventArgs e)
{
Thread thread = new Thread(ShowStr);
thread.Start(new List<string> { "Jacky", "Skysoot", "Sam" });
}

要注意的是,线程在调用有参函数时,通过 Start() 方法传递了参数给指定委托,该委托又将参数传递给了该线程欲运行的函数。看微软 Thread 类定义的元数据:

Thread 类的 4 个构造函数基本分为 2 类,有参和无參。而 ParameterizedThreadStart 委托定义的方法原型的参数为 Object 类型,这提高了传参最大的灵活性。当然,在被调用的函数内部,需要依据一定的约定将 Object 对象进行转型处理。

封装一个线程类进行函数和参数的传递

// 可定义各类型委托 示例暂定一个
public delegate void Do(object obj); public class Worker
{
Do method;
object obj; private void Work()
{
method(obj);
} // 创建工人线程时 new 出工人实例 并在线程上指定 Work()
public static Thread CreateWorkerThread(Do method, object obj)
{
Worker worker = new Worker();
worker.method = method;
worker.obj = obj;
Thread t = new Thread(worker.Work);
return t;
}
} // 任务类
public class Quest
{
public static void Quest1(object obj)
{
Console.WriteLine("工人开始:" + obj.ToString() + "\r\n");
} public static void Quest2(object obj)
{
string[] list = obj as string[];
if (obj != null)
{
foreach (var item in list)
{
Console.WriteLine("工人开始:" + item);
}
}
}
} public class Test
{
public static void Main(string[] args)
{
Thread t1 = Worker.CreateWorkerThread(Quest.Quest1, "搬砖");
t1.Start();
Thread t2 = Worker.CreateWorkerThread(Quest.Quest2, new string[] {"听音乐", "加班", "打台球" });
t2.Start();
}
}

这种封装只是一种启发的方式而已,并非模式。但封装委托后的好处在于,调用方可以灵活指定 Worker 类执行什么类型的任务,加工什么参数,而无需再去考虑其余事情。

四、C#中timer类的用法

1、System.Windows.Forms.Timer  

实现按用户定义的时间间隔引发事件的计时器。此计时器最宜用于 Windows 窗体应用程序中,并且必须在窗口中使用。

2、System.Threading.Timer 

提供以指定的时间间隔执行方法的机制。无法继承此类。

3、System.Timers.Timer  

在应用程序中生成定期事件。

这三个定时器位于不同的命名空间内,上面大概介绍了3个定时器的用途,其中第一个是只能在Windows窗体中使用的控件。

在.NET1.1里面,第3个System.Timers.Timer,也是可以拖拽使用,而.NET2.0开始取消了,只能手动编写代码。而后2个没有限制制。

下面通过具体的列子来看3个Timer的使用和区别

一 、System.Windows.Forms.Timer

 #region System.Windows.Forms.Timer
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
int num = ;
private void Form_Timer_Tick(object sender, EventArgs e)
{
label1.Text = (++num).ToString();
Thread.Sleep();
}
private void button1_Click(object sender, EventArgs e)
{
Form_Timer.Start();
}
private void button2_Click(object sender, EventArgs e)
{
Form_Timer.Stop();
}
}
#endregion

上面这个是一个很简单的功能,在Form窗体上拖了一个System.Windows.Forms.Timer控件名字为Form_Timer,在属性窗中把Enable属性设置为Ture,Interval是定时器的间隔时间。双击这个控件就可以看到 Form_Timer_Tick方法。

在这个方法中,我们让她不停的加一个数字并显示在窗体上,2个按钮提供了对计时器的控制功能。执行的时候你去点击其他窗体在回来,你会发现我们的窗体失去响应了。

因为我们这里使用Thread.Sleep(3000); 让当前线程挂起,而UI失去相应,说明了这里执行时候采用的是单线程。也就是执行定时器的线程就是UI线程。Timer 用于以用户定义的事件间隔触发事件。

Windows 计时器是为单线程环境设计的,其中,UI 线程用于执行处理。它要求用户代码有一个可用的 UI 消息泵,而且总是在同一个线程中操作,或者将调用封送到另一个线程。

在Timer内部定义的了一个Tick事件,我们前面双击这个控件时实际是增加了一行代码。

this.Form_Timer.Tick += new System.EventHandler(this.Form_Timer_Tick);  

然后Windows将这个定时器与调用线程关联(UI线程)。当定时器触发时,Windows把一个定时器消息插入到线程消息队列中。调用线程执行一个消息泵提取消息,然后发送到回调方法中(这里的Form_Timer_Tick方法)。

而这些都是单线程进行了,所以在执行回调方法时UI会假死。所以使用这个控件不宜执行计算受限或IO受限的代码,因为这样容易导致界面假死,而应该使用多线程调用的Timer。

另外要注意的是这个控件时间精度不高,精度限定为 55 毫秒。

二、System.Timers.Timer

接下来就看下另一个Timer,我们用它来改写上面的程序

#region System.Windows.Forms.Timer
public partial class Form1 : Form
{
public Form1() {
InitializeComponent();
}
int num = ;
DateTime time1 = new DateTime();
DateTime time2 = new DateTime();
//定义Timer
System.Timers.Timer Timers_Timer = new System.Timers.Timer();
private void button1_Click(object sender, EventArgs e)
{
//手动设置Timer,开始执行
Timers_Timer.Interval = ;
Timers_Timer.Enabled = true;
Timers_Timer.Elapsed += new System.Timers.ElapsedEventHandler(Timers_Timer_Elapsed);
time1 = DateTime.Now;
}
void Timers_Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
label1.Text = Convert.ToString((++num)); //显示到lable
Thread.Sleep();
}
private void button2_Click(object sender, EventArgs e)
{
//停止执行
Timers_Timer.Enabled = false;
time2 = DateTime.Now;
MessageBox.Show(Convert.ToString(time2 - time1));
}
}
#endregion

我们可以看到这个代码和前面使用Form.Timer的基本相同,不同的是我们是手动定义的对象,而不是通过拉控件。他也有Interval ,Enabled 等属性,作用和第一是一样的。

不同的是他的事件名为Elapsed ,但是和上面的Tick一样,绑定一个委托的方法。只是这里我们是手动完成的。另外不同之处是Form.Timer我们可以用Stop和Start方法控制,而这里是通过Enable属性控制。

但实际上也可以用Stop和Start方法,内部也是通过他自己的Enable来控制的。

  最大的不同就是上面的代码在调试时会报错,提示你"线程间操作无效: 从不是创建控件“label1”的线程访问它。"但如果你不调试直接运行是OK的,而且运行时你去拖动窗体会发现没有出现假死。

从这里我们就可以知道这里的Timer的创建线程和执行线程不是同一个线程。也就是使用了多线程。Timer的创建线程是UI线程,而执行线程是TheardPool中的线程所以不会假死,但调试的时候会报错,因为非控件的创建线程不能操作控件

但你可以直接运行,这里是VS05做了手脚。解决办法很多,用delegate.BeginInvoke()等等。这里介绍特有的一种方法,设置Timer的SynchronizingObject属性,Timers_Timer.SynchronizingObject = label1;

这样调试运行时就不会报错了,但是设置了这个属性Timer就编程单线程调用了,就基本和第一个完全一样了。

 /// 在timer中使用SynchronizingObject的原因(tangtao_xp的注解)
        /// 请参考https://msdn.microsoft.com/en-us//library/system.timers.timer.synchronizingobject
        /// 1.如果SynchronizingObject为null,timer的Elapsed事件会默认由线程池进行处理
        /// 2.如果timer的Elapsed事件要在windows Form等UI元件中处理,就会出现UI线程访问线程池情况
        /// 此时会引发错误和异常;将SynchronizingObject设置成要处理timer的Elapsed事件的UI元件,
        /// 该timer就会由该UI元件的线程创建,从而避免异常。

三 、System.Threading.Timer

继续用这个对象改造程序。

#region System.Windows.Forms.Timer
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
int num = ;
DateTime time1 = new DateTime();
DateTime time2 = new DateTime();
System.Threading.Timer Thread_Time;
private void button1_Click(object sender, EventArgs e)
{
//启动
Thread_Time = new System.Threading.Timer(Thread_Timer_Method, null, , );
time1 = DateTime.Now;
}
void Thread_Timer_Method(object o)
{
label1.Text = Convert.ToString((++num));
System.Threading.Thread.Sleep();
}
private void button2_Click(object sender, EventArgs e)
{
//停止
Thread_Time.Dispose();
time2 = DateTime.Now;
MessageBox.Show(Convert.ToString(time2 - time1));
}
}
#endregion

用Threading.Timer时的方法,和前面就不太相同了,所以的参数全部在构造函数中进行了设置,而且可以设置启动时间。而且没有提供start和stop方法来控制计时器。

而且是以一种回调方法的方式实现,而不是通过事件来实现的。他们之间还是有区别的。我们只有销毁掉对象来停止他。

当你运行时,你会发现他和前面的Timers.Timer一样,是多线程的,主要表现在不会假死,调试运行报错。但跟让你奇怪的是,我们的代码竟然无法让她停止下来。

调用了Dispose方法没有用。问题在那?然后有进行了测试,修改了间隔时间为100,200,500,1000,3000,4000。

这几种情况。发现当间隔为500ms以上是基本马上就停止了。而间隔时间相对执行时间越短,继续执行的时间越长。这应该是在间隔时间小于执行时间时多个线程运行造成的。因为所有的线程不是同时停止的。间隔越短,线程越多,所以执行次数越多。

最后来看下这个对象另外一个特殊的地方。

 static void Main()
{
Timer t = new Timer(Test, null, , );
Console.ReadLine();
}
public static void Test(object o)
{
Console.WriteLine("hello world");
GC.Collect();
}

这段代码会输出什么结果呢?默认情况他只输出一次,就停止了。为什么呢?根据上面说的,当定义对象t,执行代码后,进行了强制垃圾回收,因为t在Main中没有其他引用,所以被回收掉了。

但是如果我们把编译器的”优化“项取消掉,在看看情况。程序进然一直在输出。为什么执行垃圾回收却没有被回收呢?因为这个禁用优化选项,t的声明周期被扩展到了方法结束。所以一直执行。

因为编译器默认是优化的,所以我们必须保证Timer对象一直被引用,而避免被垃圾回收。所以我们可以在编译器打开优化的情况下,在Main函数最后加上t=null保证回收前被引用,但你发现,这样是没用的。

因为JIT编译器优化后会吧t=null直接删除,所以我们用t.Dispose(),就可以达到目的。在我们进行垃圾回收时,CLR发现t还有被引用,还没执行Dispose所以不会被回收。

是以Threading.Timer有时候会出现运行一次就停止或者是销毁了还在运行的情况,而且和编译器优化也有关,所以使用时要注意。

最后看下MSDN的描述: 只要在使用 Timer,就必须保留对它的引用。对于任何托管对象,如果没有对 Timer 的引用,计时器会被垃圾回收。即使 Timer 仍处在活动状态,也会被回收。当不再需要计时器时,请使用 Dispose 方法释放计时器持有的资源。

如果希望在计时器被释放时接收到信号,请使用接受 WaitHandle 的 Dispose(WaitHandle) 方法重载。计时器已被释放后,WaitHandle 便终止。

总结:

  •   System.Threading.Timer 是一个简单的轻量计时器,它使用回调方法并由线程池线程提供服务。不建议将其用于 Windows 窗体,因为其回调不在用户界面线程上进行。System.Windows.Forms.Timer 是用于 Windows 窗体的更佳选择。
  • 要获取基于服务器的计时器功能,可以考虑使用 System.Timers.Timer,它可以引发事件并具有其他功能。
  •   在《CLR Via C#》中讲多线程时有提到这3个计时器,但作者说System.Timers.Timer是对System.Threading.Timer的报装,不推荐使用,但是在我的WEB项目中的Application_Start中我还是使用的这个而不是Threading.Timer,因为使用Threading.Timer时只执行了一次就不在执行了。
  • 对于计时器在B/S结构中的使用就复杂一些,一般我们把计时器放在Application_OnStart中,这样全局维护一个计时器,可以进行定期备份数据库,定期维护用户等操作,而且方法写作静态的,以免被垃圾回收
  • 而不建议在一般的aspx页面中使用,因为服务器端的定时器对用户这样意义不大,完全可以使用JS代替。而且这个页面的每个请求都可能引入一个新的定时器,导致系统崩溃。
  • 另外,定时器是ASP.NET进程,IIS有关,所以对用重要的执行任务,还是建议写成服务或独立程序放在服务器上执行好了。
  •   System.Windows.Forms.Timer是应用于WinForm中的,它是通过Windows消息机制实现的,类似于VB或Delphi中的Timer控件,内部使用API  SetTimer实现的。它的主要缺点是计时不精确,而且必须有消息循环,Console  Application(控制台应用程序)无法使用。
  •   System.Timers.Timer和System.Threading.Timer非常类似,它们是通过.NET  Thread  Pool实现的,轻量,计时精确,对应用程序、消息没有特别的要求。System.Timers.Timer还可以应用于WinForm,完全取代上面的Timer控件。它们的缺点是不支持直接的拖放,需要手工编码。

ok,今天的学习就到此结束了,有什么不对的欢迎指出~

参考博客:

https://www.cnblogs.com/SkySoot/p/3494154.html

https://www.cnblogs.com/OpenCoder/archive/2010/02/23/1672043.html

谈谈C#中各种线程的使用及注意项~的更多相关文章

  1. 谈谈java中的线程(初级概念)

    定义 关于进程与线程的定义 可参看一下这个介绍 http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html 在不细抠定义的情况下 ...

  2. 谈谈Golang中goroutine的调度问题

    goroutine的调度问题,同样也是我之前面试的问题,不过这个问题我当时并不是很清楚,回来以后立马查阅资料,现整理出来备忘. 有一些预备知识需要说明,就是操作系统中的线程.操作系统中的线程分为两种: ...

  3. 谈谈JAVA中的安全发布

    谈谈JAVA中的安全发布 昨天看到一篇文章阐述技术类资料的"等级",看完之后很有共鸣.再加上最近在工作中越发觉得线程安全性的重要性和难以捉摸,又掏出了<Java并发编程实战& ...

  4. android中的线程池学习笔记

    阅读书籍: Android开发艺术探索 Android开发进阶从小工到专家 对线程池原理的简单理解: 创建多个线程并且进行管理,提交的任务会被线程池指派给其中的线程进行执行,通过线程池的统一调度和管理 ...

  5. rxjava源码中的线程知识

    rxjava源码中的线程知识 rx的最精简的总结就是:异步 这里说一下以下的五个类 1.Future2.ConcurrentLinkedQueue3.volatile关键字4.AtomicRefere ...

  6. C#中的线程(一)入门

    文章系参考转载,英文原文网址请参考:http://www.albahari.com/threading/ 作者 Joseph Albahari,  翻译 Swanky Wu 中文翻译作者把原文放在了& ...

  7. C#中的线程四(System.Threading.Thread)

    C#中的线程四(System.Threading.Thread) 1.最简单的多线程调用 System.Threading.Thread类构造方法接受一个ThreadStart委托,改委托不带参数,无 ...

  8. C#中的线程三 (结合ProgressBar学习Control.BeginInvoke)

    C#中的线程三(结合ProgressBar学习Control.BeginInvoke) 本篇继上篇转载的关于Control.BeginInvoke的论述之后,再结合一个实例来说明Cotrol.Begi ...

  9. C#中的线程二(Cotrol.BeginInvoke和Control.Invoke)

    C#中的线程二(Cotrol.BeginInvoke和Control.Invoke) 原文地址:http://www.cnblogs.com/whssunboy/archive/2007/06/07/ ...

随机推荐

  1. zookeeper笔记(一)

    title: zookeeper笔记(一) zookeeper 安装简记 解压文件 $ tar -zxvf zookeeper-3.4.10.tar.gz -C 安装目录 创建软连接(进入安装目录) ...

  2. Spring Boot JPA中java 8 的应用

    文章目录 Optional Stream API CompletableFuture Spring Boot JPA中java 8 的应用 上篇文章中我们讲到了如何在Spring Boot中使用JPA ...

  3. dockerfile简介及书写规则

                                       Dockerfile 简介 Dockfile是一种被Docker程序解释的脚本, Dockerfile由一条一条的指令组成,每条指 ...

  4. ps 和 top

    ps 进程和线程的关系: (1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程. (2)资源分配给进程,同一进程的所有线程共享该进程的所有资源. (3)处理机分给线程,即真正在处 ...

  5. xenomai内核解析之嵌入式实时linux概述

    Linux系统作为一个GPOS(通用操作系统)发展至今已经非常成熟可靠了,并且由于遵循GPL协议,开放所有系统源代码,非常易于裁剪.更重要的是,与其他开源的GPOS或RTOS相比,Linux系统支持多 ...

  6. Shell脚本(三)重定向

    先上一张图: 代码如下: #!/bin/bash echo "hello world" echo `ls +` 运行结果如下: PS: 1. 如果想同时将数据重定向到文件和stdo ...

  7. 利用github的webhook进行自动部署

    利用github的webhook进行自动部署 github提供了webhook功能,大概意思就是,当你提交了代码,git检测到你进行了push,可以调起你一个你知道的url. 这个功能有什么用了?比如 ...

  8. Flutter 1.17版本重磅发布

    Flutter 1.17 是2020年的第一个稳定版本,此版本包括iOS平台Metal支持(性能更快),新的Material组件,新的Network跟踪工具等等! 对所有人来说,今年是充满挑战的一年. ...

  9. 时间日期相关类:Date类,DateFormat类&SimpleDateFormat类,Calendar类

    日期相关类 1.Date类:日期时间类 A.构造方法 Date(): 根据当前系统时间创建日期对象 ​ Date(Long time):根据传入的毫秒值时间创建日期对象 B.成员方法 Long get ...

  10. LeetCode--LinkedList--141.Linked List Cycle(Easy)

    141. Linked List Cycle(Easy)2019.7.10 题目地址https://leetcode.com/problems/linked-list-cycle/ Given a l ...