1、多线程基础
1.1 基本概念
- 进程:进程是操作系统中的一个基本概念,进程包含了一个程序运行所需的资源,一个进程包含多个线程。
- 线程:线程是进程的基本执行单元,进程入口执行的第一个进程称为主线程。
- 任务:任务是一个工作单元,目的是生成结果值,或产生想要的效果。任务代表需要执行的一个作业,而线程是代表这个作业的工作者。
- 线程池:多个线程的集合,决定如何处理线程分配的逻辑。
1.2 多线程的性能问题
善用多线程可以提高程序的执行效率,例如要长时间处理数据时,可以将处理工作在其他线程中执行,避免主线程出现假死状态,也可以提高程序效率。然而,上下文切换是有代价的。必须将CPU当前的内部状态保存到内存,还必须加载与新线程关联的状态。如果线程太多,切换开销也会随之增加,反而会影响程序性能。
2、System.Threading.Thread
System.Threading.Thread是用于控制线程的基础类,通过Thread可以控制当前应用程序中线程的创建、挂起、停止、销毁。
2.1 常用属性
- CurrentContext——获取线程正在其中执行的当前上下文
- CurrentThread——获取当前正在运行的线程
- ExecutionContext——获取包含有关当前线程的各种上下文信息的对象
- IsAlive——获取一个值,该值指示当前线程的执行状态
- IsBackground——获取或设置一个值,该值指示某个线程是否为后台线程
- IsThreadPoolThread——获取一个值,该值指示线程是否属于托管线程
- ManagedThreadId——获取当前托管线程的唯一标识
- Priority——获取或设置一个值,该值指示线程的调度优先级
- ThreadState——获取一个值,该值包含当前线程的状态
2.2 常用方法
- Start()——执行本线程
- Join()——阻塞调用线程,直到某个线程终止为止,可以传递int或TimeSpan参数,设置阻塞线程的事件
- Suspend()——挂起当前线程,如果当前线程已属于挂起则不起作用
- Resume()——继续运行已挂起的线程,如果线程没有挂起则引发异常
- Sleep()——将正在运行的线程挂起,挂起事件可能超过指定的时间长度
- Abort()——终止线程,尽量不要终止线程
2.3 Thread类的使用
Thread提供初始化方法有两个重载方法,分别是传递ThreadStart委托和ParameterizedThreadStart委托。两者的区别是后者可以接收参数。当调用Thread对象的Start()后,便开始异步执行委托方法。
2.4 实例
在ShowMessage()方法中输出当前线程Id,并使用Sleep()方法模拟长时间的处理工作,在主线程中创建并开始执行异步执行。
class Program
{
static void Main()
{
Console.WriteLine("Main ThreadId is : {0}", Thread.CurrentThread.ManagedThreadId);
Thread thread = new Thread(ShowMessage);
thread.Start();
Console.WriteLine("Main thread working is Complete!");
}
private static void ShowMessage()
{
Console.WriteLine("ThreadId = {0}", Thread.CurrentThread.ManagedThreadId);
for (int i = 0; i < 10; i++)
{
Thread.Sleep(100);
Console.WriteLine("The Number is {0}", i.ToString());
}
}
使用ParameterizedThreadStart委托接收参数并输出
public class Person
{
public string Name {get; set;}
public string Age {get; set;}
}
class Program
{
static void Main()
{
Console.WriteLine("Main ThreadId is : {0}", Thread.CurrentThread.ManagedThreadId);
Thread thread = new Thread(new ParamterizedThreadStart(ShowMessage));
Person person = new Person(){ Name="msf", Age="30"};
thread.Start(person);
Console.WriteLine("Main thread working is Complete!");
}
object
{
Console.WriteLine("Name = {0}", (Person(person)).Name);
Console.WriteLine("Name = {0}", (Person(person)).Age);
for (int i = 0; i < 10; i++)
{
Thread.Sleep(100);
Console.WriteLine("The Number is {0}", i.ToString());
}
}
创建Thread对象时传递ParamterizedThreadStart委托,在调用Start()方法时传递参数,委托方法参数类型必须是object。
3、线程池
3.1 线程池的概念
ThreadStart和ParameterizedThreadStart建立线程简单,但难以管理。若建立的线程过多,不仅会占据更多内存,而且创建和销毁线程以及切换线程也会影响程序性能。
CLR线程池是一种更节省内存空间,效率也更高的线程管理模式。开发人员不直接分配线程,而是告诉线程池要做什么任务。线程池会自己创建线程,任务完成后该线程也不会销毁,而且以挂起的状态返回线程池,以备下次使用。这样节省了创建线程所造成的性能损耗,也可以使线程重复使用,从而节省内存开销。
线程池分为工作者线程和I/O线程两种,工作者线程主要用于管理CLR内部对象的运作,I/O线程则是用于与外部系统交互信息。
3.2 工作者线程
使用线程池工作者线程一般有两种方式:ThreadPool.QueueUserWorkItem()方法和委托。
3.2.1 QueueUserWorkItem()方法
ThreadPool提供两个QueueUserWorkItem()静态方法用于启动工作者线程。
- ThreadPool.QueueUserWorkItem(WaitCallback)
- ThreadPool.QueueUserWrokItem(WaitCallback, object)
WaitCallback委托指向一个带有object类型参数且没有返回值的方法,再调用QueueUserWorkItem方法启动线程。
实例:使用线程工作者线程
class Program
{
static void Main()
{
ThreadPool.SetMaxThread(1000, 1000);
ThreadPool.QueueUserWorkItem(ShowMessage, "Hello World!");
Console.WriteLine("Main thread working is Complete!");
}
private void ShowMessage(object data)
{
Console.WriteLine("The Message is {0}", data.ToString());
for (int i = 0; i < 10; i++)
{
Thread.Sleep(100);
Console.WriteLine("The Number is {0}", i.ToString());
}
}
}
通过ThreadPool.QueueUserWorkItem启动工作者线程虽然方便,但也有一定局限性。WaitCallback委托必须指向一个没有返回值且只有一个object类型参数的方法,若方法需要返回值,或需要多个参数,则难以实现。
3.2.2 委托启动工作者线程
当定义委托后,系统会自动创建一个代表委托的类,委托类包括以下几个重要方法:
- void Invoke()——调用该委托则所有方法都会被执行
- System.IAsyncResult BeginInvoke(System.AsyncCallback, System.Object)——启动线程池中的工作者线程,异步调用委托方法
- void EndInvoke(System.IAsyncResult)——结束线程后获取委托的返回值
实例:使用委托的方法实现多线程
class Program
{
delegate string MyDelegate(string Name, string Age);
static void Main()
{
MyDelegate myDel= new MyDelegate(ShowMessage);
IAsyncResult result = myDel.BeginInvoke("Msf", "30", null, null);
string data = myDel.EndInvoke(result);
}
static string ShowMessage(string Name, string Age)
{
Console.WriteLine("Name = {0}", Name);
Console.WriteLine("Age = {0}", Age);
for (int i = 0; i < 10; i++)
{
Thread.Sleep(100);
Console.WriteLine("The Number is {0}", i.ToString());
}
return "Hello " + Name;
}
}
3.2.3 IAsyncResult
执行BeginInvoke后立即调用EndInvoke会阻塞当前线程,直到异步线程完成任务后,主线程才能继续功能。这显然不是我们希望的结果。IAsyncResult提供几种方式可以提高主线程的效率。
IAsyncResult是BeginInvoke方法的返回值类型,IAsyncResult包括以下成员:
- object AsyncState——获取限定或包含有关异步操作的信息的对象
- WailHandle AsyncWaitHandle——获取等待异步操作完成的WaitHandle
- bool CompletedSynchronously——获取异步操作是否同步完成的指示
- bool IsCompleted——获取异步操作是否已完成的指示
可以通过轮询IsCompleted或AsyncWaitHandle等来判断异步操作是否完成
3.2.4 回调函数
轮询异步操作状态的方式不仅效率低,而且限制较多。所以多数情况下是使用回调函数的方式获取异步操作的返回值。
BeginInvoke方法的参数中,提供了一个AsyncCallback委托,该委托可以指定一个方法作为回调函数,该指定的回调函数必须是带有IAsyncResult且无返回值的方法。在BeginInvoke异步操作结束后,系统会调用AsyncCallback所指定的回调函数,在该回调函数中调用EndInvoke方法即可获取异步操作的返回值。BeginIvoke方法的最后一个参数用于传递外部数据,在回调方法中的IAsyncResult的AsyncState数据获取该参数。
class Program
{
delegate string MyDelegate(string Name, string Age);
static void Main()
{
MyDelegate myDel= new MyDelegate(ShowMessage);
IAsyncResult result = myDel.BeginInvoke("Msf", "30", Completed, null);
}
static string ShowMessage(string Name, string Age)
{
Console.WriteLine("Name = {0}", Name);
Console.WriteLine("Age = {0}", Age);
for (int i = 0; i < 10; i++)
{
Thread.Sleep(100);
Console.WriteLine("The Number is {0}", i.ToString());
}
return "Hello " + Name;
}
static void Completed(IAsyncResult result)
{
AsyncResult _result = (AsyncResult)result;
MyDelegate testDelegate = (MyDelegate)_result.AsyncDelegate;
string data = testDelegate.EndInvoke(result);
}
}
如果想为回调函数传递外部信息,可以通过BeginInvoke的最后一个object类型参数。在回调函数中使用AsyncResult.AsyncState就可以获取object对象。
static void Completed(IAsyncResult result)
{
AsyncResult _result = (AsyncResult)result;
MyDelegate testDelegate = (MyDelegate)_result.AsyncDelegate;
string data = testDelegate.EndInvoke(result);
// 获取参数
}
3.3 I/O线程
I/O线程是.NET访问外部资源所设置的线程。.NET为多个I/O操作创建了异步方法,例如:FileStream、TCP/IP、WebRequest、WebService等,而且每个异步方法的使用方式都非常相似,都是以BeginXXX为开始,以endXXX为结束。
3.3.1 异步读写FileStream
如果要使用FileStream的异步I/O方法,则必须使用以下构造函数创建FileStream对象,并将userAsync参数设置为true。
FileStream stream = new FileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, bool userAsync);
- path:文件的相对路径或绝对路径
- mode:确定打开或创建文件
- access:确定访问文件的方式
- share:确定文件的进程共享方式
- bufferSize:确定文件缓冲区的大小,默认最小值为8。启动异步线程时,文件大小一般大于缓冲区大小
- userAsync:确定是否启动异步I/O线程
注意:当使用BeginRead或BeginWrite方法在执行大量数据的读或写时效果更好,但对于读/写少量数据时,因为线程切换需要大量时间,所以效果可能比同步方法更慢。
1.异步写入
FileStream中包含BeginWrite、EndWrite方法可以启动I/O线程进行异步写入。
public override IAsyncResult BeginWrite(byte[] array, int offset, int numBytes, AsyncCallback userCallback, Object stateObject)
public override void EndWrite(IAsyncResult asyncResult)
BeginWrite方法的使用与BeginInvoke方法类似,返回值为IAsyncResult类型,AsyncCallback用于绑定回调函数,stateObject用于传递外部参数,array用于传递写入数据的二进制流,numberBytes用于传递数据的长度
实例:异步调用FileStream的I/O线程
class Program
{
static void Main()
{
FileStream stream = new FileStream("File.txt", FileMode.OpenOfCreate, FileAccess.ReadWrite, FileShare.ReadWrite, 1024, true);
byte[] bytes = new byte[16384];
string message = "aaaaaaaaaaassssssccccccc";
bytes = Encoding.Unicode.GetBytes(message);
stream.BeginWrite(bytes, 0 , (int)bytes.Length, new AsyncCallback(callback), stream);
stream.Flush(); // 将基本缓冲区的数据移动至目标或清除缓冲区
}
static void callback(IAsyncResult result)
{
FileStream stream = (FileStream)result.AsyncState;
stream.EndWrite(result);
}
}
2.异步读取
FileStream中包含BeginRead与EndRead可以异步调用I/O线程进行读取。
public override IAsyncResult BeginRead(byte[] array, int offset, int numBytes, AsyncCallback userCallback, Object stateObject)
public override int EndRead(IAsyncResult result)
其使用方式与BeginWrite和EndWrite类似。EndRead方法返回从流读取到的字节数量。
实例:使用FileStream异步I/O线程实现读取
class Program
{
public class FileData // 用于封装回调所需的一些参数
{
public FileStream Stream;
public int Lenght;
public byte[] ByteData;
}
static void Main()
{
byte[] bytes = new byte[888888];
FileStream stream = new FileStream("File.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, 1024, ture);
FileData fileData = new FileData();
FileData.Stream = stream;
FileData.Length = (int)stream.Lenght;
FileData.ByteData = bytes;
stream.BeginRead(bytes, 0, fileData.Lenght, new AsyncCallback(Completed), fileData);
}
public static void Completed(IAsyncResult result)
{
FileData fileData = (FileData)result.AsyncState;
int Lenght = fileData.Stream.EndRead(result);
if (Lenght != fileData.Lenght)
throw new Excption("Stream is not completed");
string data = Encoding.ASCII.GetString(fileData.ByteData, 0, fileData.Lenght);
}
}