异步和多线程

硬盘,显卡这些硬件是可以不消耗CPU资源而自动与内存交换数据的,这也是实现异步的基本条件。所以异步是硬件式的异步,而多线程就是多个thread并发。

使用委托实现异步调用

通过Action以及Func的BeginInvoke方法可以很轻松的实现异步调用,如下:

private void btnAsync_Click(object sender, EventArgs e)
{
    Console.WriteLine($"****************btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    Action<string> action = this.DoSomethingLong;
    for (int i = 0; i < 5; i++)
    {
        string name = string.Format($"btnAsync_Click_{i}");
        action.BeginInvoke(name, null, null); //异步调用
    }
    Console.WriteLine($"****************btnAsync_Click End   {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
}

BeginInvoke有两个参数一个是AsyncCallback,一个是object,前一个参数是一个委托,看名字就知道是一个回调函数,当异步执行完成之后,就会执行回调函数,object代表一个参数,如果回调函数需要参数则通过object传入,如下:

private void btnAsyncAdvanced_Click(object sender, EventArgs e)
{
    Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    Action<string> action = this.DoSomethingLong;
    IAsyncResult asyncResult = null;
    AsyncCallback callback = ia =>
    {
        Console.WriteLine(ia.AsyncState); // 通过AsyncState得到参数
        Console.WriteLine($"到这里计算已经完成了。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
    };
    asyncResult = action.BeginInvoke("btnAsyncAdvanced_Click", callback, "hao");
    Console.WriteLine($"异步非阻塞。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。");

    // 也可以通过IsCompleted属性判断线程是否已经完成
    int i = 0;
    while (!asyncResult.IsCompleted)//1 卡界面:主线程忙于等待
    {   //可以等待,边等待边做其他操作
        //可能最多200ms的延迟
        if (i < 10)
        {
            Console.WriteLine($"文件上传完成{i++ * 10}%..");//File.ReadSize
        }
        else
        {
            Console.WriteLine($"文件上传完成99.9%..");
        }
        Thread.Sleep(200);
    }
    Console.WriteLine($"上传成功了。。。..");
}

如果不想通过回调函数,同时又想等待异步执行完成,那么可以通过WaitOne方法或者EndInvoke方法来实现,如下:

private void btnAsyncAdvanced_Click(object sender, EventArgs e)
{
    {
        Action<string> action = this.DoSomethingLong;
        IAsyncResult asyncResult = action.BeginInvoke("btnAsyncAdvanced_Click", null, "hao");
        //asyncResult.AsyncWaitHandle.WaitOne();//等待任务的完成
        //asyncResult.AsyncWaitHandle.WaitOne(-1);//等待任务的完成
        //asyncResult.AsyncWaitHandle.WaitOne(1000);//等待;但是最多等待1000ms

        //action.EndInvoke(asyncResult);//可以等待
    }
    {
        //通过Func的EndInvoke方法来进行等待
        Func<int> func = () =>
        {
            Thread.Sleep(2000);
            return DateTime.Now.Day;
        };
        IAsyncResult asyncResult = func.BeginInvoke(r =>
        {
            func.EndInvoke(r);
            Console.WriteLine(r.AsyncState);
        }, "test");
    }
}

使用Thread实现多线程

Thread是C#语言对线程对象的封装,该类在System.Threading命名空间中。使用Thread类创建线程时,只需要提供线程入口,线程入口告诉程序让这个线程做什么。通过实例化一个Thread类的对象就可以创建一个线程。创建新的Thread对象时,将创建新的托管线程。Thread类接收一个ThreadStart委托或ParameterizedThreadStart委托的构造函数,该委托包装了调用Start方法时由新线程调用的方法。
Thread 中包括了多个方法来控制线程的创建、挂起、停止、销毁,如Start,join,sleep等。

static void Main(string[] args)
{
    //获取正在运行的线程
    Thread thread = Thread.CurrentThread;
    //设置线程的名字
    thread.Name = "主线程";
    //获取当前线程的唯一标识符
    int id = thread.ManagedThreadId;
    //获取当前线程的状态
    ThreadState state= thread.ThreadState;
    //获取当前线程的优先级
    ThreadPriority priority= thread.Priority;
    string strMsg = string.Format("Thread ID:{0}\n" + "Thread Name:{1}\n" +
        "Thread State:{2}\n" + "Thread Priority:{3}\n", id, thread.Name,
        state, priority);

    Console.WriteLine(strMsg);
    Console.ReadKey();
}

另外需要特别注意Thread.IsBackground可以设置是否是后台线程。后台线程是指只要所有的前台线程结束,后台线程自动结束。前台线程是指只有所有的前台线程都结束,应用程序才能结束。默认情况下创建的线程都是前台线程。

class Program
{
    static void Main(string[] args)
    {
        //演示前台、后台线程
        BackGroundTest background = new BackGroundTest(10);
        //创建前台线程
        Thread fThread = new Thread(new ThreadStart(background.RunLoop));
        //给线程命名
        fThread.Name = "前台线程";


        BackGroundTest background1 = new BackGroundTest(20);
        //创建后台线程
        Thread bThread = new Thread(new ThreadStart(background1.RunLoop));
        bThread.Name = "后台线程";
        //设置为后台线程
        bThread.IsBackground = true;

        //启动线程
        fThread.Start();
        bThread.Start();
    }
}

class BackGroundTest
{
    private int Count;
    public BackGroundTest(int count)
    {
        this.Count = count;
    }
    public void RunLoop()
    {
        //获取当前线程的名称
        string threadName = Thread.CurrentThread.Name;
        for (int i = 0; i < Count; i++)
        {
            Console.WriteLine("{0}计数:{1}", threadName, i.ToString());
            //线程休眠500毫秒
            Thread.Sleep(1000);
        }
        Console.WriteLine("{0}完成计数", threadName);

    }
}

必须在调用Start方法之前设置线程的类型,否则一旦线程运行,将无法改变其类型。

后台线程一般用于处理不重要的事情,应用程序结束时,后台线程是否执行完成对整个应用程序没有影响。如果要执行的事情很重要,需要将线程设置为前台线程。

使用ThreadPool实现多线程

线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
ThreadPool可以重用线程,避免重复的创建和销毁。

private void btnThreadPool_Click(object sender, EventArgs e)
{
    Console.WriteLine($"****************btnThreadPool_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click"));
    ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click"));
    ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click"));
    ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click"));
    ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click"));

    Thread.Sleep(10 * 1000);
    Console.WriteLine("前面的计算都完成了。。。。。。。。");
    ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click"));
    ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click"));
    ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click"));
    ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click"));
    ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click"));



    ThreadPool.SetMaxThreads(16, 16);//设置最大/小线程数
    ThreadPool.SetMinThreads(8, 8);

    //类  包含了一个bool属性
    //false--WaitOne等待--Set--true--WaitOne直接过去
    //true--WaitOne直接过去--ReSet--false--WaitOne等待
    ManualResetEvent manualResetEvent = new ManualResetEvent(false);
    ThreadPool.QueueUserWorkItem(t =>
    {
        this.DoSomethingLong("btnThreadPool_Click");
        manualResetEvent.Set();
        //manualResetEvent.Reset();
    });
    manualResetEvent.WaitOne();
}

需要注意到我们可以利用信号灯AutoResetEvent和ManualResetEvent来实现线程等待问题

使用Task实现多线程

.NET 4包含新名称空间System.Threading.Tasks,它 包含的类抽象出了线程功能。 在后台使用ThreadPool。 任务表示应完成的某个单元的工作。 这个单元的工作可以在单独的线程中运行,也可以以同步方式启动一个任务,这需要等待主调线程。 使用任务不仅可以获得一个抽象层,还可以对底层线程进行很多控制。在安排需要完成的工作时,任务提供了非常大的灵活性。 例如,可 以定义连续的工 作—— 在一个任务完成后该执行什么工作。 这可以区分任务成功与否。 另外,还可以在层次结构中安排任务。例如,父任务可以创建新的子任务。 这可以创建一种依赖关系,这样,取消父任务,也会取消其子任务。
Task有三种常用的启动方式:

/// <summary>
/// 多种线程启动方式
/// </summary>
private void StartThread()
{
    Task.Run(() => this.DoSomethingLong("btnTask_Click1"));
    Task.Factory.StartNew(() => this.DoSomethingLong("btnTask_Click2"));
    new Task(() => this.DoSomethingLong("btnTask_Click3")).Start();
    //启动带回调
    Task.Run(() => this.DoSomethingLong("Client")).ContinueWith(t => { });
}

另外有几个常用的API需要注意区分:

  • waitAny:会阻塞当前线程,等待某一个任务完成,才进入下一行,会卡界面
  • waitAll:阻塞当前线程,等待任务全部完成,会卡界面
  • WhenAny:不阻塞当前线程,等待某一个任务完成,可以搭配ContinueWith
  • WhenAll:不阻塞当前线程,等待任务全部完成,可以搭配ContinueWith
    值得一提的是 WaitAny, WaitAll也提供了超时等待的Api 如Task.WaitAll(tasks.ToArray(), 1000); 最多等待1s,超时就不等了,-1为无限等待;因为WhenAny,WhenAll 它的返回值是一个Task,所以当某些时候需要调整执行顺序时,将方法加入TaskList中,再用WaitAny,WaitAll即可。
    Task拥有一个Delay方法,可以实现延迟调用,与Thread不同,Delay不卡线程,只是可以等待一些时间,再去完成一些事情,如下:
/// <summary>
/// 延时操作
/// </summary>
private void Delay()
{
    //Task.Delay(1000);//延迟  不会卡
    //Thread.Sleep(1000);//等待   卡

    #region 同步延时,卡界面

    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Thread.Sleep(2000);
    stopwatch.Stop();
    Console.WriteLine(stopwatch.ElapsedMilliseconds);

    #endregion

    #region 异步延时,不卡界面

    Stopwatch stopwatch2 = new Stopwatch();
    stopwatch.Start();
    Task.Delay(2000).ContinueWith(t =>
    {
        stopwatch.Stop();
        Console.WriteLine(stopwatch2.ElapsedMilliseconds);
    });

    #endregion

    #region 同步+异步延时,不卡界面

    Stopwatch stopwatch3 = new Stopwatch();
    stopwatch.Start();
    Task.Run(() =>
    {
        Thread.Sleep(2000);
        stopwatch.Stop();
        Console.WriteLine(stopwatch3.ElapsedMilliseconds);
    });

    #endregion
}

任务Task和线程Thread的区别:
1、任务是架构在线程之上的,也就是说任务最终还是要抛给线程去执行。
2、任务跟线程不是一对一的关系,比如开10个任务并不是说会开10个线程,这一点任务有点类似线程池,但是任务相比线程池有很小的开销和精确的控制。
Task和Thread一样,位于System.Threading命名空间下

使用Parallel实现多线程

在C#中实现多线程的另一个方式是使用Parallel类。在.NET4中 ,另一个新增的抽象线程是Parallel类 。Parallel类在Task的基础上做了封装,定义了并行的for和foreach的静态方法,并调用parallel线程参与计算,节约了一个线程。
在Parallel下面有三个常用的方法invoke,For和ForEach,如下:

{
    //Parallel.Invoke(() => this.Coding("爱书客", "Client")
    //    , () => this.Coding("风动寂野", "Portal")
    //    , () => this.Coding("笑看风云", "Service"));
}
{
    //Parallel.For(0, 5, i => this.Coding("爱书客", "Client" + i));
}
{
    //Parallel.ForEach(new string[] { "0","1","2","3","4"}, i => this.Coding("爱书客", "Client" + i));
}
{
    ////parallelOptions 可以控制并发数量
    //ParallelOptions parallelOptions = new ParallelOptions();
    //parallelOptions.MaxDegreeOfParallelism = 3;
    //Parallel.For(0, 10, parallelOptions, i => this.Coding("爱书客", "Client" + i));
}

另外,不推荐使用Break和Stop方法,因为我们没办法控制并行程序时结果的先后顺序。

多线程常见问题

异常处理

当主程序启动时,定义了两个将会抛出异常的线程。其中一个在方法内部对异常进行了处理,另一个则没有。可以看到第二个异常没有被包裹启动线程的try/catch代码块捕获到。所以如果直接使用线程,一般来说不要在线程中抛出异常,而是在线程内部代码中使用try/catch代码块!

线程取消

如果我们要实现:多个线程并发,某个失败后,希望通知别的线程,都停下来。这是我们无法在外部中止,通过Thread.Abort也不靠谱,原因是我们掌控操作系统中的线程执行过程,正确的做法应该是通过CancellationTokenSource去标志任务是否取消。如下:

CancellationTokenSource cts = new CancellationTokenSource();
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();
            Console.WriteLine(ex.Message);
        }
    };
    taskList.Add(taskFactory.StartNew(act, name, cts.Token));
}
Task.WaitAll(taskList.ToArray());

