C#学习之旅
文章目录
1、线程的使用
在Windows应用程序中常常需要执行长时间运行的操作,例如一个算术复杂的运算符,这时,操作的执行速度就显得非常重要,开发人员可以使用线程对要执行的操作分段执行,这样就可以大大提高程序的运行速度和性能。
1.1、线程简介
1.1.1、单线程
单线程顾名思义,就是只有一个线程。默认情况下,系统为应用程序分配一个主线程,该线程执行程序以Main()方法开始和结束的代码。
实例如下:
新建一个Windows应用程序,程序会在Program.cs文件中自动生成一个Main()方法,该方法就是主线程的启动入口点。Main()方法代码如下:
[STAThread]
static void Main()
{
Application.EnableVisualStyles(); //启动应用程序的可是样式
Application.SetCompatibleTextRenderingDefault(false); //新控件使用GDI+
Application.Run(new Form1());
}
说明:
在以上代码中,Application类的Run()方法主要用于设置当前项目的主窗体,这里设置的是Form1。
1.1.2、多线程简介
一般情况下,需要用户交互的软件都必须尽可能快的对用户的活动做出反应,以便提供丰富多彩的用户体验,但同时它又必须执行必要的计算以便尽可能快地将数据呈现给用户,这时可以使用多线程来实现。
1、多线程的优点
要提高对用户的响应速度并且处理所需数据以便几乎同时完成工作,使用多线程是一种最为强大的技术,在具有一个处理器的计算机上,多线程可以通过利用用户事件之间很小的时间段在后台处理数据来达到这种效果。例如,通过使用多线程,在另一个线程正在重新计算同一应用程序中的电子表格的其他部分时,用户可以编辑该电子表格。
单个应用程序域可以使用多线程来完成以下任务
1、通过网络与Web服务器和数据库进行通信
2、执行占用大量时间的操作
3、区分具有不同优先级的任务
4、使用户界面可以在将时间分配给后台任务时仍能快速做出响应
2、多线程的优点
使用多线程有好处,同时也有坏处,建议一般不要在程序中使用太多的线程,这样可以最大限度地减少操作系统资源的使用,并可提高性能。
如果在程序中使用了多线程,可能会产生如下问题。
1、系统将为进程、AppDomain对象和线程所需的上下文信息使用内存。因此,可以创建的进程、AppDomain对象和线程的数目会受到可用内存的限制。
2、跟踪大量的进程将占用大量的处理器时间。如果线程过多,则其中大多数线程都不会产生明显的进度。如果大多数当前线程处于一个进程中,则其他进程中的线程的调度频率就会很低。
3、使用许多线程控制代码执行非常复杂,并可能产生许多Bug。
4、销毁线程需要了解可能发生的问题并对那些问题进行处理。
1.2、线程的基本操作
1.2.1、Thread类
Thread类位于System.Threading命名空间下,System.Threading命名空间提供一些可以进行多线程编程的类和接口。除同步线程活动和访问数据的类(Mutex、Monitor、Interlocked和AutoResetEvent等)外,该命名空间还包含一个ThreadPool类(它允许用户使用系统提供的线程池)和一个Timer类(它在线程池的线程上执行回调方法)。
Thread类主要用于创建并控制线程,设置线程优先级并获取其状态。一个进程可以创建一个或多个线程以执行与该进程关联的部分程序代码,线程执行的程序代码由ThreadStart委托或ParameterizedThreadStart委托指定。
线程运行期间,不同的时刻会表现为不同的状态,但它总是处于由ThreadState定义的一个或多个状态中。用户可以通过使用ThreadPriority枚举为线程定义优先级,但不能保证操作系统会接受该优先级。
实例如下:
namespace Test01
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
string strInfo = string.Empty; //定义一个字符串,用来记录线程相关信息
Thread myThread = new Thread(new ThreadStart(threadOut)); //实例化Thread线程类对象
myThread.Start(); //启动新的线程
//获取线程相关信息
strInfo = "线程唯一标识符:" + myThread.ManagedThreadId;
strInfo += "\n线程名称:" + myThread.Name;
strInfo += "\n线程状态:" + myThread.ThreadState.ToString();
strInfo += "\n线程优先级:" + myThread.Priority.ToString();
strInfo += "\n是否为后台线程:" + myThread.IsBackground;
Thread.Sleep(1000); //使主线程休眠1秒钟
myThread.Abort("退出"); //通过主线程阻止新开线程
myThread.Join(); //等待新开的线程结束
MessageBox.Show("线程运行结束");
richTextBox1.Text = strInfo;
}
public void threadOut()
{
MessageBox.Show("新线程开始运行");
}
}
}
注意:
在程序中使用线程时,需要在命名空间区域添加using System.Threading命名空间。
1.2.2、创建线程
创建一个线程非常简单,只需将其声明并为其提供线程起点处的方法委托即可。创建新的线程时,需要使用Thread类,Thread类具有接受一个ThreadStart委托或ParameterizedThreadStart委托的构造函数,该委托包装了调用Start()方法时由新线程调用的方法。创建了Thread类的对象之后,线程对象已存在并进行配置,但并为创建实际的线程,这时,只有在调用Start()方法后,才会创建实际的线程。
Start()方法用来使线程被安装执行,它有两种重载形式,下面分别介绍。
(1)、导致操作系统将当前实例的状态更改为ThreadState.Running,语法如下:
public void Start()
(2)、使操作系统将当前实例的状态更改为ThreadState.Running,并选择提供包含线程执行的方法要使用的数据的对象,语法如下:
public void Start(Object parameter)
parameter:一个对象,包含线程执行的方法要使用的数据。
注意:
如果线程已经终止,就无法通过再次调用Start()方法来重新启动。
实例如下:
namespace Test02
{
class Program
{
static void Main(string[] args)
{
Thread myThread; //声明线程
//用线程起始点的ThreadStart委托创建该线程的实例
myThread = new Thread(new ThreadStart(createThread));
myThread.Start(); //启动线程
}
public static void createThread()
{
Console.Write("创建线程");
}
}
}
注意:
线程的入口(本例中为createThread)不带任何参数。
1.2.3、线程的挂起与修复
创建完一个线程并启动之后,还可以挂起、修复、休眠或终止它。
线程的挂起与修复分别可以通过调用Thread类中的Suspend()方法和Resume()方法实现,下面对这两个方法进行详细介绍。
1、Suspend()方法
Suspend()方法用来挂起线程,如果线程已挂起,则不起作用,语法如下。
public void Suspend()
说明:
调用Suspend()方法挂起线程时,.NET允许要挂起的线程再执行几个指令,目的是为了到达.NET认为线程可以安全挂起的状态。
2、Resume()方法
Resume()方法用来继续已挂起的线程,语法如下。
public void Suspend()
说明:
通过Resume()方法来修复被暂停的线程时,无论调用了多少次Suspend()方法,调用Resume()方法均会使另一个线程脱离挂起状态,并导致该线程继续执行。
实例如下:
namespace Test03
{
class Program
{
static void Main(string[] args)
{
Thread myThread; //声明线程
//用线程起始点的ThreadStart委托创建该线程的实例
myThread = new Thread(new ThreadStart(createThread));
myThread.Start(); //启动线程
myThread.Suspend(); //挂起线程
myThread.Resume(); //恢复挂起的线程
}
public static void createThread()
{
Console.Write("创建线程");
}
}
}
1.2.4、线程休眠
线程休眠主要通过Thread类的Sleep()方法实现,该方法用来将当前线程阻止指定时间,它有两种重载形式,下面分别进行介绍。
(1)、将当前线程挂起指定的时间,语法如下:
public static void Sleep(int millisecondsTimeout)
millisecondsTimeout:线程被阻止的毫秒数。指定零以指定应挂起此线程以使其他等待线程能够执行,指定Infinite以无限期阻止线程。
(2)、将当前线程阻止指定的时间,语法如下:
public static void Sleep(TimeSpan timeout)
timeout:线程被阻止的时间量的TimeSpan。指定零以指定应挂起此线程以使其他等待线程能够执行,指定Infinite以无限期阻止线程。
1.2.5、终止线程
终止线程可以分别使用Thread类的Abort()方法和Join()方法实现,下面对这两个方法进行详细介绍。
1、Abort()方法
Abort()方法用来终止线程,它有两种重载形式,下面分别介绍。
(1)、终止线程,在调用此方法的线程上引发ThreadAbortException异常,以开始终止此线程的过程,语法如下:
public void Abort()
(2)、终止线程,在调用此方法的线程上引发ThreadAbortException异常,以开始终止此线程并提供有关线程终止异常信息的过程,语法如下:
public void Abort(Object stateInfo)
stateInfo:一个对象,它包含应用程序特定的信息(如状态),该信息可供正被终止的线程使用。
实例如下:
namespace Test04
{
class Program
{
static void Main(string[] args)
{
Thread myThread; //声明线程
//用线程起始点的ThreadStart委托创建该线程的实例
myThread = new Thread(new ThreadStart(createThread));
myThread.Start(); //启动线程
myThread.Abort(); //终止线程
}
public static void createThread()
{
Console.Write("线程实例");
}
}
}
注意:
线程的Abort()方法用于永久地停止托管线程。调用Abort()方法时,公共语言运行库在目标线程中引发ThreadAbortException异常,目标线程可捕捉此异常。一旦线程被终止,它将无法重新启动。
2、Join()方法
Join()方法用来阻止调用线程,直到某个线程终止为止,它有3种重载形式,下面分别介绍。
(1)、在继续执行标准的COM和SendMessage消息处理期间阻止调用线程,直到某个线程终止为止,语法如下:
public void Join()
(2)、在继续执行标准的COM和SendMessage消息处理期间阻止调用线程,直到某个线程终止或经过了指定时间为止,语法如下:
public bool Join(int millisecondsTimeout)
millisecondsTimeout:等待线程终止的毫秒数。
返回值:如果线程已终止,则为true;如果线程在经过了millisecondsTimeout参数指定的时间量后未终止,则为false;
(3)、在继续执行标准的COM和SendMessage消息处理期间阻止调用线程,直到某个线程终止或经过了指定时间为止,语法如下:
public bool Join(TimeSpan timeout)
timeout:等待线程终止的时间量的TimeSpan
返回值:如果线程已终止,则为true;如果线程在经过了timeout参数指定的时间量后未终止,则为false;
namespace Test05
{
class Program
{
static void Main(string[] args)
{
Thread myThread; //声明线程
//用线程起始点的ThreadStart委托创建该线程的实例
myThread = new Thread(new ThreadStart(createThread));
myThread.Start(); //启动线程
myThread.Join(); //阻止调用该线程,知道该线程终止
}
public static void createThread()
{
Console.Write("线程实例");
}
}
}
注意:
如果在应用程序中使用了多线程,辅助线程还没有执行完毕,在关闭窗体时必须关闭辅助线程,否则会引发异常。
1.2.6、线程的优先级
线程的优先级指定一个线程相对于另一个线程的相对优先级。每个线程都有一个分配的优先级。在公共语言运行库内创建的线程最初被分配为Normal优先级,而在公共语言运行库外创建的线程,在进入公共语言运行库时将保留先前的优先级。
线程是根据其优先级而调度执行的,用于确定线程执行顺序的调度算法随操作系统的不同而不同。在某些操作系统下,具有较高优先级(相对于可执行线程而言)的线程经过调度后总是首先运行。如果具有相同优先级的多个线程都可用,则程序将遍历处于该优先级的线程,并为每个线程提供一个固定的时间片段来执行。只要具有较高优先级的线程可以运行,具有较低优先级的线程就不会执行。如果在给定优先级上不在有可运行的线程,则程序将移到下一个较低的优先级并在该优先级加上调度线程执行;如果具有较高优先级的线程可以运行,则具有较低优先级的线程将被抢光,并允许具有较高优先级的线程再次执行。除此之外,当应用程序的用户界面在前台和后台之间移动时,操作系统还可以动态调整线程的优先级。
说明:
一个线程的优先级不影响该线程的状态,该线程的状态在操作系统可以调度该线程之前必须为Running。
开发人员可以通过访问线程的Priority属性来获取和设置其优先级。Priority属性用来获取或设置一个值,该值指示线程的调度优先级,其语法如下:
public ThreadPriority Priority{get; set;}
属性值:ThreadPriority值之一。默认值为Normal。
namespace Test06
{
class Program
{
static void Main(string[] args)
{
Thread thread1 = new Thread(new ThreadStart(Thread1)); //使用自定义方法Thread1声明线程
thread1.Priority = ThreadPriority.Lowest; //设置线程的调度优先级
Thread thread2 = new Thread(new ThreadStart(Thread2)); //使用自定义方法Thread2声明线程
thread1.Start(); //开启线程一
thread2.Start(); //开启线程二
Console.Read();
}
static void Thread1()
{
Console.WriteLine("线程一");
}
static void Thread2()
{
Console.WriteLine("线程二");
}
}
}
1.2.7、线程同步
在应用程序中使用多线程的一个好处是每个线程都可以异步执行。对于Windows应用程序,耗时的任务可以在后台执行,而使应用程序窗口和控件保持响应。对于服务器应用程序,多线程处理提供了用不同线程处理每个传入请求的能力;否则,在完全满足前一个请求之前,将无法处理每个新请求。然而,线程的异步意味着必须协调对资源(如文件、网络连接和内存)的访问;否则,两个或多个的线程可能在同一时间访问相同的资源,而每个线程都不知道其他线程的操作,结果将产生不可预知的数据损坏。
线程同步是指并发线程高效、有序地访问共享资源所采用的技术,所谓同步,是指某一时刻只有一个线程可以访问资源,只有当资源所有者主动放弃了代码或资源的所有权时,其他线程才可以使用这些资源。
线程同步可以分别使用C#中的lock关键字,Monitor类和Mutex类实现。
1、使用C#中的lock关键字实现线程同步
lock关键字可以用来确保代码块完成运行,而不会被其他线程中断,它是通过在代码块运行期间为给定对象获取互斥锁来实现的。
lock语句以关键字lock开头,它有一个作为参数的对象,在该参数的后面还有一个一次只能由一个线程执行的代码块。lock语句的语法格式如下。
Object thisLock = new Object();
lock(thisLock)
{
//要运行的代码块
}
提供给lock语句的参数必须为基于引用类型的对象,该对象用来定义锁的范围。严格地说,提供给lock语句的参数只是用来唯一标识由多个线程共享的资源。例如一个容器对象被多个线程使用,则可以将该容器传递给lock语句,而lock语句中的代码块将访问该容器。只要其他线程在访问该容器前先锁定该容器,则对该对象的访问将是安全同步的。
通常,最好避免锁定public类型或不受应用程序控制的对象实例。例如,如果该实例可以被公开访问,则lock(this)可能会有问题。因为不受控制的代码也可能会锁定该对象,这将可能导致死锁,即两个或多个线程等待释放同一个对象。处于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题,锁定字符串尤其危险。因为字符串被公共语言运行库(CLR)“暂留”,这意味着整个程序中任何给定的字符串都只有一个实例。因此,只要在应用程序进程中的任何具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。因此,最好锁定不会被暂留的私有或受保护成员。
说明:
事实上lock语句是用Monitor类来实现的,它等效与try/finally语句块,使用lock关键字通常比直接使用Monitor类更可取,一方面是因为lock更简洁;另一方面是因为lock确保了即使受保护的代码引发异常,也可以释放基础监视器。这是通过finally关键字来实现的,无论是否引发异常它都执行关联的代码块。
namespace Test07
{
class Program
{
static void Main(string[] args)
{
Program myProgram = new Program(); //实例化类对象
myProgram.LockThread(); //调用锁定线程方法
}
void LockThread()
{
lock (this) //锁定当前线程,以实现同步
{
Console.WriteLine("锁定线程以实现线程同步");
}
}
}
}
2、使用Monitor类实现线程同步
Monitor类提供了同步对象的访问机制,它通过向单个线程授予对象锁来控制对对象的访问,对象锁提供限制访问代码块(通常称为临界区)的能力。当一个线程拥有对象锁时,其他线程不能获取该锁。
Monitor类的主要功能如下。
(1)、它根据需要与某个对象相关联。
(2)、它是未绑定的,也就是说可以直接从上下文调用它
(3)、不能创建Monitor类的实例
注意:
使用Monitor类锁定的是对象(即引用类型)而不是值类型。
namespace Test08
{
class Program
{
static void Main(string[] args)
{
Program myProgram = new Program(); //实例化类对象
myProgram.LockThread(); //调用锁定线程方法
}
void LockThread()
{
Monitor.Enter(this); //锁定当前线程
Console.WriteLine("锁定线程以实现线程同步");
Monitor.Exit(this); //释放当前线程
}
}
}
说明:
从上例和本例上看,这两个例子实现的功能是相同的,但似乎使用lock关键字更简单一些,那为何还要使用Monitor类呢?因为使用Monitor类有更好的控制能力,例如,它可以使用Wait()方法指示活动的线程等待一段时间,当线程完成操作时,还可以使用Pulse()方法或PulseAll()方法通知等待中的线程。
3、使用Mutex类实现线程同步
当两个或更多线程需要同时访问一个共享资源时,系统需要使用同步机制来确保一次只有一个线程使用资源。Mutex类是同步基元,它只向一个线程授予对共享资源的独占访问权。如果一个线程获取了互斥体,则要获取该互斥体的第二个线程将被挂起,直到第一个线程释放该互斥体。Mutex类与监视器类似,它防止多个线程在某一时间同时执行某个代码块,然而与监视器不同的是,Mutex类可以用来使跨进程的线程同步。
可以使用WaitHandle.WaitOne方法请求互斥体的所属权,拥有互斥体的线程可以在对WaitOne()方法的重复调用中请求相同的互斥体而不会阻止其执行,但线程必须调用同样多次数的ReleaseMutex()方法以释放互斥体的所属权。Mutex类强制线程标识,因此互斥体只能由获得它的线程释放。
当用于进程间同步时,Mutex称为“命名Mutex”,因为它将用于另一个应用程序,因此它不能通过全局变量或静态变量共享。必须给它指定一个名称,才能使两个应用程序访问同一个Mutex对象。
使用Mutex类实现线程同步很简单,首先实例化一个Mutex类对象,它的构造函数中比较常用的有public Mutex(bool initallyOwned)。其中,参数initallyOwned指定了创建该对象的线程是否希望立即获得其所有权,当在一个资源得到保护的类中创建Mutex类对象时,常将该参数设置为false。然后在需要单线程访问的地方调用其等待方法,等待方法请求Mutex对象的所有权。这时,如果该所有权被另一个线程所拥有,则阻塞请求线程,并将其放入等待队列中,请求线程将保持阻塞,直到Mutex对象收到了其所有者线程发出将其释放的信号为止。所有者线程在终止时释放Mutex对象,或者调用ReleaseMutex()方法来释放Mutex对象。
说明:
尽管Mutex类可以用于进程内的线程同步,但是使用Monitor类通常更为可取,因为Monitor监视器是专门为.NET Framework而设计的,因而它可以更好的利用资源。相比之下,Mutex类时Win32构造的包装。尽管Mutex类比监视器更为强大,但是相对于Monitor类,它所需要的互操作转换更消耗计算机资源。
实例如下:
namespace Test09
{
class Program
{
static void Main(string[] args)
{
Program myProgram = new Program(); //实例化类对象
myProgram.LockThread(); //调用锁定线程方法
}
void LockThread()
{
Mutex myMutex = new Mutex(false); //实例化Mutex类对象
myMutex.WaitOne(); //阻止当前线程
Console.WriteLine("锁定线程以实现线程同步");
myMutex.ReleaseMutex(); //释放Mutex对象
}
}
}
属性 | 说明 |
---|---|
ApartmentState | 获取或设置此线程的单元状态 |
CurrentContext | 获取线程正在执行的当前上下文 |
CurrentThread | 获取当前正在运行的线程 |
IsAlive | 获取一个值,该值表示当前线程的执行状态 |
ManagedThreadId | 获取当前托管线程的唯一标识符 |
Name | 获取或设置线程的名称 |
Priority | 获取或设置一个值,该值指示线程的调度优先级 |
ThreadState | 获取一个值,该值包含当前线程的状态 |
方法 | 说明 |
---|---|
Abort | 在调用此方法的线程上引发ThreadAbortException,以开始终止此线程的过程。调用此方法通常会终止线程 |
GetApartMentState | 返回一个ApartmentState值,该值指示单元状态 |
GetDomain | 返回当前线程正在其中运行的当前域 |
Interput | 中断处于WaitSleepJoin线程状态的线程 |
Join | 阻塞调用线程,直到某个线程终止时为止 |
ResetAbort | 取消为当前线程请求的Abort |
Resume | 继续已挂起的线程 |
SetApartmentState | 在线程启动前设置其单元状态 |
Sleep | 将当前线程阻止指定的毫秒数 |
SpinWait | 导致线程等待由iterations参数定义的时间量 |
Start | 使线程被安排进行执行 |
Suspend | 挂起线程,或者如果线程已挂起,则不起作用 |
VolatileRead | 读取字段值。无论处理器的数目或处理器缓存的状态如何,该值都是由计算机的任何处理器写入的最新值 |
VolatileWrite | 立即向字段写入一个值,以使该值对计算机中的所有处理器都可见 |
优先级值 | 说明 |
---|---|
AboveNormal | 可以将Thread安排在具有Highest优先级的线程之后,在具有Normal优先级的线程之前 |
BelowNormal | 可以将Thread安排在具有Normal优先级的线程之后,在具有Lowest优先级的线程之前 |
Highest | 可以将Thread安排在具有任何其他优先级的线程之前 |
Lowest | 可以将Thread安排在具有任何其他优先级的线程之后 |
Normal | 可以将Thread安排在具有AboveNormal优先级的线程之后,在具有BelowNormal优先级的线程之前。默认情况下,线程具有Normal优先级。 |
方法 | 说明 |
---|---|
Enter | 在指定对象上获取排它锁 |
Exit | 释放指定对象上的排它锁 |
Pulse | 通知等待队列中的线程锁定对象态度的更改 |
PulseAll | 通知所有等待线程对象状态的更改 |
TryEnter | 试图获取指定对象的排他锁 |
Wait | 释放对象上的锁并阻止当前的线程,直到它重新获取该锁 |
方法 | 说明 |
---|---|
Close | 在派生类中被重写,释放由当前WaitHandle持有的所有资源 |
OpenExising | 打开现有的已命名的互斥体 |
ReleaseMutex | 释放Mutex一次 |
SignalAndWait | 原子操作的形式,向一个WaitHandle发出信号并等待另一个 |
WaitAll | 等待指定数组中的所有元素都受到信号 |
WaitAny | 等待指定数组中的任一元素收到信号 |
WaitOne | 当在派生类中重写时,阻止当前线程,直到当前的WaitHandle收到信号 |