.Net 异步多线程总结

一、进程与线程

进程是指一个程序在计算机上运行时,全部的计算资源的合集;
线程是程序的最小执行单位,包含计算资源,任何一个操作的响应都是线程来完成的;
多线程是指多个线程并发执行;
多线程虽然能够提升程序的运行效率,但是消耗的资源更多,所以线程并不是越多越好。

二、同步和异步

同步和异步都是针对方法而言;
同步方法指的是程序按照顺序执行,必须等待上一操作完成后才能继续往后执行;
异步方法指的是方法在调用之后立即返回,以便程序在该被调用方法返回后调用其它方法执行其他任务。

三、.Net框架中涉及异步多线程的知识
不要试图通过启动顺序或者时间等待来控制线程的执行的顺序,因为线程执行是启动无序执行时间不确定的,控制线程顺序的方法可以有三种: 回调、等待、状态判断

//首先定义一个耗时比较多的方法来做后续调用
 private void DoSomething(string name)
 {
      Console.WriteLine($"******DoSomething Start {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}*****");
      long lResult = 0;
      for (int i = 0; i < 1000000000; i++)
      {
          lResult += i;
      }

      //Thread.Sleep(5000);

      Console.WriteLine($"******DoSomething   End {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}*****");
 }
  • Async (采用委托实现,委托是.Net Framework 1.0 提供的功能)
    声明一个委托 由于.net 3.0 后提供了Action Func 委托,因此这里不再自己新创建委托,使用这两个即可
