System.Threading命名空间提供了许多类型用来构建多线程应用程序。比如访问线程池、Timer类、以及大量的用来同步访问共享资源的类型。其中最基础的类型是Thread。使用该类型中定义的方法能够在当前应用程序域中创建、挂起、停止和销毁线程。
(通过Thread可以获得当前线程的统计信息)
以编程方式创建次线程
可以通过ThreadStrat委托和ParameterizedThreadStart委托执行在次线程中执行的方法。前者执行一个没有参数、无返回值的方法,局限是无法给过程传递参数,所以这一委托通常被设计用来正在后台运行、而没有更多的交互作用。后者则允许包含一个System.Object类型的参数。
ThreadStrat使用
public class Printer
{
public void PrintNumbers()
{
}
}
Printer p = new Printer();
Thread backgroundThread =new Thread(new ThreadStart(p.PrintNumbers));
backgroundThread.Name = "Secondary";
backgroundThread.Start();
PParameterizedThreadStart使用
前台线程和后台线程
n 前台线程:能阻止应用程序的终结。一直到所有前台线程终止后,CLR才能关闭应用程序(就是卸载应用程序域);
n 后台线程(有时候也叫守护线程,daemon thread):被CLR认为是程序中执行中可以做出牺牲的途径,在任何时候,当应用程序结束时(主程序结束),所有后台线程也会被自动终止(不管是否在执行)。
所有通过Thread.Start()方法创建的线程都自动式前台线程。意味着,知道所有线程本身单元的工作都执行完成了,应用程序才会被下载。另外,只要把IsBackground 属性改成true,那么该线程就变成后台线程。
看下面的代码:
public class Printer
{
public void PrintNumbers()
{
Console.WriteLine("-> {0} is executing PrintNumbers()",
Thread.CurrentThread.Name);
Console.Write("Your numbers: ");
for (int i = 0; i < 10; i++)
{
Console.Write("{0}, ", i);
Thread.Sleep(2000);
}
Console.WriteLine();
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** Background Threads *****\n");
Printer p = new Printer();
Thread bgroundThread =
new Thread(new ThreadStart(p.PrintNumbers));
// 后台线程
bgroundThread.IsBackground = true;
bgroundThread.Start();
}
}
本来,成灰应该打印出1---9,然后才会退出。在Main()方法执行完毕,应用程序就会自动结束,次线程虽然还在运行,但是,设置了打印的线程为后台线程,所以它也被结束了。看不到完整的输出。把该语句去掉以后,可以看到完整的输出。
并发问题
看下面的代码:
在该程序域中的主线程产生了10个工作线程,每个工作线程同时执行同一个Printer实例的PrintNumbers()方法。由于没有预防锁定共享资源。所以在PrintNumders()输出到控制台之前,调用PrintNumders()方法的线程可能会被挂起。当每个线程都调用Printer来输出数字的时候,线程调度器可能正在切换线程,这导致了不同的输出结果。
public class Printer
{
public void PrintNumbers()
{
Console.WriteLine("-> {0} is executing PrintNumbers()",
Thread.CurrentThread.Name);
Console.Write("Your numbers: ");
for (int i = 0; i < 10; i++)
{
//线程休眠秒数
Random r = new Random();
Thread.Sleep(100 * r.Next(5));
Console.Write("{0}, ", i);
}
Console.WriteLine();
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("*****Synchronizing Threads *****\n");
Printer p = new Printer();
// 使个线程全部执行同一个对象的统一方法
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++)
{
threads[i] =
new Thread(new ThreadStart(p.PrintNumbers));
threads[i].Name = string.Format("Worker thread #{0}", i);
}
// 开始每一个线程
foreach (Thread t in threads)
t.Start();
Console.ReadLine();
}
}
}
(一种可能的结果)
使用lock关键字
对于上面的问题,需要一种方式来通过编程控制对共享资源的同步访问。首选的是lock关键字。这个关键字允许定义一段线程同步的代码语句。采用这种方式,后进入的线程不会中断当前线程,而是停止自身的下一步执行。Lock关键字需要一个标记,即一个引用对象,线程在进入锁定访问的时候必须获得这个标记。当试图锁定的是一个实例级对象的私有方法时,使用方法本身所在对象的引用就可以了。将上面的代码,修改为:
// 锁标记(如果要锁定静态方法,只需要一个私有静态对象成员作为锁标记)
private object threadLock = new object();
public void PrintNumbers()
{
lock (threadLock)
{
//显示线程信息
Console.WriteLine("-> {0} is executing PrintNumbers()",
Thread.CurrentThread.Name);
// 输出数字
Console.Write("Your numbers: ");
for (int i = 0; i < 10; i++)
{
//线程休眠秒数
Random r = new Random();
Thread.Sleep(100 * r.Next(5));
Console.Write("{0}, ", i);
}
Console.WriteLine();
}
}
看到,结果如下:
使用System.Threading.Monitor进行同步
Lock关键字实际上是和System.Threading.Monitor类一同使用时的速记符号。经过编译器的处理,锁定区域实际上被转换为如下内容:
Monitor.Enter(threadLock );
try
{
//显示线程信息
Console.WriteLine("-> {0} is executing PrintNumbers()",
Thread.CurrentThread.Name);
// 输出数字
Console.Write("Your numbers: ");
for (int i = 0; i < 10; i++)
{
//线程休眠秒数
Random r = new Random();
Thread.Sleep(100 * r.Next(5));
Console.Write("{0}, ", i);
}
Console.WriteLine();
}
finally
{
Monitor.Exit(threadLock);
}
相比较于lock,使用System.Threading.Monitor可以有更好的控制能力。使用该类型,可以(使用Wait()方法)只是活动的线程等待一段时间,在当前线程完成操作时,使用(Pulse()或PulseAll())通知等待中的线程。
使用System.Threading.Interlocked进行原子操作
在底层的CIL代码,赋值和简单的数字运算都不是原子操作。System.Threading.Interlocked类允许我们来原子型操作单个数据,使用它比Monitor更简单。
Increment 和 Decrement 方法递增或递减变量并将结果值存储在单个操作中。在大多数计算机上,增加变量操作不是一个原子操作,需要执行下列步骤:
1.将实例变量中的值加载到寄存器中。
2.增加或减少该值。
3.在实例变量中存储该值。
如果不使用 Increment 和 Decrement,线程会在执行完前两个步骤后被抢先。然后由另一个线程执行所有三个步骤。当第一个线程重新开始执行时,它覆盖实例变量中的值,造成第二个线程执行增减操作的结果丢失。
Exchange
{
int newVal = Interlocked.Increment(ref intVal);
}
使用
{
int newVal = Interlocked.Exchange(ref intVal,83);
}
最后一个同步原语是[Synchronization]特性。它位于System.Runtime.Remoting.Contexts命名空间下。这个类级别的特性有效地使对象的所有实例的成员都保持线程安全。当CLR分配带[Synchronization]对象时,它会把这个对象放在同步上下文中。(它的主要问题是,即使一个方法没有使用线程敏感的数据,CLR仍然会锁定对该费那个发的调用,这会降低性能)。
使用Timer Callback
许多程序需要定期调用具体费那个发。比如,可能有一个应用程序需要在状态栏上通过一个辅助函数显示当前时间,或,可能希望应用程序调用一个辅助函数,让它执行非紧迫的后台任务,比如,监察是否拥有新邮件。像这些情况,可以使用System.Threading.Timer和TimerCallback委托。
CLR线程池
为了提高效率,使用BeginInvoke()的时候,CLR并不会创建新的线程,委托的BeginInvoke()方法创建了维护的工作线程池。可以使用Threading的ThreadPool类型与之交互。
如果想要使用池中的工作线程排队执行一个方法,可以使用ThreadPool.QueueUserWorkItem()方法。这个被重载的方法可以让你传递一个可选的Object类型的自定义状态数据给WaitCallback委托实例:
class ThreadPoolApp
{
public static void executeThreadPool()
{
Console.WriteLine("Main thread started. ThreadID = {0}",
Thread.CurrentThread.ManagedThreadId);
Printer p = new Printer();
WaitCallback workItem = new WaitCallback(PrintTheNumbers);
// 调用这个方法10次
for (int i = 0; i < 10; i++)
{
ThreadPool.QueueUserWorkItem(workItem, p);
}
Console.WriteLine("所有任务都已经入队,执行");
}
static void PrintTheNumbers(object state)
{
Printer task = (Printer)state;
task.PrintNumbers();
}
}
我们可以看到,比起显示地创建线程对象,使用这个被CLR锁维护的线程池的好是:
1.线程池减少了线程创建、开始。亭子的次数,提高了效率。
2.使用线程池,能够使我们的注意力费那个在业务逻辑上,而不是多线程架构上
但是,有时候,还是需要手动管理。比如:
1.如果需要设置线程优先级别,或者线程池中的线程总是后台线程,且它的优先级是默认的。
2.如果需要有一个带有固定标识的线程便于退出、挂起或通过名字发现它。
BackgroundWorker组件的作用
它位于System.ComponentModel命名空间下,构建一个Windows Forms桌面应用且需要执行在应用程序主UI线程之外的线程中长期的任务(调用远程服务、进行数据库事务、下载大文件等等)时,BackgroundWorker就能发挥它的所用。(可以直接使用Threading下的类型,但是BackgroundWorker更加方便)。
要使用BackgroundWorker,我们只需要告诉它希望在后台执行那个方法并且调用RunWorkerAsync()即可。调用线程(通常是主线程)继续正常运行,而工作方法会一步执行。结束以后,BackgroundWorker类型会通过触发RunWorkerCompleted事件来通知调用线程。
联系客服