原外文地址可以点击此处查看
本文介绍了多线程的工作原理。您将了解操作系统如何管理线程执行,并向您展示如何在程序中操作Thread类来创建和启动托管线程。包含的知识点有线程创建,竞争条件,死锁,监视器,互斥锁,同步和信号量等。
线程是程序中的独立指令流。线程类似于串行执行程序。但是,单独一个线程本身不是一个程序,它不能独立运行,而是在程序的上下文中运行。
线程的实际用法不是关于单个串行线程,而是在单个程序中使用多个线程。同时运行并执行各种任务的多个线程称为多线程。线程被认为是一个轻量级进程,因为它在程序的上下文中运行并使用分配给该程序的资源。
使用任务管理器,您可以打开“进程”(原文是线程感觉是手误写错?)列,查看每个进程的进程和线程数。在这里,您可以注意到只有cmd.exe具有单个线程,而所有其他应用程序使用多个线程。
操作系统调度线程。线程具有优先级,并且每个线程都有自己的堆栈,但程序代码和堆的内存在单个进程中所有线程之间共享。
进程由一个或多个执行线程组成。进程始终由至少一个称为主线程的线程(C#程序中的Main()方法)组成。单线程进程只包含一个线程,而多线程进程包含多个执行线程。
在计算机上,操作系统加载并启动应用程序。每个应用程序或服务在计算机上作为单独的进程运行。下图说明实际运行的进程比系统中运行的实际应用程序多。其中许多进程是后台操作系统进程,它们在操作系统加载时自动启动。
System.Threading命名空间
与许多其他功能一样,在.NET中,System.Threading是提供各种类型的命名空间,以帮助构建多线程应用程序。
类型 | 描述 |
---|---|
Thread | 它表示在CLR中执行的线程。使用它,我们可以在应用程序域中生成其他线程。 |
Mutex | 它用于应用程序域之间的同步。 |
Monitor | 它使用Locks和Wait实现对象的同步。 |
Smaphore | 它允许限制可以同时访问资源的线程数。 |
Interlock | 它为多个线程共享的变量提供原子操作。 |
ThreadPool | 它用于与CLR维护的线程池进行交互。 |
ThreadPriority | 它表示线程优先级,例如高,正常,低。 |
System.Threading.Thread类
Thread类允许您在程序中创建和管理托管线程的执行。这些线程称为托管线程。
成员 | 类型 | 描述 |
---|---|---|
CurrentThread | Static | 返回当前运行线程的引用。 |
Sleep | Static | 暂停当前线程特定的持续时间。 |
GetDoamin | Static | 返回当前应用程序域的引用。 |
CurrentContext | Static | 返回当前正在运行的线程的当前上下文的引用。 |
Priority | Instance level | 获取或设置线程优先级。 |
IsAlive | Instance level | 以True或False值的形式获取线程状态。 |
Start | Instance level | 指示CLR启动线程。 |
Suspend | Instance level | 挂起线程。 |
Resume | Instance level | 恢复以前挂起的线程。 |
Abort | Instance level | 指示CLR终止线程。 |
Name | Instance level | 允许建立线程名称。 |
IsBackground | Instance level | 指示线程是否在后台运行。 |
获取当前线程信息
为了说明Thread类型的基本用法,我们创建一个控制台应用程序,其中CurrentThread属性来获取当前正在执行的线程的Thread对象。
using System;
using System.Threading;
namespace threading
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("**********Current Thread Informations***************\n");
Thread t = Thread.CurrentThread;
t.Name = "Primary_Thread";
Console.WriteLine("Thread Name: {0}", t.Name);
Console.WriteLine("Thread Status: {0}", t.IsAlive);
Console.WriteLine("Priority: {0}", t.Priority);
Console.WriteLine("Context ID: {0}", Thread.CurrentContext.ContextID);
Console.WriteLine("Current application domain: {0}",Thread.GetDomain().FriendlyName);
Console.ReadKey();
}
}
}
编译完这个应用程序后,输出结果如下:
简单线程创建
以下示例说明Thread类的实现,其中Thread类的构造函数接受委托参数。创建Thread类对象后,可以使用Start()方法启动该线程,如下所示;
using System;
using System.Threading;
namespace threading
{
class Program
{
static void Main(string[] args)
{
Thread t = new Thread(myFun);
t.Start();
Console.WriteLine("Main thread Running");
Console.ReadKey();
}
static void myFun()
{
Console.WriteLine("Running other Thread");
}
}
}
运行应用程序后,您将获得以下两个线程的输出:
这里要注意的重点是,不能保证首先出现输出的是什么,换句话说,不能保证哪个线程先出现。因为线程由操作系统调度。所以哪个线程先出现可能每次都不同。
后台线程
只要至少有一个前台线程正在运行,应用程序的进程就会一直运行。如果Main()方法结束但是还有多个正在运行的前台线程,则应用程序的进程将保持活动状态,直到所有前台线程完成其工作,并且通过前台线程终止,所有后台线程也将立即终止。
使用Thread类创建线程时,可以通过设置属性IsBackground将其定义为前台线程或后台线程。Main()方法将线程“t”的此属性设置为false。设置新线程后,主线程只是向控制台写入结束消息。新线程写入开始和结束消息,并在它之间休眠2秒。
using System;
using System.Threading;
namespace threading
{
class Program
{
static void Main(string[] args)
{
Thread t = new Thread(myFun);
t.Name = "Thread1";
t.IsBackground = false;
t.Start();
Console.WriteLine("Main thread Running");
Console.ReadKey();
}
static void myFun()
{
Console.WriteLine("Thread {0} started", Thread.CurrentThread.Name);
Thread.Sleep(2000);
Console.WriteLine("Thread {0} completed", Thread.CurrentThread.Name);
}
}
}
编译运行此程序时,会看到写入控制台的完成消息,因为新线程是前台线程。这里的输出如下:
如果将IsBackground属性更改为true,则控制台中显示的结果将如下所示:
并发问题
当启动访问相同数据的多个线程时,程序需要确保共享数据不受其他线程更改其值。
竞争条件
如果两个或多个线程访问同一对象并且未同步对共享状态的访问,则会发生竞争条件。为了说明竞争条件的问题,让我们构建一个控制台应用程序。此应用程序使用Test类通过暂停当前线程来打印10个数字。
using System;
using System.Threading;
namespace threading
{
public class Test
{
public void Calculate()
{
for (int i = 0; i < 10; i++)
{
Thread.Sleep(new Random().Next(5));
Console.Write(" {0},", i);
}
Console.WriteLine();
}
}
class Program
{
static void Main(string[] args)
{
Test t = new Test();
Thread[] tr = new Thread[5];
for (int i = 0; i < 5; i++)
{
tr[i] = new Thread(new ThreadStart(t.Calculate));
tr[i].Name = String.Format("Working Thread: {0}", i);
}
//Start each thread
foreach (Thread x in tr)
{
x.Start();
}
Console.ReadKey();
}
}
}
编译运行此程序后,此应用程序中的主线程首先生成五个辅助线程。并告诉每个工作线程在同一个Test类实例上调用Calculate()方法。因此,所有五个线程同时开始访问Calculate()方法,因为我们没有采取任何预防措施来锁定此对象的共享资源; 这将导致竞争条件,应用程序产生不可预测的输出,如下所示
死锁
应用程序中锁太多可能会导致程序出现问题。在死锁中,至少有两个线程互相等待释放锁。由于两个线程相互等待,发生死锁情况下线程无休止地互相等待导致程序停止响应。
下面两个方法都通过锁定它们来改变对象obj1和obj2的状态。方法DeadLock1()首先锁定obj1,接着锁定obj2,类似地,方法DeadLock2()首先锁定obj2,然后锁定obj1。因此释放对obj1的锁定,接下来发生线程切换,第二个方法启动并获取obj2的锁定。第二个线程现在等待obj1的锁定。两个线程现在都在等待,不会互相释放。这是一种典型的死锁。
using System;
using System.Threading;
namespace threading
{
class Program
{
static object obj1 = new object();
static object obj2 = new object();
public static void DeadLock1()
{
lock (obj1)
{
Console.WriteLine("Thread 1 got locked");
Thread.Sleep(500);
lock (obj2)
{
Console.WriteLine("Thread 2 got locked");
}
}
}
public static void DeadLock2()
{
lock (obj2)
{
Console.WriteLine("Thread 2 got locked");
Thread.Sleep(500);
lock (obj1)
{
Console.WriteLine("Thread 1 got locked");
}
}
}
static void Main(string[] args)
{
Thread t1 = new Thread(new ThreadStart(DeadLock1));
Thread t2 = new Thread(new ThreadStart(DeadLock2));
t1.Start();
t2.Start();
Console.ReadKey();
}
}
}
同步
同步可以避免多个线程可能出现的问题(例如Race条件和死锁)。通常建议不在线程之间共享数据来避免并发问题。当然,这并非是总是可能的。如果数据共享是不可避免的,那么必须使用同步,以便一次只有一个线程访问和更改共享状态。
下面章节讨论各种同步技术。
锁
我们可以使用lock关键字同步对共享资源的访问。通过这样做,传入的线程不能中断当前线程,阻止它完成其工作。lock关键字需要对象引用。
通过采用之前的竞争条件问题,我们可以通过对关键语句实施锁定来完善此程序,使其从竞争条件中输出不确定的数据变成可靠的数据,如下所示
public class Test
{
public object tLock = new object();
public void Calculate()
{
lock (tLock)
{
Console.Write(" {0} is Executing",Thread.CurrentThread.Name);
for (int i = 0; i < 10; i++)
{
Thread.Sleep(new Random().Next(5));
Console.Write(" {0},", i);
}
Console.WriteLine();
}
}
}
在编译该程序之后,这次它产生了如下所示的期望结果。在这里,每个线程都有足够的机会完成其任务。
监视器
lock语句由编译器解析为使用Monitor类。Monitor类几乎类似于锁,但它的优点是比lock语句更好地控制。可以显式地指示锁的进入和退出,如下面的代码所示。
object tLock = new object();
public void Calculate()
{
Monitor.Enter(tLock);
try
{
for (int i = 0; i < 10; i++)
{
Thread.Sleep(new Random().Next(5));
Console.Write(" {0},", i);
}
}
catch{}
finally
{
Monitor.Exit(tLock);
}
Console.WriteLine();
}
实际上,如果观察使用lock语句的任何应用程序的IL代码,您将在其中找到Monitor类引用,如下所示:
使用[Synchronization]属性
[Synchronization]属性是System.Runtime.Remoting.Context命名空间的成员。为了线程安全,这个类级属性有效地锁定了对象的所有实例。
using System.Threading;
using System.Runtime.Remoting.Contexts;
[Synchronization]
public class Test:ContextBoundObject
{
public void Calculate()
{
for (int i = 0; i < 10; i++)
{
Thread.Sleep(new Random().Next(5));
Console.Write(" {0},", i);
}
Console.WriteLine();
}
}
互斥锁
Mutex代表互斥锁,它提供跨多个线程的同步。互斥锁是从WaitHandle派生而来的,您可以执行WaitOne()来获取互斥锁,并成为互斥锁的所有者。通过调用ReleaseMutex()方法释放互斥体,如下所示:
using System;
using System.Threading;
namespace threading
{
class Program
{
private static Mutex mutex = new Mutex();
static void Main(string[] args)
{
for (int i = 0; i < 4; i++)
{
Thread t = new Thread(new ThreadStart(MutexDemo));
t.Name = string.Format("Thread {0} :", i+1);
t.Start();
}
Console.ReadKey();
}
static void MutexDemo()
{
try
{
mutex.WaitOne(); // Wait until it is safe to enter.
Console.WriteLine("{0} has entered in the Domain", Thread.CurrentThread.Name);
Thread.Sleep(1000); // Wait until it is safe to enter.
Console.WriteLine("{0} is leaving the Domain\r\n", Thread.CurrentThread.Name);
}
finally
{
mutex.ReleaseMutex();
}
}
}
}
编译运行此程序后,它会显示每个新线程何时首次进入其应用程序域。一旦完成任务,它就会被释放,第二个线程就会启动,依此类推。
信号量
信号量与互斥锁非常相似,但信号量可以由多个线程同时使用,而互斥锁则不能。对于信号量,可以定义允许多少线程同时访问信号量屏蔽的资源。
在下面的示例中,创建了5个线程和2个信号量。在信号量类的构造函数中,您可以定义可以使用信号量获取的锁的数量。
using System;
using System.Threading;
namespace threading
{
class Program
{
static Semaphore obj = new Semaphore(2, 4);
static void Main(string[] args)
{
for (int i = 1; i <= 5; i++)
{
new Thread(SempStart).Start(i);
}
Console.ReadKey();
}
static void SempStart(object id)
{
Console.WriteLine(id + "-->>Wants to Get Enter");
try
{
obj.WaitOne();
Console.WriteLine(" Success: " + id + " is in!");
Thread.Sleep(2000);
Console.WriteLine(id + "<<-- is Evacuating");
}
finally
{
obj.Release();
}
}
}
}
在我们运行此应用程序时,会立即创建2个信号量,其他信号量等待,因为我们创建了5个线程。所以3个处于等待状态。任何一个线程释放一个信号量,其余的获得一个接一个地创建