------- Windows Phone 7手机开发、.Net培训、期待与您交流! -------
在黑马论坛上提出了一个关于多线程的定时任务的问题,可能是我们Asp.Net板块论坛太冷清了,回答的人很少,也有可能自己提出的问题too easy,别人不屑回答。现将自己关于多线程学到整理成笔记,供大家参考。
一、线程处理概述
进程是操作系统中正在执行的不同应用程序的一个实例,操作系统把不同的进程分离开来。
线程是操作系统分配处理器时间的基本单元,每个线程都维护异常处理程序、调度优先级和一组系统用于在调度该线程前保存线程上下文的结构。
每个应用程序域都是用单个线程启动的(应用程序的入口点Main方法),应用程序域中的代码可以创建附加应用程序域和附加线程。
线程的优缺点
线程处理使程序能够执行并发处理,因而特别适合需要同时执行多个操作的场合。
多线程处理可解决用户响应性能和多任务的问题,但同时引入了资源共享和同步问题等问题。
二、创建多线程应用程序
应用程序运行时,将创建新的应用程序域。当运行环境调用应用程序的入口点(Main方法)时,将创建应用程序主线程。下面给出一段主线程示例。
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
public class WorkerThreadExample
{
static void Main()
{
Console.WriteLine("主线程:开始……");
// 主线程睡眠5秒钟
Thread.Sleep(5000);
Console.WriteLine("主线程:结束。");
Console.ReadLine();
}
}
}
2.创建和启动新线程
主线程以外的线程一般称之为工作线程。创建新线程的大致步骤如下:
创建一个将在主线程外执行的函数,即类的方法,用于执行新线程要执行的逻辑操作
在主线程(Main方法)中创建一个Thread的实例,指向步骤1中的函数。例如:Thread newThread = new Thread(anObject.AMethod)
调用创建的Thread的实例的Start()方法,以启动新线程。例如:newThread.Start()。创建和启动新线程示例。
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
public class Worker
{ // 工作线程执行逻辑的实现方法.
public void DoWork()
{
while (!_shouldStop)
{
Console.WriteLine("工作线程:working...");
}
Console.WriteLine("工作线程:terminating gracefully.");
}
public void RequestStop()
{
_shouldStop = true;
}
// 使用Volatile以启发编译器,本数据成员将被多线程访问.
private volatile bool _shouldStop;
}
public class WorkerThreadExample
{
static void Main()
{
Console.WriteLine("主线程:Starting worker thread...");
// 创建工作线程对象。但不启动线程.
Worker workerObject = new Worker();
Thread workerThread = new Thread(workerObject.DoWork);
// 启动工作线程.
workerThread.Start();
// 循环直至激活工作线程.
while (!workerThread.IsAlive) ;
// 让主线程睡眠1毫秒,以允许工作线程完成自己的工作:
Thread.Sleep(1);
// 要求工作线程停止自己:
workerObject.RequestStop();
// 使用Join方法阻止当前线程,直至对象线程终止.
workerThread.Join();
Console.WriteLine("主线程:Worker thread has terminated.");
Console.ReadLine();
}
}
}
3.暂停、中断和销毁线程
调用Thread.Sleep方法会导致当前线程立即阻止,阻止时间的长度等于传递给Thread.Sleep 的毫秒数。注意:一个线程不能针对另一个线程调用Thread.Sleep。
暂停线程示例
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
class PauseThreadTest
{
static void Main()
{
Console.WriteLine("主线程启动...");
//创建并启动工作线程
Thread newThread = new Thread(ThreadMethod);
//newThread.SetApartmentState(ApartmentState.MTA);
newThread.Start();
Console.WriteLine("主线程睡眠300ms...");
Thread.Sleep(300);
Console.WriteLine("主线程终止...");
Console.ReadKey();
}
static void ThreadMethod()
{
Console.WriteLine("工作线程启动...");
Console.WriteLine("工作线程睡眠1000ms...");
Thread.Sleep(1000);
Console.WriteLine("工作线程终止...");
}
}
}
通过对被阻止的线程调用Thread.Interrupt,可以中断正在等待的线程并引发ThreadInterruptedException,从而使该线程脱离造成阻止的调用。
中断线程示例
using System;
using System.Security.Permissions;
using System.Threading;
namespace CSharpPractice.Threading
{
class ThreadInterrupt
{
static void Main()
{
StayAwake stayAwake = new StayAwake();
Thread newThread = new Thread(stayAwake.ThreadMethod);
newThread.Start();
// 如果newThread 当前正被阻止或者将被阻止
// 下面的代码将在ThreadMethod中产生一个异常
Console.WriteLine("主线程调用newThread的Interrupt方法.");
newThread.Interrupt();
// 等待newThread结束.
newThread.Join();
Console.ReadKey();
}
}
class StayAwake
{
public void ThreadMethod()
{
Console.WriteLine("newThread正在执行ThreadMethod……");
try
{
Console.WriteLine("newThread 将进入睡眠……");
Thread.Sleep(Timeout.Infinite);
}
catch (ThreadInterruptedException e)
{
Console.WriteLine("newThread 被主线程中断.");
}
}
}
}
Abort 方法用于永久地停止也即销毁托管线程。调用Abort时,公共语言运行库在目标线程中引发ThreadAbortException,目标线程可捕捉此异常。
销毁线程示例
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
class Test
{
public static void Main()
{
Thread newThread = new Thread(new ThreadStart(TestMethod));
newThread.Start();
Thread.Sleep(1000);
// 销毁newThread.
Console.WriteLine("主程序销毁新线程……");
newThread.Abort("来自于主程序的信息.");
// 等待线程终止.
newThread.Join();
Console.WriteLine("新线程终止 – 退出主程序.");
Console.ReadLine();
}
static void TestMethod()
{
try
{
while (true)
{
Console.WriteLine("新线程运行中……");
Thread.Sleep(1000);
}
}
catch (ThreadAbortException abortException)
{
Console.WriteLine((string)abortException.ExceptionState);
}
}
}
}
三、线程优先级和线程调度
每个线程都有一个分配的优先级,在运行库内创建的线程最初被分配 Normal 优先级。通过线程的Priority属性可以获取和设置其优先级。下面是线程优先级和线程调度示例。
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
class PriorityTest
{
bool loopSwitch;
public PriorityTest()
{
loopSwitch = true;
}
public bool LoopSwitch
{
set { loopSwitch = value; }
}
public void ThreadMethod()
{
long threadCount = 0;
while (loopSwitch)
{
threadCount++;
}
Console.WriteLine("{0} with {1,11} priority " +
"has a count = {2,13}", Thread.CurrentThread.Name,
Thread.CurrentThread.Priority.ToString(),
threadCount.ToString("N0"));
}
}
class Test
{
static void Main()
{
PriorityTest priorityTest = new PriorityTest();
ThreadStart startDelegate =
new ThreadStart(priorityTest.ThreadMethod);
Thread threadOne = new Thread(startDelegate);
threadOne.Name = "ThreadOne";
Thread threadTwo = new Thread(startDelegate);
threadTwo.Name = "ThreadTwo";
threadTwo.Priority = ThreadPriority.BelowNormal;
Console.WriteLine("启动线程1(优先级默认为Normal)......");
threadOne.Start();
Console.WriteLine("启动线程2(优先级设置为BelowNormal)......");
threadTwo.Start();
// 允许数10 seconds.
Console.WriteLine("请耐心等待5秒......");
Thread.Sleep(5000);
priorityTest.LoopSwitch = false;
Console.ReadLine();
}
}
}
四、线程同步
当多个线程可以调用单个对象的属性和方法时,一个线程可能会中断另一个线程正在执行的任务,使该对象处于一种无效状态。因此必须针对这些调用进行同步处理。可以使用lock语句同步代码块,或使用监视器同步代码块。
2.使用lock语句同步代码块
lock语句使用lock关键字将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。lock关键字可以确保当一个线程位于代码的临界区时,另一个线程不会进入该临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。代码块完成运行,而不会被其他线程中断。
lock语句以关键字lock开头,并以一个对象作为参数,在该参数的后面为线程互斥的代码块。
使用lock语句同步代码块示例
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
class Account //账户类
{
private Object thisLock = new Object();
int balance;
Random r = new Random(); //准备生成随机数
public Account(int initial) //账户构造函数
{
balance = initial;
}
int Withdraw(int amount) //从账户中取款
{
// 本条件永远不会为True,除非注释掉lock语句
if (balance < 0) //账户余额不足,<=0
{
throw new Exception("账户余额不足(<=0)!");
}
// 注释掉下面的lock语句,测试lock关键字的效果
// lock (thisLock)
{
if (balance >= amount) //账户余额>取款额
{
Console.WriteLine("取款前账户余额: " + balance); //取款前账户余额
Console.WriteLine("取款额(-) : -" + amount); //取款额
balance = balance - amount;
Console.WriteLine("取款后账户余额: " + balance); //取款后账户余额
return amount;
}
else
{
return 0; // 拒绝交易
}
}
}
public void DoTransactions() // 执行交易DoTransactions()
{ // 从账户中取100次钱款,每次取款额为1~100中的随机数
for (int i = 0; i < 100; i++)
{
Withdraw(r.Next(1, 100));
}
}
}
class Test
{
static void Main()
{
Thread[] threads = new Thread[10]; //线程数组(10个元素)
Account acc = new Account(1000); //新建账户对象,初始存款额为1000
for (int i = 0; i < 10; i++) //循环10次
{ //每次开始新的线程,执行账户交易DoTransactions()
Thread t = new Thread(new ThreadStart(acc.DoTransactions));
threads[i] = t;
}
for (int i = 0; i < 10; i++) //循环10次
{
threads[i].Start(); //开始线程
}
Console.ReadLine();
}
}
}
3.使用监视器同步代码块
使用监视器(Monitor)也可以防止多个线程同时执行代码块。调用Monitor.Enter方法,允许一个且仅一个线程继续执行后面的语句;其他所有线程都将被阻止,直到执行语句的线程调用Exit。
4.同步事件和等待句柄
同步事件允许线程通过发信号互相通信,从而实现线程需要独占访问的资源的同步处理控制
同步事件有两种:AutoResetEvent(自动重置的本地事件)和 ManualResetEvent(手动重置的本地事件)
每种事件又包括两种状态:收到信号状态(signaled)和未收到信号状态(unsignaled)。下面是同步事件和等待句柄示例。
using System;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
namespace CSharpPractice.Threading
{
public class SyncEvents
{
public SyncEvents()
{
_newItemEvent = new AutoResetEvent(false);
_exitThreadEvent = new ManualResetEvent(false);
_eventArray = new WaitHandle[2];
_eventArray[0] = _newItemEvent;
_eventArray[1] = _exitThreadEvent;
}
public EventWaitHandle ExitThreadEvent
{
get { return _exitThreadEvent; }
}
public EventWaitHandle NewItemEvent
{
get { return _newItemEvent; }
}
public WaitHandle[] EventArray
{
get { return _eventArray; }
}
private EventWaitHandle _newItemEvent;
private EventWaitHandle _exitThreadEvent;
private WaitHandle[] _eventArray;
}
public class Producer //制造者线程
{
public Producer(Queue<int> q, SyncEvents e) //构造函数
{
_queue = q;
_syncEvents = e;
}
// Producer.ThreadRun方法一直循环,直到“退出线程”事件变为终止状态。
// 此事件的状态由 WaitOne 方法使用 SyncEvents 类定义的 ExitThreadEvent 属性进行测试。
// 在这种情况下,检查该事件的状态不会阻止当前线程,
// 因为 WaitOne 使用的第一个参数为零,这表示该方法应立即返回。
// 如果 WaitOne 返回 true,则说明该事件当前处于终止状态。
// ThreadRun 方法将返回,其效果相当于终止执行此方法的辅助线程。
public void ThreadRun()
{
int count = 0;
Random r = new Random();
// 在“退出线程”事件终止前,Producer.ThreadStart 方法将尝试在队列中保留 20 项。
// 项只是 0 到 100 之间的一个整数。在添加新项前,必须锁定该集合,
// 以防止使用者线程和主线程同时访问该集合。这一点是使用 lock 关键字完成的。
// 传递给 lock 的参数是通过 ICollection 接口公开的 SyncRoot 字段。
// 此字段专门为同步线程访问而提供。对该集合的独占访问权限
// 被授予 lock 后面的代码块中包含的所有指令。对于制造者添加
// 到队列中的每个新项,都将调用“新项”事件的 Set 方法。
// 这将通知使用者线程离开挂起状态并开始处理新项。
while (!_syncEvents.ExitThreadEvent.WaitOne(0, false))
{
lock (((ICollection)_queue).SyncRoot)
{
while (_queue.Count < 20)
{
_queue.Enqueue(r.Next(0, 100));
_syncEvents.NewItemEvent.Set();
count++;
}
}
}
Console.WriteLine("制造者线程: produced {0} items", count);
}
private Queue<int> _queue;
private SyncEvents _syncEvents;
}
public class Consumer //使用者线程
{
public Consumer(Queue<int> q, SyncEvents e)
{
_queue = q;
_syncEvents = e;
}
// Consumer.ThreadRun
// Consumer 对象还定义名为 ThreadRun 的方法。
// 与制造者的 ThreadRun 类似,此方法由 Main 方法创建的辅助线程执行。
// 然而,使用者的 ThreadStart 必须响应两个事件。
public void ThreadRun()
{
int count = 0;
// 使用 WaitAny 阻止使用者线程,直到所提供的数组中的任意一个
// 等待句柄变为终止状态。在这种情况下,数组中有两个句柄,
// 一个用来终止辅助线程,另一个用来指示有新项添加到集合中。
// WaitAny 返回变为终止状态的事件的索引。“新项”事件是数组中的
// 第一个事件,因此索引零表示新项。在这种情况下,检查索引 1
//(它指示“退出线程”事件),并使用它来确定此方法是否继续使用项。
// 如果“新项”事件处于终止状态,将通过 lock 获得对集合的
// 独占访问权限并使用新项。因为此示例生成并使用数千个项,
// 所以不显示使用的每个项,而是使用 Main 定期显示队列中的内容
while (WaitHandle.WaitAny(_syncEvents.EventArray) != 1)
{
lock (((ICollection)_queue).SyncRoot)
{
int item = _queue.Dequeue();
}
count++;
}
Console.WriteLine("使用者线程: consumed {0} items", count);
}
private Queue<int> _queue;
private SyncEvents _syncEvents;
}
public class ThreadSyncSample
{ // 线程同步示例:使用 lock 获得对队列的独占访问权限
private static void ShowQueueContents(Queue<int> q) //此方法由主线程执行的(被Main调用)
{
lock (((ICollection)q).SyncRoot)
{ // 对整个集合进行枚举
foreach (int item in q)
{
Console.Write("{0} ", item);
}
}
Console.WriteLine();
}
static void Main()
{ // 创建一个队列(该队列的内容将被生成和使用)
Queue<int> queue = new Queue<int>();
// 创建SyncEvents 的一个实例
SyncEvents syncEvents = new SyncEvents();
Console.WriteLine("配置制造者线程和使用者线程......");
// 配置 Producer 和 Consumer 对象以供辅助线程使用
Producer producer = new Producer(queue, syncEvents);
Consumer consumer = new Consumer(queue, syncEvents);
Thread producerThread = new Thread(producer.ThreadRun);
Thread consumerThread = new Thread(consumer.ThreadRun);
// 调用 Start 方法来启动两个辅助线程。
// 独立于当前正在执行 Main 方法的主线程开始异步执行过程
Console.WriteLine("启动制造者线程和使用者线程......");
producerThread.Start();
consumerThread.Start();
// Main通过调用 Sleep 方法将主线程挂起。
for (int i = 0; i < 4; i++) //重复四次
{ // 将当前正在执行的线程挂起2500毫秒
Thread.Sleep(2500);
// 2500毫秒后,Main 将重新激活,显示队列的内容
ShowQueueContents(queue);
}
// Main通过调用“退出线程”事件的 Set 方法通知辅助线程终止
Console.WriteLine("发信号,告知终止线程......");
syncEvents.ExitThreadEvent.Set();
//对每个辅助线程调用 Join 方法以阻止主线程,
//直到每个辅助线程都响应该事件并终止。
producerThread.Join();
consumerThread.Join();
Console.ReadLine();
}
}
5.使用Mutex同步代码块
mutex(mutually exclusive,互斥体)由 Mutex 类表示,与监视器(Monitor)类似,用于防止多个线程在某一时间同时执行某个代码块。
与监视器不同的是,mutex 可以用来使跨进程的线程同步。尽管mutex可以用于进程内的线程同步,但是它会消耗更多的计算资源,所以进程内的线程同步建议使用监视器(Monitor)。
当用于进程间同步时,mutex 称为“命名mutex”,因为它将用于另一个应用程序,因此它不能通过全局变量或静态变量共享。必须给它指定一个名称,才能使两个应用程序访问同一个mutex 对象。
Mutex 有两种类型:未命名的局部 mutex 和已命名的系统 mutex。下面是使用Mutex同步代码块示例。
// 本例显示如何使用“命名mutex”在进程或线程间发送信号。
// 在2个或多个命令行窗口运行本程序。
// 每个进程将创建一个Mutex对象:命名互斥体"MyMutex".
// 命名mutex是一个系统对象,其生命周期
// 由其所代表的Mutex 对象的生命周期所确定。
// 当第一个进程创建其局部Mutex时创建命名mutex。
// 本例中,命名mutex属于第一个进程。
// 当销毁所有Mutex对象时,释放此命名mutex
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
public class Test
{
public static void Main()
{
// 创建命名mutex。只能存在一个名为"MyMutex"的系统对象。
Mutex m = new Mutex(false, "MyMutex");
// 试图获取对命名mutex的控制权。
// 如果命名mutex被另一个线程所控制,则等待直至其被释放
Console.WriteLine("等待Mutex. . . . . .");
m.WaitOne();
// 保持对mutex的控制,直至用户按ENTER键.
Console.WriteLine("本应用拥有mutex。请按ENTER键释放之并退出!");
Console.ReadLine();
m.ReleaseMutex();
}
}
}
五、线程池
线程池是可以用来在后台执行多个任务的线程集合,这使主线程可以自由地异步执行其他任务。线程池通常用于服务器应用程序。每个传入请求都将分配给线程池中的一个线程,因此可以异步处理请求,而不会占用主线程,也不会延迟后续请求的处理。
一旦线程池中的某个线程完成任务,它将返回到等待线程队列中,等待被再次使用。这种重用使应用程序可以避免为每个任务创建新线程的开销。
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
public class Fibonacci
{
public Fibonacci(int n, ManualResetEvent doneEvent)
{
_n = n;
_doneEvent = doneEvent;
}
// 线程池回调:计算 Fibonacci 结果.
public void ThreadPoolCallback(Object threadContext)
{
int threadIndex = (int)threadContext;
Console.WriteLine("线程 {0} 开始......", threadIndex);
_fibOfN = Calculate(_n);
Console.WriteLine("线程 {0} 结果计算......", threadIndex);
_doneEvent.Set();
}
// 递归方法计算第N个Fibonacci数:F (n ) = F (n - 1) + F(n - 2).
public int Calculate(int n)
{
if (n <= 1) // F(0)=0; F(1)=1
{
return n;
}
return Calculate(n - 1) + Calculate(n - 2);
}
public int N { get { return _n; } }
private int _n;
public int FibOfN { get { return _fibOfN; } }
private int _fibOfN;
private ManualResetEvent _doneEvent;
}
public class ThreadPoolExample // 线程池
{
static void Main()
{
const int FibonacciCalculations = 5; // 5个Fibonacci数
// 每个事件用于每个Fibonacci对象
ManualResetEvent[] doneEvents = new ManualResetEvent[FibonacciCalculations];
Fibonacci[] fibArray = new Fibonacci[FibonacciCalculations];
Random r = new Random();
// 使用ThreadPool配置和启动线程:
Console.WriteLine("启动 {0} 个任务......", FibonacciCalculations);
for (int i = 0; i < FibonacciCalculations; i++)
{
doneEvents[i] = new ManualResetEvent(false);
Fibonacci f = new Fibonacci(r.Next(1, 10), doneEvents[i]);
fibArray[i] = f;
ThreadPool.QueueUserWorkItem(f.ThreadPoolCallback, i);
}
// 等待线程池中的所有线程的计算...
WaitHandle.WaitAll(doneEvents);
Console.WriteLine("完成所有计算!");
// 显示结果:第1项到第10项之间的5个Fibonacci数...
Console.WriteLine("第1项[F(0)]到第10项[F(9)]之间的任意5个Fibonacci数为----");
for (int i = 0; i < FibonacciCalculations; i++)
{
Fibonacci f = fibArray[i];
Console.WriteLine("Fibonacci({0}) = {1}", f.N, f.FibOfN);
}
Console.ReadLine();
}
}
}
------- Windows Phone 7手机开发、.Net培训、期待与您交流! -------