线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。当程序中有多个线程运行,就称为多线程。
多线程在windows form等编程是经常会用到的,它可以让UI主线程不阻塞,同时更有效的利用cpu资源,有更好的用户体验。但是当多个线程访问共享资源(比如静态变量)时,就需要考虑到线程同步的问题。比如当有一个线程对共享资源在写的时候,别的线程需要等待写完成后再获取资源,但是它会影响代码的执行效率。
线程的委托方法有ThreadStart和ParameterizedThreadStart,ParameterizedThreadStart委托需要方法传递object的参数。
Thread方法
Start(),并非立即执行,取决于操作系统的线程管理策略。
Sleep(),阻塞方法,当线程阻塞时,它不会占用CPU的时间。
Interrupt(),唤醒出于睡眠或者等待中的线程,如果调用Interrupt时,线程不是睡眠或等待状态时,会立即跑出异常。
Join(),一个线程的操作需要等待另一个线程执行完毕后才继续执行。阻塞方法,不占用CPU时间。
IsBackground属性,线程分前台线程和后台线程,当前台线程执行完成后,程序立即退出,不会等待后台线程执行。
Abort(),强制退出一个线程,会抛出ThreadAbortExcepion,但是即使不捕获它,也不会影响到整个进程。
线程同步
有lock,Monitor,Mutex,EventWaitHandle,Semaphore等同步机制。
lock和Monitor是.NET用一个特殊结构实现的,Monitor对象是完全托管的、完全可移植的,并且在操作系统资源要求方 面可能更为有效,同步速度较快,但不能跨进程同步。lock(Monitor.Enter和Monitor.Exit方法的封装),主要作用是锁定临界区,使临 界区代码只能被获得锁的线程执行。Monitor.Wait和Monitor.Pulse用于线程同步,类似信号操作,个人感觉使用比较复杂,容易造成死锁。
lock是我们经常用的,也是最简单的。
private object objLock = new object();
public void AddOne() // 没有加锁,会出现重复情况。
{
lock (objLock)
{
count++;
Console.WriteLine("current value is " + count);
}
}
比如上面的代码中count是共享资源,因为多个线程访问AddOne方法,会在线程切换中使加一的情况出现缺少连续的数的情况,所以我们需要用lock关键字把对资源操作的代码给包括起来。这样当一个线程获得objLock对象的锁后, 它会去执行lock内的代码,直到执行完成,而别的线程只能等待,直到锁被释放。
其实lock是对Monitor的封装,但是Monitor功能比lock要多。
lock(obj) { } 等价为: try {Monitor,有Enter,Exit,Wait,Pulse等方法,也有TryEnter方法。它可以实现多线程之间的先后执行顺序。Monitor.Enter(obj) } catch() {} finally { Monitor.Exit(obj) }
Enter,Exit比较简单,获得锁和释放锁。
Wait是释放锁,并把自己阻塞,一直到获得锁后再执行。
Thread t = new Thread(new ThreadStart(Produce));
t.Start();
Thread tt = new Thread(new ThreadStart(Consume));
tt.Start();
public void Produce()
{
while (true)
{
Monitor.Enter(obj);//获得锁
value++;
Monitor.Pulse(obj);//通知线程锁状态改变,没有Pulse的话,线程就会死锁。
Monitor.Wait(obj);//等待并释放锁
}
}
public void Consume()
{
while (true)
{
Monitor.Enter(obj);
Console.WriteLine(value);
Monitor.Pulse(obj);
Monitor.Wait(obj);
}
}
如上代码,实现的内容是控制一个线程加一,另一个才输出值。它可以控制线程执行的先后顺序。如果使用lock,或者Monitor的Enter和Exit,不能保证执行的顺序是按照加一后输出。但是Monitor也比较复杂,经过我的测试,Enter,Pulse,Wait是需要一起使用的,否则就死锁了。
死锁,死锁就是两个或者两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象。例如线程1拥有锁A,需要锁B,线程2拥有锁B,需要锁A,这样就会引起死锁。这里可以使用Monitor的TryEnter()方法解决,当无法获取锁时,释放自己已经有的锁。
使用WaitHandle,Mutex,EventWaitHandle,Semaphore继承了WaitHandle
互斥锁Mutex和事件对象EventWaitHandle属于内核对象,利用内核对象进行线程同步,线程必须要在用户模式和内核模 式间切换,所以一般效率很低,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。
Mutex(互斥体)
<pre name="code" class="csharp"> Mutex t = new Mutex();
void ThreadEntry()
{
t.WaitOne();
Console.WriteLine("thread sync test");
t.ReleaseMutex();
}
互斥锁Mutex类似于一个接力棒,拿到接力棒的线程才可以开始跑,当然接力棒一次只属于一个线程(Thread Affinity),如果这个线程不释放接力棒(Mutex.ReleaseMutex),那么没办法,其他所有需要接力棒运行的线程都知道能等着看热闹。Mutex可以用于多进程同步,当多进程共享内存时,可以使用Mutex进行进程间同步。这里不做讨论。
EventWaitHandle
ManualResetEvent和AutoResetEvent都继承自EventWaitHandle。他们有2个状态,收到信号和未收到信号。
ManualResetEvent
<pre name="code" class="csharp">public class ThreadSyncTest
{
ManualResetEvent mer = new ManualResetEvent(false);//参数false,表示一开始在mer.WaitOne这是等待的。如果是true,则一开始就不需要等待,可以直接往下执行
public ThreadSyncTest()
{
ThreadStart ts = new ThreadStart(ThreadEntry);
new Thread(ts).Start();
}
public void ThreadEntry()
{
while (true)
{
mer.WaitOne();
Console.WriteLine("Run");
Thread.Sleep(500);
}
}
public void Start()
{
mer.Set();//给mer一个信号,继续往下执行
}
public void Stop()
{
mer.Reset();//使线程停止在mer.WaitOne()处,无法往下执行
}
}
reset方法,使ManualResetEvent 处于未收到信号状态,无法往下执行。
set方法,使ManualResetEvent处于收到信号状态,可以往下执行。
AutoResetEvent与ManualResetEvent的区别是WaitOne方法,合并了Reset方法。调用WaitOne()方法使线程停止,需要再次Set,才能继续执行。
Semaphore, 限制可同时访问某一资源或资源池的线程数。