很多事情都是在同时进行,在C#中为了模拟这种状态,引入了线程机制。简单说,当程序同时完成多件事情的时候,就是所谓的多线程程序。多线程应用广泛,开发人员可以使用多线程对要执行的操作分段执行,这样可以大大提高程序的运行速度和性能。
1、线程简介
在Windows操作系统中,每个正在运行的应用程序都是一个进程。一个进程可以包括一个或者多个线程。线程是进程中可以并行执行的程序段,它可以独立占用处理器时间片,同一个进程中的线程可以共用进程分配的资源和控件。多线程的应用程序可以在同一时刻处理多项任务。
(1)单线程简介
单线程:只有一个线程。默认情况下,系统为应用程序分配一个主线程,该线程执行程序中的Main方法开始和结束代码。
注意:在以上代码中,Application类的Run方法用于在当前线程上开始运行标准应用程序,并且使指定窗口可见。
(2)多线程简介
问题提出:一般情况下,需要用户进行交互的软件都必须尽可能快的对用户的活动做出反映,一遍提供丰富的用户体验。但是同时又必须执行必要的计算以便尽可能快地将数据呈献给用户,这个时候就需要多线程来进行实现。
I.多线程的优点
要提高对用户响应速度并且处理所需数据以便几乎同时完成工作。使用多线程是一种最为强大的技术,在具有一个处理器的计算机上,多线程可以通过利用用户事件之间很小的事件段在后台处理数据来表达这种效果。
单个应用程序域可以使用多线程来完成以下任务:
通过网络和Web服务器和数据库进行通信。
执行占用大量时间的操作。
区分具有不同优先级的任务。
使用户界面可以在将时间分配给后台任务时仍然能够快速做出相应。
II.多线程的缺点
使用多线程有好处,也有坏处,建议一般不要在程序中使用太多的线程,这样可以最大限度的减少操作系统资源的使用,并且可以提高性能。
使用多线程会产生的问题:
系统将为进程、AppDomain对象和线程所需的上下文信息使用内存。因此,可以创建的进程、AppDomain对象和线程的数目会受到可用内存的限制。
跟踪大量的线程将会占用大量的处理器时间。如果线程过多,则其中大多数线程都不会产生明显的进度。如果大多数当前线程处于一个进程中,则其他进程中的线程的调度频率就会很低。
使用许多线程控制代码执行非常复杂,会产生很多bug。
销毁线程需要了解可能发生的问题并且对这些问题进行处理。
2、线程的基本使用
C#对象对线程进行操作的时候,主要用到了Thread类,该类位于System.Threading命名空间下。通过使用Tread类,可以对线程进行创建、暂停、回复、休眠、终止以及设置优先权的操作。还可以通过使用Monitor类、Mutex类和lock关键字控制线程间的同步执行。
(1)Thread类
Thread类,该类位于System.Threading命名空间下。
System.Threading命名空间包含的东西:同步线程活动和访问数据的类(Mutex、Monitor、Interlocked和池)和一个Timer类(在线程池线程上执行回调方法)。
Thread类的功能:创建并且控制线程、设置线程优先级并且获取其状态。一个进程可以创建一个或多个线程以该进程关联的部分程序代码,线程执行的程序代码由ThreadStart委托或ParameterizedThreadStart委托指定。
线程运行期间,不同的时刻会表现为不同的状态,但是它总是处于由ThreadStart定义的一个或者多个状态中。用户可以通过使用ThreadPriority枚举为线程定义优先级,但不能保证操作系统会接受该优先级。
Tread类的常用属性及说明
CurrentTread:获取当前正在运行的线程。
IsAlive:获取一个值,该值指示当前线程的执行状态。
Name:获取或设置线程的名称。
Priority:获取或设置一个值,该值指示线程的调度优先级。
TreadState:获取一个值,该值包含当前线程的状态。
方法:
About:在调用此方法在线程上引发ThreadAboutException,以开始终止此线程的过程。调用此方法通常会终止线程。
Join:阻止调用线程,知道某个线程终止为止。
ResetAbout:取消为当前线程请求的About。
Resume:继续已挂起线程。
Sleep:将当前线程阻止指定的毫秒数。
Start:是线程被安排进行执行。
Suspent:挂起线程,或者如果线程已挂起,则不起作用。
示例程序:运行一个子线程并且获得该线程的相关信息。
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
string strInfo = string.Empty; //定义一个字符串,用来记录线程相关信息
Thread t = new Thread(new ThreadStart(TreadFunction));
//创建Thread类的对象,生成一个子线程
t.Start();
//获取线程相关信息
strInfo = "新建子线程唯一标识符:" + t.ManagedThreadId;
strInfo += "\n新建子线程名称" + t.Name;
strInfo += "\n新建子线程状态" + t.ThreadState.ToString();
strInfo += "\n新建子线程优先级" + t.Priority.ToString();
strInfo += "\n新建子线程是否为后台程序" + t.IsBackground;
//使主线程休眠0.5秒钟
Thread.Sleep(500);
t.Abort("新建子推出");//临时阻止新建子线程
t.Join();//等待新建的线程结束
MessageBox.Show("新建子线程执行结束","新建子线程");
richTextBox1.Text = strInfo;
//显示线程信息
}
public void TreadFunction()//线程执行的方法
{
MessageBox.Show("新建子线程开始执行","新建子线程");
//弹出提示框
}
}
注意:要有相应的System引用。
(2)创建线程
创建一个线程非常简单,只需要将其声明并为其提供线程起始点处的方法委托即可。创建新的线程的时候,需要使用Tread类,Tread类具有接受一个ThreadStart委托或ParameterizedTreadStart委托的构造函数,该委托包装了调用Start方法时由新线程调用的方法。创建了Thread类的对象之后,线程对象已存在并且已配置,但并没有创建实际的线程,这时,只有在调用Start方法之后,才会创建实际的线程。
Start方法用来使线程被安排进行执行,他有两种重载形式:
I.导致操作系统将当前实例的状态更改为ThreadState.Running。
public void Start()
II.使得操作系统将当前实例的状态更改为ThreadState.Running。并且选择提供包含线程执行的方法要使用的数据的对象。
public void Start(Object parameter)
//parameter表示一个对象,包含线程执行的方法要用的数据。
注意:如果线程已经终止,就无法再次调用Start进行重新启动。
示例程序:
class Program
{
public static void ThreadFunction()//线程的执行方法
{
Console.Write("创建一个线程,并且当前线程已经启动");
}
static void Main(string[] args)
{
Thread t = new Thread(new ThreadStart(ThreadFunction));
//用线程起始点的ThreadStart委托创建该线程的实例
t.Start();
}
}
注意:线程的入口是不带任何参数的。
(3)线程的挂起和恢复
创建一个线程并且启动之后,还可以挂起、恢复、休眠或者终止它。
线程的挂起和回复分别可以通过调用Thread类中的Suspend方法和Resume方法实现,下面对这两个方法进行详细介绍:
Suspend方法:
Suspend方法用于挂起线程,如果线程已经挂起,则不起作用。
public void Suspend()
注意:调用Suspend方法挂起线程的时候,.NET允许要挂起的线程再执行几个指令,目的是为了到达.NET认为线程可以安全挂起的状态。
class Program
{
public static void ThreadFunction()
{
Console.Write("创建一个线程,然后会被挂起");
}
static void Main(string[] args)
{
Thread t = new Thread(new ThreadStart(ThreadFunction));
//用线程起始点的ThreadStart委托创建该线程的实例
t.Start();
if (t.ThreadState == ThreadState.Running)
{
t.Suspend();//挂起线程
t.Resume();//回复挂起线程
}
else
{
Console.WriteLine(t.ThreadState.ToString());
}
}
}
(4)线程休眠
线程休眠主要通过Thread类的Sleep方法实现,该方法用来将当前线程阻止指定时间,它有两种重载形式,下面分别进行介绍。
I. 将当前线程挂起指定时间
public static void Sleep(int millisecondTimeout)
//millisecondTimeout表示线程被阻止的毫秒数,指定0以指示应挂起此线程以使得使得其他等待线程能够执行,指定Infinite以无限期阻止线程。
public static void Sleep(TimeSpan timeout)
//timeout:线程被阻止的时间量的TimeSpan,指定0以指示应该乖巧此线程以使得其他等待线程能够执行,指定Infinite以无限期阻止线程。
(5)终止线程
终止线程可以分别使用Thread类的About方法和Join方法实现,下面对这两个方法进行详细的介绍。
I.About方法
使用About方法来终止线程有两种重载形式:
public void About()
//终止线程,在调用此方法的线程上引发ThreadAboutException异常,以开始终止此线程的过程。
public void About(Object stateInfo)
//终止线程,在调用此方法的线程上引发ThreadAboutException异常,以开始终止此线程并且提供有关线程终止的异常信息的过程。
//stateInfo:表示一个对象,包含应用程序特定的信息(如状态),该信息可供正被终止的线程使用
示例程序:
class Program
{
public static void ThreadFunction()
{
Console.Write("创建一个线程,然后会被终止");
}
static void Main(string[] args)
{
Thread t = new Thread(new ThreadStart(ThreadFunction));
//用线程起始点的ThreadStart委托创建该线程的实例
t.Start();
t.Abort();
//使用About方法终止了新创建的线程,控制台窗口看不到任何输出信息。
}
}
注意:线程的About方法用于永久地停止托管线程。调用About方法时,公共语言运行库在目前线程中引发ThreadAboutException异常,目标线程可捕捉此异常。一旦线程被终止,将无法再次终止。
II.Join方法
Join方法用来阻止调用线程,知道某个线程终止为止。有三种重载形式:
在继续执行标准的COM和SendMessage消息处理期间阻止调用线程,直到某个线程终止为止。
public void Join()
在继续执行标准的COM和SendMessage消息处理期间阻止调用线程,直到某个线程终止或者经过了指定时间为止。
public bool Join(int millisecondsTimeout)
// millisecondsTimeout表示等待线程终止的毫秒数。如果线程已终止,则返回值为true,如果线程在经过了参数指定的毫秒数后为终止,则返回值为false。
在继续执行标准的COM和SendMessage消息处理期间阻止调用线程,直到某个线程终止或者经过了指定时间为止。
public bool Join(TimeSpan timeout)
//参数timeout表示等待线程终止的时间量的TimeSpan.如果线程已终止,则返回值为true,如果线程在经过了参数指定的时间量后为终止,则返回值为false。
示例程序:
class Program
{
public static void ThreadFunction()
{
Console.Write("创建一个线程,然后会被终止");
}
static void Main(string[] args)
{
Thread t = new Thread(new ThreadStart(ThreadFunction));
//用线程起始点的ThreadStart委托创建该线程的实例
t.Start();
t.Join();
}
}
(6)线程的优先级
线程优先级指定一个线程相对与另一个线程的相对优先级。每个线程都有一个分配的优先级。在公共语言运行库内创建的线程最初被分配为Normal级别,然而在公共语言运行库外创建的线程,在进入公共语言运行库时将保留其先前的优先级。
线程是根据其优先级的调度执行的。
用于确定线程执行顺序的调度算法随操作系统的不同而不同。
在某些操作系统下,具有最高优先级(相对于可执行线程而言)的线程经过调度后总是首先执行。
如果具有相同优先级的多个线程都可用,则程序将遍历处于该优先级的线程,并为每个线程提供一个固定的时间片来执行。只要具有较高优先级的线程可以运行,较低优先级的线程就不会运行。
如果在给定优先级上不再有可运行线程,则程序将会移到下一个较低优先级并在该优先级上调度线程以执行。如果具有较高优先级的线程可以运行,则具有较低优先级的线程将被抢先,并允许具有较高优先级的线程再次执行。
注意:线程的优先级不影响该线程的状态,该线程的状态在操作系统可以调度该线程之前必须为Running.
线程的优先级值以及说明:
Highest:可以将Thread安排在具有任何其他优先级的线程之前。
AboveNormal:可以将Thread安排在具有Highest优先级的线程之后,在具有Normal优先级的线程之前。
Normal:默认情况下的线程优先级。
BelowNormal:可以将Thread安排在具有Normal优先级的线程之后,在具有Normal优先级的线程之前。
Lowing:可以将Thread安排在任何其他优先级的线程之后。
注意:开发人员可以通过访问线程的Priority属性来获取和设置其优先级。Priority属性用来获取或设置一个值,该值指示线程的调度优先级。
public ThreadPriority Prority{get;set;}
//属性值为ThreadPriority类型的枚举值之一,默认为Normal。
class Program
{
public static void ThreadFunction1()
{
Console.Write("创建一个线程1");
}
public static void ThreadFunction2()
{
Console.Write("创建一个线程2");
}
static void Main(string[] args)
{
Thread t1 = new Thread(new ThreadStart(ThreadFunction1));
//用线程起始点的ThreadStart委托创建该线程的实例
Thread t2 = new Thread(new ThreadStart(ThreadFunction2));
t2.Priority = ThreadPriority.Highest;
t2.Start();
t1.Start();
}
}
(7)线程同步
在单线程程序中,每次只能做一件事情,后面的事情需要等待前面的事情完成之后才能进行。但是如果多线程程序,就会发生两个线程抢占资源的问题。C#提供线程同步机制来纺织资源访问的冲突。C#提供线程同步机制,来纺织资源访问的冲突。
线程同步机制是指可以并发线程高效有序的访问共享资源所采用的技术。同步:是指某一时刻只有一个线程可以访问资源,只有当资源所有者主动放弃了代码或资源的所有权的时候,其他线程才可以访问资源。线程同步技术主要用到lock关键字、Monitor类、Mutex类。
I.使用lock关键字实现线程同步
lock关键字可以用来确保代码完成运行,而不会被其他线程中断,他是通过在代码块运行期间为给定对象获取互斥锁来实现的。
lock语句用关键字lock开头,有一个作为参数的对象,在该参数的后面还有一个一次只能有一个线程执行的代码块。
Object thisLock=new Object();
lock(thisLock)
{
//要运行的代码块
}
注意:提供给lock语句的参数必须是基于引用类型的对象,该对象用来定义锁的范围。严格来说,提供给lock语句的参数只是用来唯一标识由多个线程共享的资源,所以它可以是任意类实例,然而,实际上,此参数通常标识需要进行线程同步的资源。
示例程序:
class TestLock
{
private Object obj = new Object();
private int i = 0;
public void testRun()
{
lock (obj)
{
Console.WriteLine("i的初始值为" + i);
Thread.Sleep(1000);
i++;
Console.WriteLine("i在自增之后的值:" + i);
}
}
}
class Program
{
static void Main(string[] args)
{
TestLock T1 = new TestLock();
for (int i = 0; i < 3; i++)
{
Thread th = new Thread(T1.testRun);
th.Start();
}
Console.Read();
}
}
II.使用Monitor类实现线程同步
Monitor类提供了同步对对象的访问机制,它通过向单个线程授予对象锁来控制对对象的访问,对象锁提供限制访问代码块(通常称为临界区)的能力。当一个线程拥有对象锁时,其他任何线程都不能获取该锁。
Monitor类的常用方法以及说明:
Enter:在指定对象上获取排他锁。
Exit:释放指定对象上的排它锁。
Wait:释放对象上的锁并阻止当前线程,直到他重新获取该值。
注意:使用Monitor类锁定的是对象(引用类型)而不是值类型。
示例程序:
class Program
{
static void Main(string[] args)
{
TestMonitor tm = new TestMonitor();
for (int i = 0; i < 3; i++)
{
Thread th = new Thread(tm.TestRun);
th.Start();
}
Console.Read();
}
}
class TestMonitor//线程要访问的公共资源类
{
private Object obj = new object();//定义同步对象
private int i = 0;
public void TestRun()//线程的绑定方法
{
Monitor.Enter(obj);//同步对象上获得排它锁
Console.WriteLine("the value of i is: "+i);
Thread.Sleep(1000);
i++;
Console.WriteLine("i after addition: "+i.ToString());
Monitor.Exit(obj);//释放同步对象上的排它锁
}
}
III.使用Mutex类实现线程同步
当两个或者更多线程需要同时访问一个共享资源时,系统需要使用同步机制来确保一次只有一个线程使用该资源。Mutex类是同步基元,他只向一个线程授予对共享资源的独占访问权。如果一个线程获得了互斥体,则要获取该互斥体的第二个线程将被挂起,知道第一个线程释放了该互斥体。Mutex类和监督器类似,防止多个线程在某一时间同时执行某个代码块,然而和监视器不同的是,Mutex类可以用来使跨进程的线程同步。
可以使用Mutex类的WaitOne方法请求互斥体的所属权,拥有互斥体的线程可以在对WaitOne方法的重复调用中请求相同的互斥体而不会阻止其执行,但线程必须调用同样多次数的Mutex类的ReleaseMutex方法来释放互斥体的所属权。Mutex类强制线程表示,因此互斥体只能由获得它的线程释放。
Mutex类的方法:
Close:在派生类被重写的时候,释放由当前WaitHandle持有的所有资源。
ReleaseMutex:释放Mutex类一次。
WaitOne:当在派生类中重写时,阻止当前线程,直到当前的WaitHandle收到信号。
class Program
{
static void Main(string[] args)
{
TestMutex tm = new TestMutex();
for (int i = 0; i < 3; i++)
{
Thread th = new Thread(tm.TestRun);
th.Start();
}
Console.Read();
}
}
class TestMutex//线程要访问的公共资源类
{
Mutex m = new Mutex(false);
private int i = 0;
public void TestRun()//线程的绑定方法
{
while (true)
{
if (m.WaitOne())//阻止线程,等待WaiteHandle收到信息
{
break;
}
}
Console.WriteLine("the value of i is: "+i);
Thread.Sleep(1000);
i++;
Console.WriteLine("i after addition: "+i.ToString());
m.ReleaseMutex();//执行完毕,释放资源
}
}