其中,注意到CancellationTokenSource是外部对Task的控制,如取消、定时取消。可以通过在外部CancellationTokenSource对象进行控制是否取消任务的运行。当在 MyTask 中的 cancelTokenSource.IsCancellationRequested 判断如果是取消了任务的话 就跳出while循环执行。也就结束了任务

多线程临时变量

当我们想通过循环语句直接输出多线程结果的时候,一定要注意添加临时变量,防止输出的是同一个变量,如下:

for (int i = 0; i < 5; i++)
{
    Task.Run(() =>
      {
          Thread.Sleep(100);
          Console.WriteLine(i);
      });
}

for (int i = 0; i < 5; i++)
{
    int k = i;
    Task.Run(() =>
    {
        Thread.Sleep(100);
        Console.WriteLine(k);
    });
}

//i最后是5 全程就只有一个i  等着打印的时候,i==5
/k             全程有5个k   分别是0 1 2 3 4
//k在外面声明   全程就只有一个k    等着打印的时候,k==4
int k = 0;
for (int i = 0; i < 5; i++)
{
    k = i;
    new Action(() =>
    {
        Thread.Sleep(100);
        Console.WriteLine($"k={k} i={i}");
    }).BeginInvoke(null, null);
}

如果我们不声明内部变量k,则Task输出的变量i,均为5,这主要是因为在for循环中,多线程并不会阻塞主线程,导致输出的时候,i已经循环到了5。同理,在外部声明声明k,则只会输出最终k的值,为4.

