在B/S开发中我们可能很少需要用到多线程,一方面,在同一个会话中需要同时执行的操作并不多,另一方面, 因为浏览器为我们做了一些工作。服务器进行长时间运算的时候浏览器会等待,并有一些友好的提示。而不是挂起在那。(如果你觉得这些提示还不能令人满意,一些客户端技术如AJAX能达到一些不错的效果。)
然而当我们需要做一些windows程序或是服务时情况就不同了,最简单的情况,我们执行一个耗时操作,这个时候你会发现你的界面没有响应了。这是很糟糕的用户体验。用户根本不知道程序是否还在正常运行。所以我们需要有多个线程来执行不同的任务。.net为我们提用了几种实现多线程的方法,如Thread类、异步调用、ThreadPool等。但在使用中也会遇到一些问题,下面就我遇到过的问题作一个归纳和总结。
1. 在其他线程中更新UI
起因: .net Windows窗体使用STA模型。STA模型意味着可以在任何线程上创建窗口,但窗口一旦创建后就不能切换线程,并且对它的所有函数调用都必须在其创建线程上发生(后称GUI线程)。在其他线程中访问UI对象是危险的,虽然你也许不会马上看见异常。
解决方法:使用Control对象的Invoke,BeginInvoke和EndInvoke来封送方法到GUI线程上执行。
提示:
1) 可以使用Form对象的InvokeRequired属性来判断是否需要封送;
2) 异步操作的回调也不是在UI线程上执行,所以必须封送;
3) 不必在System.Windows.Forms.Timer事件处理器中封送,该对象不同于.Net Framework中另外两个记时器(分别为System.Timers.Timer和System.Threading.Timer),System.Windows.Forms.Timer不是多线程的。同时你应该注意不要在System.Windows.Forms.Timer的事件处理器中执行长时间操作。这样同样会挂起UI。
示例:
private MethodInvoker Invoker;
private void btnStart_Click(object sender, EventArgs e)
{
Invoker = new MethodInvoker(AsyncCallMethod);
//开始异步调用,此方法将马上返回,并在新线程上调用AsyncCallMethod,当调用结束时将执行AsyncCallback回调
Invoker.BeginInvoke(new AsyncCallback(AsyncCallback), null);
lblStatus.Text = "调用开始";
btnStart.Enabled = false;
}
private void AsyncCallMethod()
{
//执行一些耗时操作
System.Threading.Thread.Sleep(10000);
}
private void AsyncCallback(IAsyncResult ar)
{
Invoker.EndInvoke(ar);
//判断是否在GUI线程上
if (this.InvokeRequired)
{
this.BeginInvoke(new MethodInvoker(UpdateUI));
}
}
private void UpdateUI()
{
lblStatus.Text = "异步调用结束";
btnStart.Enabled = true;
}
2. 避免多个线程同时访问一段代码
起因:在多线程程序中有时需要做到某些操作同时只能允许一个线程调用。如某property的get和set如果在多线程程序中使用,两个线程同时进入了get和set,一个线程为属性写入了新值,而另一个线程取回的则是以前的值。这往往是不希望看到的。
解决方法:在需要同步的地方使用互斥锁。只有获取到锁才执行语句,未获取到锁将等待,直到获取到锁。
提示:
1) C#中的lock语句,VB.NET中的synclock语句相当于Monitor的Enter和Exit。
2) 在set和get中分别加上lock或都加上lock可以实现“只允许一个线程写,写入时其他线程可以读”、“只允许一个线程写,写入时不可读”等多种效果。
3) 只能获取引用对象的互斥锁。
4) 可以简单的使用lock (this)来获取当前对象的互斥锁。
5) 当你使用Monitor的时候一定将Exit放入Finally块中,否则当其间代码出现异常时锁就不能被释放,而使用lock/synclock则避免了这种情况
示例:
private object objTest;
public object ObjTest
{
get
{
lock (objTest)
{
return objTest;
}
}
set
{
lock (objTest)
{
objTest = value;
}
}
}
3. 在多线程中访问某个值类型变量
起因:当多个线程同时访问值类型字段时会出现脏读
解决方法:Interlocked对象提供了对值类型变量的原子操作。
提示:提供递增,递减,比较,更换值等操作
示例:
using System.Threading;
public class ThreadSafe
{
private int totalValue = 0;
public int Total
{
get { return totalValue; }
}
public int AddToTotal(int addend)
{
int initialValue, computedValue;
do
{
// 将totalValue当前值缓存在本地变量
initialValue = totalValue;
// 计算缓存值与增加量的和
computedValue = initialValue + addend;
//比较缓存值与当前totalValue是否一样,是则更新totalValue为computedValue
//不相等则再次循环
} while (initialValue != Interlocked.CompareExchange(
ref totalValue, computedValue, initialValue));
return computedValue;
}
}
4. 实现线程间通信
起因:多线程程序中有时需要做到某个线程等待其他线程通知才继续执行。
解决方法:使用ManualResetEvent和AutoResetEvent
提示:AutoResetEvent和ManualResetEvent的区别在于ManualReset在发送信号后后将一直保持发信号状态,直到被Reset;而AutoResetEvent发送信号后在等待线程恢复执行后由系统Reset
示例:
class Program
{
static System.Threading.ManualResetEvent AcceptDone = new System.Threading.ManualResetEvent(true);
static void Main(string[] args)
{
Socket objSock = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
IPEndPoint LocalEndPoint = new IPEndPoint(IPAddress.Any,8088);
objSock.Bind(LocalEndPoint);
objSock.Listen(100);
while(true){//循环等待连接
AcceptDone.Reset();
Console.WriteLine("等待连接");
//异步接受连接
objSock.BeginAccept(new AsyncCallback(AcceptCallback), objSock);
//使用ManualResetEvent等待连接完毕
AcceptDone.WaitOne();
}
}
static void AcceptCallback(IAsyncResult ar)
{
Socket handler = ((Socket)ar.AsyncState).EndAccept(ar);
Console.WriteLine("收到一个连接");
Console.WriteLine("IP:{0}", ((IPEndPoint)handler.RemoteEndPoint).Address);
//设置ManualResetEvent状态,让主线程继续接受连接
AcceptDone.Set();
System.Threading.ManualResetEvent ReadDone = new System.Threading.ManualResetEvent(true);
StateObject state = new StateObject();
state.workSocket = handler;
state.ReadDone = ReadDone;
while (handler.Connected)
{
state.ReadDone.Reset();
Array.Clear(state.buffer, 0, state.buffer.Length);
handler.BeginReceive(state.buffer, 0, state.BufferSize, SocketFlags.None,
new System.AsyncCallback(readCallback), state);
state.ReadDone.WaitOne();
}
Console.WriteLine(state.sb.ToString());
}
static void readCallback(IAsyncResult ar)
{
StateObject State = (StateObject)ar.AsyncState;
if (State.workSocket.Connected)
{
int Read = State.workSocket.EndReceive(ar);
if (Read > 0)
{
State.sb.Append(System.Text.Encoding.GetEncoding("gb2312").GetString(State.buffer, 0, Read));
}
else
{
State.workSocket.Close();
}
State.ReadDone.Set();
}
}
}
class StateObject{
public Socket workSocket = null;
public int BufferSize = 1024;
public Byte[] buffer = new byte[1024];
public System.Text.StringBuilder sb = new System.Text.StringBuilder();
public System.Threading.ManualResetEvent ReadDone;
}
其他值得注意的:
使用lock的时候一定要防止死锁。如线程a取得了对象A的锁,而线程b取得了对象B的锁,这时线程a又去获取对象B的锁,线程b去获取对象A的锁,这时候线程a/b都不可能再继续运行下去,即形成了死锁。
使用异步调用必须调用其EndInvoke方法。否则线程不能得到正常释放。线程池中的线程可能会被用完。