//不带返回值的委托
{
    Action act = this.DoSomething;
    IAsyncResult iAsyncResult = null;
    AsyncCallback callback = ar =>
    {
        Console.WriteLine(object.ReferenceEquals(ar, iAsyncResult));
        Console.WriteLine(ar.AsyncState);
        Console.WriteLine($"这里是BeginInvoke的回调函数。。。{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
    };

    /* 
     * BeginInvoke异步调用,会开启一个新的线程,不同于Invoke同步方法 
     * 此方法返回IAsyncResult类型的一个参数,实际上这个参数就是上面AsyncCallback委托的传入参数
     * 该方法的第三个参数,会传给回调方法中的IAsyncResult类型的参数,将其放在AsyncState字段
     * 这里就是利用回调的方式来控制线程的执行顺序
    */
    asyncResult = act.BeginInvoke("委托类型的参数", callback, "lucas"); //也可以用下面表示
    asyncResult = act.BeginInvoke("委托类型的参数", ar => {
        Console.WriteLine(ar.AsyncState); //可以拿到传入的第三个参数
    },"lucas");

    /*通过状态来控制,缺点是 1、主线程需要等待  2、边等待边做事儿  3、有误差*/
    while (!asyncResult.IsCompleted)
    {
        Thread.Sleep(200);
    }

    /*利用等待来控制线程的执行顺序*/
    asyncResult.AsyncWaitHandle.WaitOne(); //等待子线程执行完主线程再继续执行
    asyncResult.AsyncWaitHandle.WaitOne(-1);//同上
    asyncResult.AsyncWaitHandle.WaitOne(1000);//等1秒钟后再执行
    act.EndInvoke(asyncResult);//也是等待   
    Console.WriteLine("这里是BeginInvoke执行完毕之后才继续执行");
}

//带返回值的委托
{
    Func func = i => i.ToString();

    IAsyncResult iAsyncResult = func.BeginInvoke(DateTime.Now.Year, ar =>
    {
        string resultIn = func.EndInvoke(ar); //调用EndInvoke,获得返回值
    }, "lucas");
}
  • Thread (线程,.Net Framework 1.0 )
    功能最强大,但很多方法已经不建议使用,也是唯一一个可以使用前台线程概念的方法,后续的ThreadPool等都是后台线程
    当未指定 thread.IsBackground = true;直接调用thread.Start();则为前台线程
{
    ThreadStart threadStart = new ThreadStart(() => DoSomething("lucas"));

    Thread thread = new Thread(threadStart);

    thread.IsBackground = true; //若开启,则为后台线程,UI线程退出后,也跟着退出

    thread.Start();//前台线程,UI线程退出后,还会继续执行完
}

{
    /* 
     * Thread控制线程的顺序可以使用下面的方式 
    */

    thread.Join();//等待子线程执行完主线程再继续执行

    while (thread.ThreadState != System.Threading.ThreadState.Running) //判断状态
    {

    }

    #region 结合委托来实现回调

    /// 
    /// 定义一个基于Thread的回调,与BeginInvoke相似
    /// 
    /// 
    /// 
    private void CallbackByThread(ThreadStart threadStart, Action callback)
    {
       ThreadStart startNew = new ThreadStart(() =>
       {
           threadStart();
           callback.Invoke(); //此处会等待threadStart执行完再执行
       });
      Thread thread = new Thread(startNew);
      thread.Start();
   }

  /// 
  /// 定义一个带返回值的回调
  /// 
  /// 
  /// 
  /// 
  private Func ReturnByThread(Func funcT)
  {
      T t = default(T);
      ThreadStart startNew = new ThreadStart(() =>
      {
           t = funcT.Invoke();
      });
      Thread thread = new Thread(startNew);
      thread.Start();

      return new Func(() => {
          thread.Join(); //等待子线程执行完毕再继续执行
          return t;
          });
 }

{
    CallbackByThread(()=>{ Console.WriteLine("这里是新开的线程"); },()=>{ Console.WriteLine("这里是回调方法");  });

    Func<string> func = ReturnByThread(()=>{ Console.WriteLine("新开线程执行,后续可以获取返回值"); });
    string result = func.Invoke(); //获取到返回值,类似EndInvoke

}
 #endregion 

}
  • ThreadPool
    线程池,.Net Framework 2.0 出现,目的为了去掉各种复杂的api避免想thread一样的滥用,因为线程是由CLR管理的,同时引入池化的概念,减少线程创建和销毁的成本.
{   
    ThreadPool.QueueUserWorkItem(o => {
        DoSomething("lucas");
    }, "lucas");

    ThreadPool.QueueUserWorkItem(o =>
    {
        DoSomething("lucas");
        // 回调,再包一层
        new Action(() => { 
            Console.WriteLine("这里是回调方法"); 
        }).Invoke(); 
    });


    /*
     * 线程池的执行顺序可以用 ManualResetEvent 来控制
     * 任何时候如果需求不需要等待,则不要随便去设置这个
    */

    ManualResetEvent mre = new ManualResetEvent(false); //关闭
    ThreadPool.QueueUserWorkItem( o => {
        Thread.Sleep(5000);
        Console.WriteLine("");

        mre.Set(); //此处调用Set之后变成true,WaitOne()才会继续往下执行
    });
    Console.WriteLine("WaitOne 之前");
    mre.WaitOne(); // 需要set之后才会往后执行,如果没有Set,不会往后执行,再Set之后还可以Reset将其关闭,则会继续等待
    Console.WriteLine("WaitOne 之后");
}
{

    #region PoolSet

    ThreadPool.SetMaxThreads(8, 8);
    ThreadPool.SetMinThreads(8, 8);
    int workerThreads = 0;
    int ioThreads = 0;
    ThreadPool.GetMaxThreads(out workerThreads, out ioThreads);
    Console.WriteLine(String.Format("Max worker threads: {0};    Max I/O threads: {1}", workerThreads, ioThreads));

    ThreadPool.GetMinThreads(out workerThreads, out ioThreads);
    Console.WriteLine(String.Format("Min worker threads: {0};    Min I/O threads: {1}", workerThreads, ioThreads));

    ThreadPool.GetAvailableThreads(out workerThreads, out ioThreads);
    Console.WriteLine(String.Format("Available worker threads: {0};    Available I/O threads: {1}", workerThreads, ioThreads));

    #endregion

}
  • Task
    任务,.Net Framework 3.0 提供,使用了线程池,所以全部是后台线程
{
    /*两种启动方式是一样的*/
    Task.Run(new Action(() => {
        Thread.Sleep(5000);
    }));

    Task.Factory.StartNew(() => { Console.WriteLine("");});
}   

{
    /*
     * 需要多线程加快速度,同时又有要求全部完成后,才能返回
     * 多业务操作,希望并发,但是全部完成后再返回
     */
    TaskFactory taskFactory = Task.Factory;
    List<Task> taskList = new List<Task>();
    taskList.Add(taskFactory.StartNew(() => { }));
    taskList.Add(taskFactory.StartNew(() => { }));
    taskList.Add(taskFactory.StartNew(() => { }));
    taskList.Add(taskFactory.StartNew(() => { }));

    Task.WaitAll(taskList.ToArray()); //等待,任务全部完成后再往后执行

    /*
     * 需要多线程加快速度,同时又有要求某个完成后,才能返回
     * 多业务操作,希望并发,但是任意某个任务完成后再返回
    */
    Task.WaitAny(taskList.ToArray()); 

    //想知道是哪个任务完成,可以传入第二个参数,然后通过ContinueWith来获得
    Task task = taskFactory.StartNew(o => { }, "lucas");
    //通过task.AsyncState 就是下面的 tAsyncSate来进行判断 
    Task task1 = taskFactory.StartNew(o => { }, "lucas").ContinueWith(t => Console.WriteLine($"这里是{t.AsyncState}的回调"));

    //不阻塞主线程使用回调
    taskFactory.ContinueWhenAny(taskList.ToArray(), t => Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId.ToString()}"));
    taskFactory.ContinueWhenAll(taskList.ToArray(), tList => Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId.ToString()}"));

    //带返回值的
    Task<int> intTask = taskFactory.StartNew(() => 123);
    int result = intTask.Result; //获得返回值

}
  • Parallel
    并行运算,.Net Framework 4.0 提供
{
    //跟task很像,等价于 task+waitall ,启动多个线程计算,而且主线程也参与计算,节约了一个线程
    Parallel.Invoke(() => DoSomething("Task_001"),
                    () => DoSomething("Task_002"),
                    () => DoSomething("Task_003"),
                    () => DoSomething("Task_004"),
                    () => DoSomething("Task_005"));

    Parallel.For(0, 5, t =>
    {
        DoSomething($"Task_00{t}");
    });

    Parallel.ForEach(new int[] { 0, 1, 2, 3, 4 }, t =>
    {
        DoSomething($"Task_00{t}");
    });


    ParallelOptions options = new ParallelOptions()
    {
        MaxDegreeOfParallelism = 3  //设置启动并发任务数量最多是3个
    };

    //只会开启3个线程执行,并且包含了主线程
    Parallel.ForEach(new int[] { 0, 1, 2, 3, 4 }, options, t =>
    {
        DoSomething($"Task_00{t}");
    });

    //可以利用委托的第二个参数state来结束并行运算
    Parallel.ForEach(new int[] { 0, 1, 2, 3, 4 }, options, (t, state) =>
    {
        DoSomething($"Task_00{t}");
        //state.Stop();//结束全部的
        //state.Break();//停止当前的
        //return;
    });
}
  • async/await
    C# 5.0 .Net Framework 4.5 提供
    是一个语法糖,由编译器提供,而不是CLR升级的
    1、不能单独的使用await,使用await时方法声明必须有async,指使用async的话和普通方法没区别
    2、await只能放在Task前面
    3、不推荐void返回值,使用Task来代替
    4、async/await本身不会产生多线程,只是和Task使用,本质上还是Task
private static async void NoReturn()
{
    //主线程执行
    Console.WriteLine($"NoReturn Sleep before await,ThreadId={Thread.CurrentThread.ManagedThreadId}");
    TaskFactory taskFactory = new TaskFactory();
    Task task = taskFactory.StartNew(() =>
    {
        Console.WriteLine($"NoReturn Sleep before,ThreadId={Thread.CurrentThread.ManagedThreadId}");
        Thread.Sleep(3000);
        Console.WriteLine($"NoReturn Sleep after,ThreadId={Thread.CurrentThread.ManagedThreadId}");
            });
        await task;//主线程到这里就返回了,执行主线程任务

        //一流水儿的写下去的,耗时任务就用await

        //子线程执行   其实是封装成委托,在task之后成为回调(编译器功能  状态机实现)
        //task.ContinueWith()
        //这个回调的线程是不确定的:可能是主线程  可能是子线程  也可能是其他线程
        Console.WriteLine($"NoReturn Sleep after await,ThreadId={Thread.CurrentThread.ManagedThreadId}");
}

/// 
/// 无返回值  async Task == async void
/// Task和Task能够使用await, Task.WhenAny, Task.WhenAll等方式组合使用。Async Void 不行
/// 
/// 
private static async Task NoReturnTask()
{
     //这里还是主线程的id
     Console.WriteLine($"NoReturnTask Sleep before await,ThreadId={Thread.CurrentThread.ManagedThreadId}");

     Task task = Task.Run(() =>
     {
          Console.WriteLine($"NoReturnTask Sleep before,ThreadId={Thread.CurrentThread.ManagedThreadId}");
          Thread.Sleep(3000);
          Console.WriteLine($"NoReturnTask Sleep after,ThreadId={Thread.CurrentThread.ManagedThreadId}");
      });
      await task;
      Console.WriteLine($"NoReturnTask Sleep after await,ThreadId={Thread.CurrentThread.ManagedThreadId}");

      //return new TaskFactory().StartNew(() => { });  //不能return  没有async才行
}

/// 
/// 带返回值的Task  
/// 要使用返回值就一定要等子线程计算完毕
/// 
/// async 就只返回long
private static async Task<long> SumAsync()
{
     Console.WriteLine($"SumAsync 111 start ManagedThreadId={Thread.CurrentThread.ManagedThreadId}");
     long result = 0;

     await Task.Run(() =>
     {
          for (int k = 0; k < 10; k++)
          {
              Console.WriteLine($"SumAsync {k} await Task.Run ManagedThreadId={Thread.CurrentThread.ManagedThreadId}");
              Thread.Sleep(1000);
          }

          for (long i = 0; i < 999999999; i++)
          {
               result += i;
          }
    });

    return result;
}

{
    Task<long> t = SumAsync();
    Console.WriteLine($"Main Thread Task ManagedThreadId={Thread.CurrentThread.ManagedThreadId}");
    long lResult = t.Result;//访问result,主线程等待Task的完成
    t.Wait();//等价于上一行
}
  • 线程的异常处理

    多线程里面的异常不会被外部线程捕获到,除非调用WaitAll进行等待,所以使用多线程的时候,是不允许抛出异常的,也就是说线程的异常需要在内部自己try/catch处理掉。

try{
        TaskFactory taskFactory = new TaskFactory();
        List taskList = new List();
        for (int i = 0; i < 20; i++)
        {
            string name = string.Format($"btnThreadCore_Click_{i}");
            Action<object> act = t =>
            {
                try
                {
                    Thread.Sleep(2000);
                    if (t.ToString().Equals("btnThreadCore_Click_11"))
                    {
                        throw new Exception(string.Format($"{t} 执行失败"));
                    }
                    if (t.ToString().Equals("btnThreadCore_Click_12"))
                    {
                        throw new Exception(string.Format($"{t} 执行失败"));
                    }
                    Console.WriteLine("{0} 执行成功", t);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
        };
        taskList.Add(taskFactory.StartNew(act, name));
    }
        Task.WaitAll(taskList.ToArray()); //此处调用WaitAll后上面抛出的异常才能被捕获,否则无法捕获异常
}
catch(AggregateException aex)
{
    foreach (var item in aex.InnerExceptions)
    {
         Console.WriteLine(item.Message);
    }
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}
  • 线程的取消
    线程取消不能试图去操作线程,而是要操作信号量(一个共享变量,多个线程可以共同访问到的,可以说hi是变量,可以是数据库的数据,也可以是硬盘上的某数据),每个线程在执行的过程当中,经常的去查看这个信号量,然后自己结束掉自己,线程不能被其他线程终止,只能自己结束自己,结束的时候延迟的时间是不可避免的。
TaskFactory taskFactory = new TaskFactory();
List taskList = new List();

//CancellationTokenSource可以在cancel后,取消没有启动的任务
CancellationTokenSource可以在cancel后,取消没有启动的任务
CancellationTokenSource cts = new CancellationTokenSource();//bool值
for (int i = 0; i < 40; i++)
{
    string name = string.Format("btnThreadCore_Click{0}", i);
    Action<object> act = t =>
    {
        try
        {
            Thread.Sleep(2000);
            if (t.ToString().Equals("btnThreadCore_Click11"))
            {
                throw new Exception(string.Format("{0} 执行失败", t)); //抛异常做测试
            }
            if (t.ToString().Equals("btnThreadCore_Click12"))
            {
                throw new Exception(string.Format("{0} 执行失败", t));
            }
            if (cts.IsCancellationRequested)//检查信号量
            {
                Console.WriteLine("{0} 放弃执行", t);
                return;
            }
            else
            {
                Console.WriteLine("{0} 执行成功", t);
            }
        }
        catch (Exception ex)
        {
            cts.Cancel(); //这里抛异常后进行了Cancel,其他线程运行的时候检测到信号量便返回不再执行
            Console.WriteLine(ex.Message);
        }
    };
    taskList.Add(taskFactory.StartNew(act, name, cts.Token));
}
Task.WaitAll(taskList.ToArray());
  • 线程安全
    当多个线程访问共有变量(都能访问的局部变量/全局变量/数据库的一个值/硬盘)时,可能会出现线程冲突。
    .Net中提供了线程安全的集合:System.Collections.Concurrent.ConcurrentDictionary
    解决线程冲突的方法:
    1、使用lock,lock方法块里面是单线程的,因此lock里面的代码要尽量少,只包含共享的就好
    2、没有冲突,尽量从数据上隔离开
    lock的使用:
    1、lock ==Monitor.Enter 检查下这个变量有没有被lock 如果有就等着,没有就占用,然后进去执行,执行完了释放
    2、尽量不要使用 lock(this) 锁定当前实例,因为如果其他地方要使用这个实例变量则会都被锁定了
    3、如果每个实例想要单独的锁定,声明一个 private object 的变量
    4、不要锁定一个字符串,例如 string a=”123456” ;lock(a) { } string b=”123456”;由于.Net中字符串是使用享元模式的内存分配,字符串值是唯一的,会锁定别的变量b
    5、声明 private static readonly object LockObject= new object(); 的时候最好加上readonly,这也是微软推荐的解决线程冲突lock的最佳使用方法,防止该变量在lock中被更改失效。
private static readonly object LockObject= new object();
private int TotalCount = 0;//
private List<int> IntList = new List<int>();
for (int i = 0; i < 10000; i++)
{
     int newI = i;
     taskList.Add(taskFactory.StartNew(() =>
     {
         int m = 3 + 2;
         lock (LockObject)//lock后的方法块,任意时刻只有一个线程可以进入  
         //Monitor.Enter(LockObject)
         {   
             //这里就是单线程
             this.TotalCount += 1;//多个线程同时操作,有时候操作被覆盖了
             IntList.Add(newI);
         }
     }));

}
Task.WaitAll(taskList.ToArray());

Console.WriteLine(this.TotalCount);
Console.WriteLine(IntList.Count());

四、 总结
1、使用多线程的时候,尽量不要是用Thread,ThreadPool
2、最好使用Task,Parallel,async/await
3、使用委托进行异步操作的时候,尽量不要自己声明委托了,因为微软已经提供了Action 、Func两种委托,足够使用了
4、使用线程的时候要注意异常的捕获,和线程安全问题

你可能感兴趣的:(.Net,进阶)