每个正在操作系统中运行的应用程序都是一个进程,一个进程可以包括一个或多个线程。线程是操作系统分配处理器时间的基本单元。进程就好像是一个公司,公司中的每个员工就相当于线程,公司想要运行就必须得有负责人,负责人就相当于主线程。
单线程
单线程就是只有一个线程。默认情况下,系统为应用程序分配一个主线程,该线程执行程序中以Main方法开始和结束的代码。
多线程
需要用户交互的软件都必须尽可能的对用户的活动做出反应,以便提供更丰富的用户体验。但同时它又必须执行必要的计算,以便尽可能快的将数据呈现给用户,这时就要使用多线程。
优点:要提供对用户的响应速度并且处理所需数据,以便同时完成工作,使用多线程是一种强大的技术。多线程可以通过利用用户事件之间很小的时间段在后台处理数据来达到这种效果。
Thread类
Thread类位于System.Threading命名空间下,System.Threading命名空间提供一些可以进行多线程编程的类和接口。Thread类主要用于创建并控制线程、设置线程优先级并获取其状态。
属性 | 说明 |
ApartmentState | 获取或设置该线程的单元状态 |
CurrentContext | 获取线程正在其中执行的当前上下文 |
CurrentThread | 获取当前线程正在运行的线程 |
IsAlive | 获取一个值,该值指示当前线程的执行状态 |
ManagedThreadld | 获取当前托管线程的唯一标识符 |
Name | 获取或设置线程的名称 |
Priority | 获取或设置一个值,改制指示线程的调度优先级 |
ThreadState | 获取一个值,该值包含当前线程的状态 |
方法 | 说明 |
Abort | 在调用该方法的线程上引发ThreadAbortException,以开始终止该线程的过程。调用该方法通常会终止线程 |
GetApartmentState | 返回一个ApartmentState值,该值指示单元状态 |
GetDomain | 返回当前线程正在其中运行的当前域 |
GetDomainID | 返回唯一的应用程序标识符 |
Interrupt | 中断处于WaitSleepJoin线程状态的线程 |
Join | 阻止调用线程,直到某个线程终止时为止 |
ResetAbort | 取消为当前线程请求的Abort |
Resume | 继续已挂起的线程 |
SetpartmentState | 在线程启动前设置其单元状态 |
Sleep | 将当前线程阻止指定的毫秒数 |
SpainWait | 导致线程等待由iterations参数定义的时间量 |
Start | 使线程被安排进行执行 |
Suapent | 挂起线程,或者如果线程已挂起,则不起作用 |
VolatileRead | 读取字段值。无论处理器的数目或处理器缓存的状态如何,该值都是由计算机的任何处理器写入的最新值 |
VolatileWrite | 立即向字段写入一个值,一边该值对计算机中的所有处理器都可见 |
演示使用Thread类的相关方法:
namespace Thread3
{
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("主线程开始运行");
}
}
}
运行结果:
创建线程
创建一个线程非常简单,只需要将其声明并为其提供线程起始点处的委托即可。创建新的线程时,需要使用Thread类,该类具有接受一个ThreadStart委托或ParameterizedThreadStart委托的构造函数。
线程中为什么要使用委托ThreatStart:http://www.cnblogs.com/lvdongjie/p/5469274.html
ThreadStart:用于无返回值、无参数的方法
namespace Thread1
{
class Program
{
static void Main(string[] args)
{
//创建ThreadStart委托实例
ThreadStart TS = new ThreadStart(MyThread);
Console.WriteLine("Create the main thread");
//创建Thread类的实例
Thread thread = new Thread(TS);
thread.Start();
}
//线程函数
static void MyThread()
{
Console.WriteLine("Child start Thread...");
Thread.Sleep(1000);
Console.WriteLine("Thread is over...");
}
}
}
运行结果:
ParameterizedThreadStart:用于带参数的方法
namespace Thread1
{
class Program
{
static void Main(string[] args)
{
//创建线程委托实例
ParameterizedThreadStart pts = new ParameterizedThreadStart(MyThread);
Console.WriteLine(" Creating the Child thread");
//创建线程对象
Thread thread = new Thread(pts);
thread.Start(10);
Console.ReadKey();
}
private static void MyThread(object n)
{
Console.WriteLine("Thread started ...");
for(int i = 0;i <= (int)n; i+=2)
{
Console.WriteLine(i);
}
}
}
}
运行结果:
线程的挂起和恢复
创建完一个线程并启动后,还可以挂起、恢复、休眠或终止它。线程的挂起与恢复可以通过调用Thread类的Suspend方法和Resume方法实现。
- Suspend方法
public void Suspend();
该方法用来挂起线程,如果线程已挂起,则不起作用。调用Suspend方法挂起线程时,.NET允许挂起的线程再执行几个指令,目的是为了到达.NET认为线程可以安全挂起的状态。
- Resume方法
public void Resume();
该方法用来继续已挂起的线程。通过Resume方法来恢复被暂停的线程时,无论调用多少次Suspend方法,调用Resume方法均会使另一个线程脱离挂起状态。
namespace Thread1
{
class Program
{
static void Main(string[] args)
{
Thread myThread; //声明线程
//用线程起始点的ThreadStart委托创建该线程的实例
myThread = new Thread(new ThreadStart(createThread));
myThread.Start(); //启动线程
myThread.Suspend(); //挂起线程
myThread.Resume(); //恢复挂起的线程
}
private static void createThread()
{
Console.WriteLine("创建线程");
}
}
}
线程休眠
线程休眠主要通过Thread类的Sleep方法实现。该方法用来将线程阻止指定的时间。
namespace Thread4
{
class Program
{
static void Main(string[] args)
{
ThreadStart ts = new ThreadStart(ChildThread);//创建一个线程的委托
Console.WriteLine("创建子线程");
Thread thread = new Thread(ts);
thread.Start();
Console.ReadKey();
}
public static void ChildThread()
{
Console.WriteLine("子线程启动");
int SleepTime = 2000;
Console.WriteLine("子线程休眠{0}秒", SleepTime / 1000);
Thread.Sleep(SleepTime);
Console.WriteLine("子线程恢复");
}
}
}
运行结果:
终止线程
1. Abort方法
Abort方法用来终止线程,它有两种重载形式:
(1)终止线程,在调用该方法的线程上引发ThreadAbortException异常,以开始终止该线程: public void Abort();
(2)终止线程,在调用该方法的线程上引发ThreadAbortException异常,以开始终止该线程并提供有关线程终止的异常信息的过程:
public void Abort(Object stateInfo)
namespace Thread5
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("主线程启动");
//创建一个线程委托实例
ThreadStart ts = new ThreadStart(ChildThread);
Console.WriteLine("创建子线程");
Thread thread = new Thread(ts);//创建线程对象
thread.Start();//启动线程
Thread.Sleep(2000);//主线程休眠
Console.WriteLine("终止子线程");
thread.Abort();//线程终止
Console.ReadKey();
}
public static void ChildThread()
{
try
{
Console.WriteLine("子线程启动");
//打印20以内的偶数
for (int i = 0; i <= 20; i += 2)
{
Thread.Sleep(500);//线程休眠1s
Console.WriteLine(i);
}
Console.WriteLine("子线程完成");
}
catch
{
Console.WriteLine("线程中止异常");
}
finally
{
Console.WriteLine("捕获异常信息");
}
}
}
}
运行结果:
2. Join 方法
Join() | 在继续执行标准的 COM 和 SendMessage 消息泵处理期间,阻止调用线程,直到由该实例表示的线程终止。 |
Join(int 32) | 在继续执行标准的 COM 和 SendMessage 消息泵处理期间,阻止调用线程,直到由该实例表示的线程终止或经过了指定时间为止 |
Join(TimeSpan) | 在继续执行标准的 COM 和 SendMessage 消息泵处理期间,阻止调用线程,直到由该实例表示的线程终止或经过了指定时间为止。 |
Join():
public class Example
{
static Thread thread1, thread2;
public static void Main()
{
thread1 = new Thread(ThreadProc);
thread1.Name = "Thread1";
thread1.Start();
thread2 = new Thread(ThreadProc);
thread2.Name = "Thread2";
thread2.Start();
}
private static void ThreadProc()
{
Console.WriteLine("\nCurrent thread: {0}", Thread.CurrentThread.Name);
if (Thread.CurrentThread.Name == "Thread1" &&
thread2.ThreadState != ThreadState.Unstarted)
thread2.Join();
Thread.Sleep(4000);
Console.WriteLine("\nCurrent thread: {0}", Thread.CurrentThread.Name);
Console.WriteLine("Thread1: {0}", thread1.ThreadState);
Console.WriteLine("Thread2: {0}\n", thread2.ThreadState);
}
}
运行结果:
建议去官网看对join()方法的详细解释:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.thread.join?view=netframework-4.8
线程优先级
线程的优先级指定一个线程相对于另一个线程的相对优先级。每个线程都有一个分配的优先级。线程是根据其优先级而调度执行的。
优先级值 | 说明 |
AboveNormal | 可以将Thread安排在具有Highest优先级的线程之后,在具有Normal优先级的线程之前 |
BelowNormal | 可以将Thread安排在具有Normal优先级的线程之后,在具有Lowest优先级的线程之前 |
Highest | 可以将Thread安排在任何其他优先级的线程之前 |
Lowest | 可以将Thread安排在任何其他优先级的线程之后 |
Normal | 可以将Thread安排在具有AboveNormal优先级的线程之后,在具有Below Normal优先级的线程之前。默认情况下,线程具有Normal优先级 |
//线程优先级
namespace Thread2
{
class Program
{
static void Main(string[] args)
{
//因为下面要用到program类中的非静态函数,所以先创建该类对象
Program program = new Program();
//创建线程委托1
ThreadStart ts1 = new ThreadStart(program.even);
Console.WriteLine("In Main: Creating the thread1 thread.");
//创建线程1的实例
Thread thread1 = new Thread(ts1);
//设置打印偶数优先级为最低
thread1.Priority = ThreadPriority.Lowest;
//创建线程委托2
ThreadStart ts2 = new ThreadStart(program.odd);
Console.WriteLine("In Main: Creating the thread2 thread.");
//创建线程2的实例
Thread thread2 = new Thread(ts2);
//设置打印奇数优先级为最高
thread2.Priority = ThreadPriority.Highest;
thread1.Start();//偶数 低
thread2.Start();//奇数 高
Console.ReadKey();
}
//打印奇数
public void odd()
{
Console.WriteLine("List of odd numbers: ");
for (int i = 1; i < 100; i += 2)
{
Console.Write(i + " ");
}
Console.WriteLine();
}
//打印偶数
public void even()
{
Console.WriteLine("List of even numbers: ");
for (int i = 0; i < 100; i += 2)
{
Console.Write(i + " ");
}
Console.WriteLine();
}
}
}
运行结果:
第一次:
第二次:
第三次:
从上面的运行效果可以看出,由于输岀奇数的线程的优先级高于输出偶数的线程,所以在输出结果中优先输出奇数的次数会更多。
线程同步
lock:
lock关键字可以用来确保代码块完整运行,而不会被其他线程中断,它是通过在代码块运行期间为给定对象获取互斥锁来实现。
using System;
using System.Threading;
//线程优先级
namespace Thread2
{
class Program
{
static void Main(string[] args)
{
//因为下面要用到program类中的非静态函数,所以先创建该类对象
Program program = new Program();
//创建线程委托1
ThreadStart ts1 = new ThreadStart(program.even);
Console.WriteLine("In Main: Creating the thread1 thread.");
//创建线程1的实例
Thread thread1 = new Thread(ts1);
//设置打印偶数优先级为最低
thread1.Priority = ThreadPriority.Lowest;
//创建线程委托2
ThreadStart ts2 = new ThreadStart(program.odd);
Console.WriteLine("In Main: Creating the thread2 thread.");
//创建线程2的实例
Thread thread2 = new Thread(ts2);
//设置打印奇数优先级为最高
thread2.Priority = ThreadPriority.Highest;
thread1.Start();//偶数 低
thread2.Start();//奇数 高
Console.ReadKey();
}
//打印奇数
public void odd()
{
lock (this)
{
Console.WriteLine("List of odd numbers: ");
for (int i = 1; i < 100; i += 2)
{
Console.Write(i + " ");
}
Console.WriteLine();
}
}
//打印偶数
public void even()
{
lock (this)
{
Console.WriteLine("List of even numbers: ");
for (int i = 0; i < 100; i += 2)
{
Console.Write(i + " ");
}
Console.WriteLine();
}
}
}
}
运行结果:
Monitor:
Monitor类提供了同步对象的访问机制,他通过向单个线程授予对象锁来控制对象的访问,对象锁提供限制访问代码块的能力。当一个线程拥有对象锁时,其他线程都不能获取该锁。
Monitor类的主要功能:
- 它根据需要与某个对象相关联
- 他是未绑定的,可以直接从任何上下文调用它
- 不能创建Monitor类的实例
方法 | 说明 |
Enter | 在指定对象上获取排他锁 |
Exit | 释放指定对象上的排他锁 |
Pulse | 通知等待队列上的线程锁定对象状态的更改 |
PulseAll | 通过所有的等待线程对象状态的更改 |
TryEnter | 试图获取指定对象的排他锁 |
Wait | 释放对象上的锁并阻止当前线程,直到它重新获取该锁 |
namespace Thread4
{
class Program
{
static void Main(string[] args)
{
Program myProgarm = new Program();//实例化对象
myProgarm.LockThread(); //调用锁定线程方法
}
void LockThread()
{
Monitor.Enter(this); //锁定当前线程
Console.WriteLine("锁定当前线程以实现线程同步");
Monitor.Exit(this); //释放当前线程
}
}
}
Mutex:
当两个或更多线程同时访问一个共享资源时,系统需要使用同步机制来确保一次只有一个线程使用该资源。Mutex类是同步基元,他向一个线程授予对共享资源的独占访问权。如果一个线程获取了互斥体,则要获取该互斥体的第二个线程将被挂起,知道第一个线程释放该互斥体。
方法 | 说明 |
Close | 在派生类中被重写时,释放当前Waithandle持有的所有资源 |
OpenExisting | 打开现有的已命名互斥体 |
ReleaseMutex | 释放Mutex一次 |
SignalAndWait | 原子操作的形式,向一个WaitHandle发出信号并等待另一个 |
WaitAll | 等待指定数组中的所有元素都收到信号 |
WaitAny | 等待指定数组中的任一元素收到信号 |
WaitOne | 当在派生类中重写时,阻止当前线程,直到当前的WaitHandle收到信号 |
namespace Thread4
{
class Program
{
static void Main(string[] args)
{
Program myProgarm = new Program();//实例化对象
myProgarm.LockThread(); //调用锁定线程方法
}
void LockThread()
{
Mutex myMutex = new Mutex(false); //实例化mutex类对象
myMutex.WaitOne(); //阻止当前线程
Console.WriteLine("锁定线程以实现线程同步");
myMutex.ReleaseMutex(); //释放Mutex类
}
}
}
前台线程和后台线程
只有一个前台线程在运行,应用程序的进程就在运行,如果多个前台线程在运行,但是Main方法结束了,应用程序的进程仍然是运行的,直到所有的前台线程完成其任务为止。
在默认情况下,用Thread类创建的线程是前台线程。线程池中的线程总是后台线程。在用Thread类创建线程的时候,可以设置IsBackground属性,表示它是一个前台线程还是一个后台线程。
看下面例子中前台线程和后台线程的区别:
class Progrem{
static void Main(){
var t1 = new Thread(ThreadMain){IsBackground=false};
t1.Start();
Console.WriteLine("Main thread ending now.");
}
static void ThreadMain(){
Console.WriteLine("Thread +" + Thread.CurrentThread.Name + "started");
Thread.Sleep(3000);
Console.WriteLine("Thread +" + Thread.CurrentThread.Name + "started");
}
}
后台线程用的地方: 如果关闭Word应用程序,拼写检查器继续运行就没有意义了,在关闭应用程序
的时候,拼写检查线程就可以关闭。
线程池
创建线程需要时间。 如果有不同的小任务要完成,就可以事先创建许多线程 ,在应完成这些任务时发出请求。 这个线程数最好在需要更多的线程时增加,在需要释放资源时减少,不需要自己创建线程池,系统已经有一个ThreadPool类管理线程。
这个类会在需要时增减池中线程的线程数,直到达到最大的线程数。 池中的最大线程数是可配置的。
在双核 CPU中,默认设置为1023个工作线程和 1000个I/O线程。也可以指定在创建线程池时应立即启动的最小线程数,以及线程池中可用的最大线程数。 如果有更多的作业要处理,线程池中线程的个数也到了极限,最新的作业就要排队,且必须等待线程完成其任务。
线程池示例
static void Main(){
int nWorkerThreads;
int nCompletionThreads;
ThreadPool.GetMaxThreads(out nWorkerThreads,out nCompletionPortThreads);
Conslole.WriteLine("Max worker threads : "+ nWorkerThreads+
" I/O completion threads: "+ CompletionPortThreads);
for(int i = 0;i<5;i++){
ThreadPool.QueueUserWorkItem(JobForAThrea);
}
Thread.Sleep(3000);
}
static void JobForAThread(object state){
for(int i = 0;i < 3; i++){
Console.WriteLine("Loop"+ i +", running in pooled thread"
+Thread.CurrentThread.ManagedThreadld);
Thread.Sleep(50);
}
}
示例应用程序首先要读取工作线程和I/O线程的最大线程数,把这些信息写入控制台中。
接着在for循环中调用ThreadPool.QueueUserWorkItem方法,传递一个WaitCallBack类型的委托,把JobForThread方法赋予线程池中的线程。线程池收到这些请求后,就会从池中选择一个线程来调用该方法。
如果线程池还没有运行,就会创建一个线程池,并启动第一个线程。如果线程池已经在运行,且有一个空闲线程来完成该任务,就把作业传递给这个线程。
使用线程池需要注意的事项:
- 线程池中的所有线程都是后台线程。如果进程的所有前台线程都结束了,所有的后台线程就会停止。不能把入池的线程改为前台线程。
- 不能给入池的线程设置优先级或名称
- 入池的线程只能用于时间较短的任务。如果线程要一直运行就应使用Thread类创建一个线程。