线程安全和锁lock

当多线程访问同一个共有变量,即都能访问局部变量/全局变量/数据库的一个值,或者是共享的硬盘文件,则一定要注意是否会有线程安全问题。
假如我们在类内部声明了一个全局变量,如:

private int TotalCount = 0;//
private List<int> IntList = new List<int>();

并想通过多线程对其进行操作,输出最终结果,如下:

for (int i = 0; i < 10000; i++)//i有问题
{
    int newI = i;//没问题 全新的newI
    taskList.Add(taskFactory.StartNew(() =>
    {
        this.TotalCount += 1;
        TotalCountIn += 1;
        this.IntList.Add(newI);
    }};
}
Task.WaitAll(taskList.ToArray());
Console.WriteLine(this.TotalCount);
Console.WriteLine(TotalCountIn);
Console.WriteLine(this.IntList.Count());

我们会发现taskList和TotalCountIn并不是像我们想象的一般,存有10000个值。事实上,这就是线程安全问题。
常有的解决办法有三种:

  • 加锁,因为只有一个线程可以进去,没有并发,所以解决了问题,但是牺牲了性能,所以要尽量缩小lock的范围
  • 数据拆分,避免冲突
  • 安全队列 ConcurrentQueue 一个线程去完成操作

如下,是一种推荐的加锁的方案,
先声明一个静态可读对象:

private static readonly object btnThreadCore_Click_Lock = new object();

然后对该对象加锁:

lock (btnThreadCore_Click_Lock)//lock后的方法块,任意时刻只有一个线程可以进入
//只能锁引用类型,占用这个引用链接   不要用string 因为享元
{   //这里就是单线程
  this.TotalCount += 1;
  TotalCountIn += 1;
  this.IntList.Add(newI);
}

之所以直接声明一个静态可读对象,而不是通过lock(this)来实现加锁,是因为lock(this)是对实例加锁,每次实例化是不同的锁,同一个实例是相同的锁,有别人对其加锁的风险。

Async、Await异步编程方式

在4.5版本中.NET又引入了Async和Await两个新的关键字,在语言层面对并行编程给予进一步的支持,使得用户能以一种简洁直观的方式实现并行编程。
await和async是一种基于编译器的功能(C#和VB.NET都提供了这个功能),不仅如此,它在实现原理上和yield非常像——await/async和yield都被编译器在编译时转化为了状态机。
状态机是一种非常常用的编程模式,基本上所有的编译器都是基于状态机实现的,当访问这篇博文的时候浏览器就是使用状态机将从cnblogs.com服务器上获取的html文本解析为html元素树,再绘制到屏幕上。
使用Async来标记方法为异步方法,主线程碰到await时会立即返回,继续以非阻塞形式执行主线程下面的逻辑。当await耗时操作完成时,继续执行Async下面的逻辑。
如下,当我们使用await语法时,本质类似于使用ContinueWith方法,其之后的程序会在await线程完成之后再执行。

/// <summary>
/// async/await  不能单独await
/// await 只能放在task前面
/// 不推荐void返回值,使用Task来代替
/// Task和Task<T>能够使用await, Task.WhenAny, Task.WhenAll等方式组合使用。Async Void 不行
/// </summary>
private static async Task 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}");
    });

    //task.ContinueWith(t =>
    //{
    //    Console.WriteLine($"NoReturn Sleep after await,ThreadId={Thread.CurrentThread.ManagedThreadId}");
    //});
    await task;//到这里就返回主线程,执行主线程任务

    //这个回调的线程是不确定的:可能是主线程  可能是子线程  也可能是其他线程
    Console.WriteLine($"NoReturn Sleep after await,ThreadId={Thread.CurrentThread.ManagedThreadId}");
}

参考:
C#多线程
Async和Await异步编程的原理

posted @ 2021-01-27 21:21  Jamest  阅读(303)  评论(0编辑  收藏  举报