Smart Client学习笔记(7) 使用多线程创建高响应智能客户端应用程序
来源:互联网 发布:网络隐私权的内涵包括 编辑:程序博客网 时间:2024/06/05 17:39
这次课程讲解了如何进行多线程编程
多线程应用程序的特点:
• 具有同时有多条“执行线路”的能力
• 行为无法预知并且每次调试时行为不同
• 线程增加了程序的复杂性
• 多线程不但可以用于我们所关注的SmartClient当中,也可以用于任何应用程序当中
为什么要讨论多线程应用程序?
• 向用户提供及时响应
• 在运行时层面并行执行任务
• 获得更好的全面的应用程序性能
• 硬件资源得到全面利用
在winform开发中,如果不是用多线程进行开发,那么在算法计算时间比较长的情况下,会阻塞主线程,导致界面的停滞。
进程的概念:
• 进程是正在执行的程序的一个实例
• 进程只有在执行时才存在
• 进程拥有4GB地址空间以包含应用程序的代码和数据(windows 平台下)
• 每个进程至少有一个线程
线程的概念:
• 线程是“执行单元”,负责进程内所包含代码的执行
• 调度器识别并且只调度线程,以允许程序并行运行
• 线程使用处理器寄存器,并且当其它线程开始运行时栈和其它资源都会被保存起来
• 线程可以运行在不同的优先级上
• 一个进程能够拥有多个线程
使用多线程的注意要点:
• 只有当确实需要时才使用处理器:多线程编程会影响处理器的效率,只有当必须使用的情况下才使用。
• 不要使用轮询的方式来同步线程:轮询通过循环重复地检查异步调用的状态。轮询是效率最低的线程管理方法,因为它重复地检查各个线程属性的状态,因而浪费了大量资源。
• 使用同步对象:在多线程编程中,注意使用同步对象,来保证共享资源的原子性
• 多线程增加了应用程序的复杂性
• 托管对象构建在Windows基本线程模型之上
• System.Threading.Thread类封装了Windows线
程功能
• Thread类提供了如下功能:
–创建线程
–启动线程
–设置线程优先级
–挂起/ 恢复线程执行
–终止线程
在.net中启动线程非常简单,实例化一个Thread对象,并将一个委托作为参数传入,然后调用线程对象的Start的方法,就启动了一个线程。
如何中止一个线程:
• 当所有的线程都结束时,该应用程序才能够被终止
• 由Thread.Abort引发异常的方法,在通常环境中不是终止线程结束的最好方法:最好不要调用Abort方法来终止线程,推荐使用变量来表示线程是否终结,用Join方法来等待线程终结。Join方法是阻塞一个线程,直到这个线程终止才返回。而Abort方法则是系统悄悄的销毁了线程而且不通知用户,并且不能再次重新启动该线程,并将抛出ThreadAbortException异常。
• 在应用程序终止前,所有前台线程都必须要终止:如果我们在.net的winform中创建的线程是一个前台线程,我们关闭窗口并不表示这个应用程序被终止了,因为可能还有一个前台线程在运行中,我们可以设置IsBackground属性为true来设置线程是否为后台线程。
贴段代码示例
- private void button1_Click(object sender, EventArgs e)
- {
- button1.Enabled = false;
- button2.Enabled = true;
- button3.Enabled = true;
- label1.Text = "Processing: Active";
- label1.ForeColor = Color.Red;
- label1.Update();
- processingDone = false;
- //启动线程
- myWorkerThread = new Thread(new ThreadStart(ExecuteProcessing));
- myWorkerThread.IsBackground = checkBox1.Checked;
- myWorkerThread.Start();
- }
- private void button2_Click(object sender, EventArgs e)
- {
- //设置线程中循环的标识true,表示结束循环。
- processingDone = true;
- //等待线程的完成,因为线程不循环了但仍然要执行Thread.Sleep(5000);
- //所以还要等待5000毫秒Join方法才能返回
- myWorkerThread.Join();
- button2.Enabled = false;
- button3.Enabled = false;
- button1.Enabled = true;
- label1.Text = "Processing: Not Active";
- label1.ForeColor = Color.Black;
- }
- private void button3_Click(object sender, EventArgs e)
- {
- //直接销毁线程
- myWorkerThread.Abort();
- //等待线程的完成,因为线程销毁了,所以很快就返回
- myWorkerThread.Join();
- button2.Enabled = false;
- button3.Enabled = false;
- button1.Enabled = true;
- label1.Text = "Processing: Not Active";
- label1.ForeColor = Color.Black;
- }
- private void ExecuteProcessing()
- {
- //try
- //{
- while (!processingDone)
- {
- Thread.Sleep(100);
- }
- //}
- //finally
- //{
- Thread.Sleep(5000);
- //}
- }
有3个Button,Button1启动线程,Button1启动线程,Button2将循环标识变量设为false来终止线程中的循环,并等待利用Join方法等待线程的返回,Button3利用Abort方法销毁线程。
线程中更新UI控件:
• 控件应该只被创建它们的线程更新:也就是说在.net的winform中,UI控件应该有主线程进行更新。
• 使用Control.Invoke来从其他线程委托更新控件
• 同步和异步Invoke方法均可用:同步是Invoke方法,异步是BeginInvoke方法。
• 支持参数传递
• 当从其它线程更新UI控件时,会抛出异常
下面贴代码
- public partial class Form1 : Form
- {
- private bool processingDone;
- private Thread myWorkerThread;
- public Form1()
- {
- InitializeComponent();
- }
- private void button2_Click(object sender, EventArgs e)
- {
- processingDone = true;
- //等待线程结束
- myWorkerThread.Join();
- button2.Enabled = false;
- button1.Enabled = true;
- }
- private void ExecuteProcessing()
- {
- //创建一个委托对象,并指定这个委托的函数地址
- UpdateLabelDelegate labelUpdater = new UpdateLabelDelegate(UpdateLabel);
- this.Invoke(labelUpdater, new object[]{"Processing: Active", Color.Red});
- try
- {
- while (!processingDone)
- {
- Thread.Sleep(1000);
- //调用同步的invoke方法
- //this.Invoke(labelUpdater, new object[] { "Processing: Still Active - " + DateTime.Now, Color.Green });
- //调用异步的Invoke方法
- this.BeginInvoke(labelUpdater, new object[] { "Processing: Still Active - " + DateTime.Now, Color.Green });
- }
- }
- finally
- {
- Thread.Sleep(1000);
- // this.Invoke(labelUpdater, new object[] { "Processing: Done", Color.Black });
- this.BeginInvoke(labelUpdater, new object[] { "Processing: Done", Color.Black });
- }
- }
- //定义一个委托,这个委托对应的函数是返回值为void,第一个参数为string类型,第二个参数为Color类型
- delegate void UpdateLabelDelegate(string labelText, Color textColor);
- private void UpdateLabel(string labelText, Color textColor)
- {
- //设置Label的文本和颜色
- label1.Text = labelText;
- label1.ForeColor = textColor;
- }
- private void button1_Click(object sender, EventArgs e)
- {
- button1.Enabled = false;
- button2.Enabled = true;
- processingDone = false;
- //创建线程
- myWorkerThread = new Thread(new ThreadStart(ExecuteProcessing));
- //启动线程
- myWorkerThread.Start();
- }
- }
在上面的代码中我们可以看到,我们没有在myWorkerThread的线程中直接更新Label的文本和颜色,而是借用委托和Invoke方法来修改Label的颜色和文本。Invoke方法和BeginInvoke方法都可以调用,但是其中有细微的区别。Invoke方法调用之后,调用Invoke方法的线程将被阻塞,直到Invoke方法返回之后才继续执行,而BeginInvoke将不会阻塞调用的线程,线程将继续执行,如果要获得BeginInvoke的返回值可以利用EndInvoke方法来获得。BeginInvoke的返回值是一个IAsyncResult对象,这个对象包含了异步调用的状态等信息,EndInvoke的参数是IAsyncResult对象,返回值是object,EndInvoke将在IAsyncResult对象中检索返回值,并以返回值进行返回。
线程与线程池:
• 采用通常方式来创建线程代价非常昂贵。
• 对于生命周期相对短的线程使用线程池
• 线程池由公共语言运行时(CLR)应用,支持:
– Threading.Timer对象
– 异步操作
• ThreadPool性能
– 当在线程池中没有空闲线程可用时,其代价与CreateThread
相同
– 由于线程池线程可以被重用,因此当线程池中存在空闲线程
时创建线程所带来的开销非常小
线程池可以说是有平台管理的已经创建好的线程集合,可以由用户来管理线程池的大小。线程池的默认线程个数为25个。
• 线程池中存在的线程可以被重用
• 如果线程池中没有线程存在,则创建新的线程(当线程池内线城池小于25个线程时)
• 如果线程池中的线程空闲一段时间(通常是60秒),那么该线程将会从池中移除,释放其所占资源
- public partial class Form1 : Form
- {
- //线程数量统计
- private int threadCounter = 0;
- //线程池中的数量统计
- private int threadPoolCounter = 0;
- //信号灯对象
- private AutoResetEvent doneEvent;
- public Form1()
- {
- InitializeComponent();
- }
- private void button1_Click(object sender, EventArgs e)
- {
- //设置当前鼠标形状为等待形状
- Cursor.Current = Cursors.WaitCursor;
- threadCounter = 0;
- //初始化信号灯,并设为非终止状态
- doneEvent = new AutoResetEvent(false);
- label1.Text = "";
- label2.Update();
- //设置启动计算时间开始的毫秒数
- int elapsedTime = Environment.TickCount;
- //循环创建1000个线程,并启动线程
- for (int i = 0; i < 1000; i++)
- {
- Thread workerThread = new Thread(new ThreadStart(MyWorkerThread));
- workerThread.Start();
- }
- //等待信号灯的状态,主线程被阻塞
- doneEvent.WaitOne();
- //用当前毫秒数减去初始毫秒数得到创建1000个线程花费的时间
- elapsedTime = Environment.TickCount - elapsedTime;
- //将时间显示在Label上
- label1.Text = "Creating threads: " + elapsedTime.ToString() + "mSec";
- Cursor.Current = Cursors.Default;
- }
- private void MyWorkerThread()
- {
- //将threadCounter变量进行原子操作加1
- Interlocked.Increment(ref threadCounter);
- Thread.Sleep(0);
- if(threadCounter == 1000)
- {
- //如果线程数量等于1000,设置信号灯为终止状态
- doneEvent.Set();
- }
- }
- private void button2_Click(object sender, EventArgs e)
- {
- Cursor.Current = Cursors.WaitCursor;
- threadPoolCounter = 0;
- //设置信号灯为非终止转台
- doneEvent = new AutoResetEvent(false);
- label2.Text = "";
- label2.Update();
- //设置创建线程池线程的初始时间
- int elapsedTime = Environment.TickCount;
- for(int i = 0;i < 1000;i++)
- {
- //将方法排入线程池队列,也就是说利用线程池中的线程来执行MyWaitCallBack方法
- ThreadPool.QueueUserWorkItem(new WaitCallback(MyWaitCallBack));
- }
- //等待信号灯的终止状态,主线程被阻塞
- doneEvent.WaitOne();
- //等到创建1000个线程池中线程的毫秒数
- elapsedTime = Environment.TickCount - elapsedTime;
- //更新Label
- label2.Text = "Creating threads: " + elapsedTime.ToString() + "mSec";
- Cursor.Current = Cursors.Default;
- }
- private void MyWaitCallBack(object stateInfo)
- {
- //原子量操作threadPoolCounter
- Interlocked.Increment(ref threadPoolCounter);
- Thread.Sleep(0);
- if (threadPoolCounter == 1000)
- {
- //设置型号灯为终止状态
- doneEvent.Set();
- }
- }
- }
执行上面一段代码,我们可以观测到利用线程池来创建1000个线程比直接创建1000个线程时间上快的多。但是注意对于生命周期相对短的线程使用线程池,如果生命周期较长,不要使用线程池。
线程同步:
• 非同步线程的运行时间不可预知
• 保护数据,当多线程访问数据时避免出现不可知结果:确定数据访问的原子性,保证同一时刻只有一个线程在访问数据。
• 协调线程使线程在达到某个状态时另一个线程可以改变其状态
同步对象
• 线程通常来讲不能自治,需要开发者进行协调处理
• 我们可以使用下列同步对象:
– Interlocked
– AutoResetEvent
– ManualResetEvent
– Monitor
– Mutex
线程安全
• 多线程访问对象可能会引发问题
• 读取不一致数据
• 两个线程可能会同时更新数据,导致产生出错误的结果
• 使用同步对象来创建线程安全类
• 只有需要并发访问的类才需要线程安全
Interlocked的类我们在上面一段代码中已经贴出来了使用的一个示例,通过Increment来递增变量,Interlocked类有点直接类似lock,通过lock线程锁来控制数据的原子访问。
- public partial class Form1 : Form
- {
- private string text;
- private bool thread1Running;
- private bool thread2Running;
- private AutoResetEvent m_Event = null;
- public Form1()
- {
- InitializeComponent();
- }
- private void button1_Click(object sender, EventArgs e)
- {
- //初始化信号灯为false,那么线程中调用WaitOne方法时,线程将会
- //被阻塞
- m_Event = new AutoResetEvent(false);
- label1.Text = "";
- text = "";
- button1.Enabled = false;
- //创建两个线程,并启动他们
- Thread workerThread1 = new Thread(Thread1Function);
- Thread workerThread2 = new Thread(Thread2Function);
- thread1Running = true;
- thread2Running = true;
- workerThread1.Start();
- workerThread2.Start();
- //设置信号灯为true,当某一个线程WaitOne收到信号时,将
- //继续执行
- m_Event.Set();
- }
- private void Thread1Function()
- {
- //通过WaitOne阻塞线程,当收到信号时继续执行
- if(m_Event.WaitOne())
- {
- //设置信号灯为false,实际上不写这句代码其他线程的WaitOne
- //方法也不会接受到信号
- m_Event.Reset();
- for (int i = 0; i < 5; i++)
- {
- text += i.ToString();
- Thread.Sleep(0);
- }
- }
- //执行完毕后调用Set方法,发出信号,通知另一个线程执行
- m_Event.Set();
- thread1Running = false;
- this.Invoke(new EventHandler(WorkerThreadsFinished));
- }
- private void Thread2Function()
- {
- if(m_Event.WaitOne())
- {
- m_Event.Reset();
- for (int i = 0; i < 5; i++)
- {
- text += i.ToString();
- Thread.Sleep(0);
- }
- }
- m_Event.Set();
- thread2Running = false;
- this.Invoke(new EventHandler(WorkerThreadsFinished));
- }
- public delegate void EventHandler();
- private void WorkerThreadsFinished()
- {
- label1.Text = text;
- button1.Enabled = true;
- }
- }
我们在上面这段代码可以看到,通过AutoResetEvent同步对象的示例方法,AutoRestEvent类似与一个信号灯,当信号灯亮的时候为true,熄灭的时候为false,WaitOne方法只有在信号灯为true的时候才不会阻塞线程,如果信号灯为false肯定会阻塞线程。但在编写上面这段代码的时候,没有调用Reset方法,其他线程的WaitOne方法也不会接受到信号,Set方法会由一个WaitOne方法捕获到,在捕获到之后马上就自动将将信号灯设置为false,但是ManualResetEvent不会自动将信号灯设置为false,那么表示ManualReset对象的Reset方法将会被多个WaitOne捕获到,除非手工调用Reset方法,在上面那段例子中,Reset将不能被省略。注意这两个对象的构造函数,如果参数是true的话,不用调用Set方法WaitOne将能捕获
- public partial class Form1 : Form
- {
- private string text;
- private bool thread1Running;
- private bool thread2Running;
- //锁对象
- private object m_Lock = new object();
- public Form1()
- {
- InitializeComponent();
- }
- private void button1_Click(object sender, EventArgs e)
- {
- label1.Text = "";
- text = "";
- button1.Enabled = false;
- //创建两个线程,并启动他们
- Thread workerThread1 = new Thread(Thread1Function);
- Thread workerThread2 = new Thread(Thread2Function);
- thread1Running = true;
- thread2Running = true;
- workerThread1.Start();
- workerThread2.Start();
- }
- private void Thread1Function()
- {
- //获取排它锁
- Monitor.Enter(m_Lock);
- for (int i = 0; i < 5; i++)
- {
- text += i.ToString();
- Thread.Sleep(0);
- }
- //释放排它锁
- Monitor.Exit(m_Lock);
- thread1Running = false;
- this.Invoke(new EventHandler(WorkerThreadsFinished));
- }
- private void Thread2Function()
- {
- Monitor.Enter(m_Lock);
- for (int i = 0; i < 5; i++)
- {
- text += i.ToString();
- Thread.Sleep(0);
- }
- Monitor.Exit(m_Lock);
- thread2Running = false;
- this.Invoke(new EventHandler(WorkerThreadsFinished));
- }
- public delegate void EventHandler();
- private void WorkerThreadsFinished()
- {
- label1.Text = text;
- button1.Enabled = true;
- }
- }
这是Monitor类锁的用法,Monitor类还可以进行线程中的通讯,主要用于生产者和消费者。下面是一段简单的生产者和消费者的代码
- public partial class Form1 : Form
- {
- private string text;
- //锁对象
- private object m_Lock = new object();
- //生产者消费者标识,架设true为生产,false为消费,
- //初始值为生产
- private bool m_Flag = true;
- public Form1()
- {
- InitializeComponent();
- }
- private void button1_Click(object sender, EventArgs e)
- {
- text = "";
- button1.Enabled = false;
- //创建两个线程,并启动他们
- Thread workerThread1 = new Thread(Thread1Function);
- Thread workerThread2 = new Thread(Thread2Function);
- workerThread1.Start();
- workerThread2.Start();
- }
- //生产线程
- private void Thread1Function()
- {
- for(int i = 0; i < 100; i++)
- {
- lock(m_Lock)
- {
- //当标识变量为消费的时候,Monitor暂时释放锁,并阻塞
- //当前线程
- if(!m_Flag)
- {
- Monitor.Wait(m_Lock);
- }
- //当标识变量为生产的时候,设置文本,并把标识
- //变量设置为消费
- text = "(t1) Called";
- this.Invoke(new EventHandler(WorkerThreadsFinished));
- m_Flag = false;
- //通知线程等待队列锁状态已经被改变,Wait的线程可以继续执行
- Monitor.Pulse(m_Lock);
- }
- }
- }
- //消费线程
- private void Thread2Function()
- {
- for(int i = 0; i < 100; i++)
- {
- lock(m_Lock)
- {
- //当标识变量为生产的时候,释放锁对象,阻塞当前线程
- if(m_Flag)
- {
- Monitor.Wait(m_Lock);
- }
- text = "(t2) Called";
- this.Invoke(new EventHandler(WorkerThreadsFinished));
- //设置标识标量为生产
- m_Flag = true;
- //通知线程等待队列锁状态已经被改变,Wait的线程可以继续执行
- Monitor.Pulse(m_Lock);
- }
- }
- }
- public delegate void EventHandler();
- private void WorkerThreadsFinished()
- {
- listBox1.Items.Add(text);
- }
- }
Monitor.Wait方法将会暂时释放锁对象并阻塞线程,一旦其他线程调用Pulse方法,锁对象自动恢复,线程继续执行。
下面在看一个Mutex(互斥量)的例子
- public partial class Form1 : Form
- {
- private string text;
- private bool thread1Running;
- private bool thread2Running;
- #if RUN_IN_SYNC
- private Mutex myMutex;
- #endif
- public Form1()
- {
- InitializeComponent();
- }
- private void button1_Click(object sender, EventArgs e)
- {
- label1.Text = "";
- text = "";
- button1.Enabled = false;
- #if RUN_IN_SYNC
- myMutex = new Mutex();
- #endif
- Thread workerThread1 = new Thread(Thread1Function);
- Thread workerThread2 = new Thread(Thread2Function);
- thread1Running = true;
- thread2Running = true;
- workerThread1.Start();
- workerThread2.Start();
- }
- private void Thread1Function()
- {
- #if RUN_IN_SYNC
- myMutex.WaitOne();
- #endif
- for (int i = 0; i < 5; i++)
- {
- text += i.ToString();
- Thread.Sleep(0);
- }
- #if RUN_IN_SYNC
- myMutex.ReleaseMutex();
- #endif
- thread1Running = false;
- this.Invoke(new EventHandler(WorkerThreadsFinished));
- }
- private void Thread2Function()
- {
- #if RUN_IN_SYNC
- myMutex.WaitOne();
- #endif
- for (int i = 0; i < 5; i++)
- {
- text += i.ToString();
- Thread.Sleep(0);
- }
- #if RUN_IN_SYNC
- myMutex.ReleaseMutex();
- #endif
- thread2Running = false;
- this.Invoke(new EventHandler(WorkerThreadsFinished));
- }
- public delegate void EventHandler();
- private void WorkerThreadsFinished()
- {
- label1.Text = text;
- button1.Enabled = true;
- }
- }
我们可以看到Mutex也有WaitOne的方法,但是与AutoResetEvent的WaitOne不同是ReleaseMutex方法来通知WaitOne继续执行线程,互斥量还可以用在进程之间的数据同步
- Smart Client学习笔记(7) 使用多线程创建高响应智能客户端应用程序
- Smart Client学习笔记(6) 分布式智能客户端应用程序开发最佳实践
- Smart Client学习笔记(1) 智能客户端开发概览
- 智能客户端(Smart Client)
- 智能客户端(Smart Client)
- Smart Client (智能客户端)
- Smart Client 智能客户端介绍
- Smart Client学习笔记(4) Windows Form中的智能客户端部署
- Smart Client学习笔记(8) 智能客户端中的离线数据访问
- Smart Client学习笔记(2) 创建专业用户界面的Windows Form应用程序
- Smart Client智能客户端技术初探
- Smart Client智能客户端技术初探
- Smart Client智能客户端技术初探
- Smart Client智能客户端技术初探
- Smart Client智能客户端技术初探
- Smart Client智能客户端技术初探
- Smart Client智能客户端技术初探
- Smart Client智能客户端技术初探
- (转)来电精灵Pro
- Terminus Font in gVim & PuTTY (Windows XP) || Windows XP 中 gVim, PuTTY 添加 Terminus 字体
- 几种数据库中随机取数据的方法
- #pragma pack(4) 对齐的使用
- 谋划(12)
- Smart Client学习笔记(7) 使用多线程创建高响应智能客户端应用程序
- String and StringBuffer 详解
- 基于泛型的DAL 分页方法(未完)
- 谋划(13)
- 解决分页“奇怪”问题
- 数据库开发一
- JBPM源码分析(-)-----JbpmConfigurtion
- 谋划(14)
- 谋划(15)