欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C# async await异步编程

程序员文章站 2024-01-28 11:27:22
...

背景:做NX二次开发的时候收集了NX操作的操作日志,然后用winform做了个应用读取分析这些日志。每天会积累超过5000多份日志,所以想到了异步编程来解决速度和卡死的问题。

一、异步编程和多线程的异同

硬件里有个概念叫DMA,也就是直接访问内存不经过CPU处理,这正是异步操作的硬件基础。异步编程无需额外的线程负担,死锁的情况也少,但是和自然人的思维方式有些不一样。而多线程中的各个线程的代码还是顺序执行的,自然人理解起来简单一些,但是线程的滥用反而会造成很多问题。

所以适用场合分别为:

异步:直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及.net Remoting等跨进程的调用

多线程:需要长时间CPU运算的场合

二、异步函数简介

直接用<C# in Depth>的介绍:

async Task<int> AsyncFunction()
{
    var value = await SomeLongTimeFunction();
    
    DoUpdateOrSomething();
}

用了async修饰符声明了的函数AsyncFunction中,用await修饰某个耗时操作的表达式SomeLongTimeFunction().如果SomeLongTimeFunction没有完成那么这个异步函数AsyncFunction会立即返回。当SomeLongTimeFunction执行完毕后,异步函数将在合适的线程上继续执行下面的语句

三、使用任务的异步编程

参考文章:<使用 Async 和 Await 的异步编程>  <Task的使用>

msdn说:async 和 await 关键字不会导致创建其他线程。 因为异步方法不会在其自身线程上运行,因此它不需要多线程。 只有当方法处于活动状态时,该方法将在当前同步上下文中运行并使用线程上的时间。 可以使用 Task.Run 将占用大量 CPU 的工作移到后台线程,但是后台线程不会帮助正在等待结果的进程变为可用状态。

总而言之,Task能达到类似多线程的效果,但具体怎么分配线程由.net底层控制,程序员只需要专注业务逻辑即可。 同时具备以前线程不具备的返回值的功能(可以通过回调事件来处理),而在Task中只是简单的使用await来使用, await的task相当于同步的调用task, 同时会有返回值。

示例代码

ps:用了很多LINQ,看起来不太直观

ps2:现在看来其实写的复杂了,套了好几层。其实没必要把文件分组

首先是UI线程上按钮点击函数,目的是读取分析日志文件,并将结果显示在界面上:

async void ComputeClick(object sender, EventArgs e)
{
    Func<DateTime, DateTime, bool, Task<int>> taskFunc = (b, end, chk) =>
    {
        return Task.Run(() => ReadDataAsync(b, end, chk));
    };
    var days = await taskFunc(date1.Value.Date, date2.Value.Date, checkBox1.Checked);

    ///await完毕后才会执行以下语句
    ///更新界面表格和图形
    ShowChart();
}

显而易见,这个函数中使用了Taks.Run了一个委托taskFunc,当taskFunc执行完毕后才会更新界面。在执行过程中并没有阻塞UI线程,表现为用户仍然可以点击、浏览界面。

在ReadDataAsync中,将日志文件分为了N个组,每读取一个组就创建一个Task,这里使用了Task.WhenAll,也就是当所有任务都执行完毕以后才会继续执行

async Task<int> ReadDataAsync(DateTime begin, DateTime end, bool onlyWorkDays = true)
{
    var lists = GetFilesGroup();

    var tasks = lists.Select(list => ReadLogsAsync(list, begin, end, onlyWorkDays)).ToList();

    var t3 = Task.WhenAll(tasks);
    var results = await t3;
    foreach (var objs in results.SelectMany(list => list))
    {
        _table.Rows.Add(objs);
    }
    
    return _table.Rows.Count;
}

继续看单个任务的异步处理方法,这里又用到了Task.Run,原理是一样的。

async Task<List<object[]>> ReadLogsAsync(IEnumerable<FileInfo> files, DateTime begin, DateTime end, bool onlyWorkDays)
{
    var results = new List<object[]>();

    foreach (var file in files)
    {
        //操作UI线程上的控件
        SetProgress(1, 0, $"读取数据{file.Name}");

        string user, ticks;
        DateTime date;
        ReadFileName(file.Name, out user, out date, out ticks);

        if (DateTime.Compare(date, begin) < 0)
            continue;
        if (DateTime.Compare(date, end) > 0)
            continue;

        if (onlyWorkDays && (date.DayOfWeek == DayOfWeek.Sunday || date.DayOfWeek == DayOfWeek.Saturday))
            continue;

        Func<string, List<MenuButton>, Task<Dictionary<MenuButton, int>>> taskFunc = (path, buttons) =>
        {
            return Task.Run(() => ReadLog(path, buttons));
        };

        var dics = await taskFunc(file.FullName, _buttons);

        results.AddRange(dics.Select(dic => new object[]
        {
            date, user, dic.Key.Name, dic.Key.Chs, dic.Key.CasCadeChs, dic.Value, ticks
        }));
    }

    return results;
}

跨线程访问UI控件

刚刚肯定注意到了再ReadLogAsync中调用了控件,这里参考了这篇文章:<C# 跨线程调用控件>

private delegate void DelegateSetProgress(int value, int max, string text);

private void SetProgress(int value, int max, string text)
{
    if (statusStrip1.InvokeRequired)
    {
         Invoke(new DelegateSetProgress(SetProgress), value, max,text);
    }
    else
    {
         progressBar.Maximum += max;
         progressBar.Value += value;

        var precent = (progressBar.Value*1.0/progressBar.Maximum*100.0).ToString("0.#");
                progressText.Text = $"{precent}% {text}";
    }
}