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);
    }
}