C# 多线程入门系列(一)

简介: C# 多线程入门系列(一)

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。进程是资源分配的基本单位。所有与该进程有关的资源,都被记录在进程控制块PCB中。以表示该进程拥有这些资源或正在使用它们。本文以一些简单的小例子,简述如何将程序由同步方式,一步一步演变成异步多线程方式,仅供学习分享使用,如有不足之处,还请指正。

同步方式

业务场景:用户点击一个按钮,然后做一个耗时的业务。同步方式代码如下所示:

private void btnSync_Click(object sender, EventArgs e)
{
    Stopwatch watch = Stopwatch.StartNew();
    watch.Start();
    Console.WriteLine("************btnSync_Click同步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    for (int i = 0; i < 5; i++)
    {
        string name = string.Format("{0}_{1}", "btnSync_Click", i);
        this.DoSomethingLong(name);
    }
    Console.WriteLine("************btnSync_Click同步方法 结束,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    watch.Stop();
    Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}
/// <summary>
/// 模拟做一些长时间的工作
/// </summary>
/// <param name="name"></param>
private void DoSomethingLong(string name)
{
    Console.WriteLine("************DoSomethingLong 开始 name= {0} 线程ID= {1} 时间 = {2}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"));
    //CPU计算累加和
    long rest = 0;
    for (int i = 0; i < 1000000000; i++)
    {
        rest += i;
    }
    Console.WriteLine("************DoSomethingLong 结束 name= {0} 线程ID= {1} 时间 = {2} 结果={3}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"), rest);
}

同步方式输出结果,如下所示:

通过对以上示例进行分析,得出结论如下:

  1. 同步方式按顺序依次执行。
  2. 同步方式业务和UI采用采用同一线程,都是主线程。
  3. 同步方式如果执行操作比较耗时,前端UI会卡住,无法响应用户请求。
  4. 同步方式比较耗时【本示例9.32秒】

异步多线程方式

如何优化同步方式存在的问题呢?答案是由同步方式改为异步异步多线程方式。代码如下所示:

private void btnAsync_Click(object sender, EventArgs e)
{
    Stopwatch watch = Stopwatch.StartNew();
    watch.Start();
    Console.WriteLine("************btnAsync_Click异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    Action<string> action = new Action<string>(DoSomethingLong);
    for (int i = 0; i < 5; i++)
    {
        string name = string.Format("{0}_{1}", "btnAsync_Click", i);
        action.BeginInvoke(name,null,null);
    }
    Console.WriteLine("************btnAsync_Click异步方法 结束,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    watch.Stop();
    Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}

异步方式出结果,如下所示:

通过对以上示例进行分析,得出结论如下:

  1. 异步方式不是顺序执行,即具有无序性。
  2. 异步方式采用多线程方式,和UI不是同一个线程,所以前端UI不会卡住。
  3. 异步多线程方式执行时间短,响应速度快。

通过观察任务管理器,发现同步方式比较耗时间,异步方式比较耗资源【本例是CPU密集型操作】,属于以资源换性能。同步方式和异步方式的CPU利用率,如下图所示:

异步多线程优化

通过上述例子,发现由于采用异步的原因,线程还未结束,但是排在后面的语句就先执行,所以统计的程序执行总耗时为0秒。为了优化此问题,采用async与await组合方式执行,代码如下所示:

private async void btnAsync2_Click(object sender, EventArgs e)
{
    Stopwatch watch = Stopwatch.StartNew();
    watch.Start();
    Console.WriteLine("************btnAsync_Click2异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    await DoAsync();
    Console.WriteLine("************btnAsync_Click2异步方法 结束,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    watch.Stop();
    Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}
/// <summary>
/// 异步方法
/// </summary>
/// <returns></returns>
private async Task DoAsync() {
    Action<string> action = new Action<string>(DoSomethingLong);
    List<IAsyncResult> results = new List<IAsyncResult>();
    for (int i = 0; i < 5; i++)
    {
        string name = string.Format("{0}_{1}", "btnAsync_Click", i);
        IAsyncResult result = action.BeginInvoke(name, null, null);
        results.Add(result);
    }
    await Task.Run(()=> {
        while (true)
        {
            for (int i = 0; i < results.Count; i++) {
                var result = results[i];
                if (result.IsCompleted) {
                    results.Remove(result);
                    break;
                }
            }
            if (results.Count < 1) {
                break;
            }
            Thread.Sleep(200);
        }
    });
}

经过优化,执行结果如下所示:

通过异步多线程优化后的执行结果,进行分析后得出的结论如下:

  1. Action的BeginInvoke,会返回IAsyncResult接口,通过接口可以判断是否完成。
  2. 如果有多个Action的多线程调用,可以通过List方式进行。
  3. async与await组合,可以实现异步调用,防止线程阻塞。
  4. 通过以上方式,采用异步多线程的方式,共耗时3.26秒,比同步方式的9.32秒,提高了2.85倍,并非线性增加。且每次执行的总耗时会上下浮动,并非固定值。

异步回调

上述async与await组合,是一种实现异步调用的方式,其实Action本身也具有回调函数【AsyncCallback】,通过回调函数一样可以实现对应功能。具体如下所示:

/// <summary>
/// 异步回调
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnAsync3_Click(object sender, EventArgs e)
{
    Stopwatch watch = Stopwatch.StartNew();
    watch.Start();
    Console.WriteLine("************btnAsync_Click3异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    Action action = DoAsync3;
    AsyncCallback asyncCallback = new AsyncCallback((ar) =>
    {
        if (ar.IsCompleted)
        {
            Console.WriteLine("************btnAsync_Click3异步方法 结束,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
            watch.Stop();
            Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
        }
    });
    action.BeginInvoke(asyncCallback, null);
}
private void DoAsync3()
{
    Action<string> action = new Action<string>(DoSomethingLong);
    List<IAsyncResult> results = new List<IAsyncResult>();
    for (int i = 0; i < 5; i++)
    {
        string name = string.Format("{0}_{1}", "btnAsync_Click3", i);
        IAsyncResult result = action.BeginInvoke(name, null, null);
        results.Add(result);
    }
    while (true)
    {
        for (int i = 0; i < results.Count; i++)
        {
            var result = results[i];
            if (result.IsCompleted)
            {
                results.Remove(result);
                break;
            }
        }
        if (results.Count < 1)
        {
            break;
        }
        Thread.Sleep(200);
    }
}

异步回调执行示例,如下所示:

通过对异步回调方式执行结果进行分析,结论如下所示:

  1. 通过观察线程ID可以发现,由于对循环计算的功能进行了封装,为一个独立的函数,所以在Action通过BeginInvoke发起时,又是一个新的线程。
  2. 通过async和await在通过Task.Run方式返回时,也会重新生成新的线程。
  3. 通过回调函数,可以保证异步线程的执行顺序。
  4. 通过Thread.Sleep(200)的方式进行等待,会有一定时间范围延迟。

异步信号量

信号量方式是通过BeginInvoke返回值IAsyncResult中的异步等待AsyncWaitHandle触发信号WaitOne,可以实现信号的实时响应,具体代码如下:

private void btnAsync4_Click(object sender, EventArgs e)
{
    Stopwatch watch = Stopwatch.StartNew();
    watch.Start();
    Console.WriteLine("************btnAsync_Click4异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    Action action = DoAsync3;
    var asyncResult = action.BeginInvoke(null, null);
    //此处中间可以做其他的工作,然后在最后等待线程的完成
    asyncResult.AsyncWaitHandle.WaitOne();
    Console.WriteLine("************btnAsync_Click4异步方法 结束,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    watch.Stop();
    Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}

信号量示例截图如下所示:

通过对异步信号量方式的测试结果进行分析,得出结论如下:

  1. 信号量方式会造成线程的阻塞,且会造成前端界面卡死。
  2. 信号量方式适用于异步方法和等待完成之间还有其他工作需要处理的情况。
  3. WaitOne可以设置超时时间【最多可等待时间】。

异步多线程返回值

上述示例的委托都是无返回值类型的,那么对于有返回值的函数,如何获取呢?答案就是采用Func。示例如下所示:

private void btnAsync5_Click(object sender, EventArgs e)
{
    Stopwatch watch = Stopwatch.StartNew();
    watch.Start();
    Console.WriteLine("************btnAsync5_Click异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    string name = string.Format("{0}_{1}", "btnAsync_Click5", 0);
    Func<string, int> func = new Func<string, int>(DoSomethingLongAndReturn);
    IAsyncResult asyncResult = func.BeginInvoke(name, null, null);
    //此处中间可以做其他的工作,然后在最后等待线程的完成
    int result = func.EndInvoke(asyncResult);
    Console.WriteLine("************btnAsync5_Click异步方法 结束,线程ID= {0},返回值={1}************", Thread.CurrentThread.ManagedThreadId,result);
    watch.Stop();
    Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}
private int DoSomethingLongAndReturn(string name)
{
    Console.WriteLine("************DoSomethingLong 开始 name= {0} 线程ID= {1} 时间 = {2}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"));
    //CPU计算累加和
    long rest = 0;
    for (int i = 0; i < 1000000000; i++)
    {
        rest += i;
    }
    Console.WriteLine("************DoSomethingLong 结束 name= {0} 线程ID= {1} 时间 = {2} 结果={3}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"), rest);
    return DateTime.Now.Day;
}

采用Func方式的EndInvoke,可以获取返回值,示例如下:

通过对Func方式的EndInvoke方法的示例进行分析,得出结论如下所示:

  1. 在主线程中调用EndInvoke,会进行阻塞,前端页面卡死。
  2. Func的返回值是泛型类型,可以返回任意类型的值。

异步多线程返回值回调

为了解决以上获取返回值时,前端页面卡死的问题,可以采用回调函数进行解决,如下所示:

private void btnAsync6_Click(object sender, EventArgs e)
{
    Stopwatch watch = Stopwatch.StartNew();
    watch.Start();
    Console.WriteLine("************btnAsync6_Click异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    string name = string.Format("{0}_{1}", "btnAsync_Click6", 0);
    Func<string, int> func = new Func<string, int>(DoSomethingLongAndReturn);
    AsyncCallback callback = new AsyncCallback((asyncResult) =>
    {
        int result = func.EndInvoke(asyncResult);
        Console.WriteLine("************btnAsync6_Click异步方法 结束,线程ID= {0},返回值={1}************", Thread.CurrentThread.ManagedThreadId, result);
        watch.Stop();
        Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
    });
    func.BeginInvoke(name, callback, null);
}

采用回调方式,示例截图如下:

通过对回调方式的示例进行分析,得出结论如下:

  1. 异步回调函数中调用EndInvoke,可以直接返回,不再阻塞。
  2. 异步回调方式,前端UI线程不再卡住。

备注

以上就是通过Action和Func的对应BeginInvoke方法实现异步多线程的示例,旨在抛砖引玉,大家共同学习,一起进步。

菩萨蛮·书江西造口壁

【作者】辛弃疾 【朝代】宋

郁孤台下清江水,中间多少行人泪?西北望长安,可怜无数山。

青山遮不住,毕竟东流去。江晚正愁余,山深闻鹧鸪。

相关文章
|
2月前
|
数据采集 XML JavaScript
C# 中 ScrapySharp 的多线程下载策略
C# 中 ScrapySharp 的多线程下载策略
|
26天前
|
设计模式 C# 开发者
C#设计模式入门实战教程
C#设计模式入门实战教程
|
15天前
|
安全 数据库连接 API
C#一分钟浅谈:多线程编程入门
在现代软件开发中,多线程编程对于提升程序响应性和执行效率至关重要。本文从基础概念入手,详细探讨了C#中的多线程技术,包括线程创建、管理及常见问题的解决策略,如线程安全、死锁和资源泄露等,并通过具体示例帮助读者理解和应用这些技巧,适合初学者快速掌握C#多线程编程。
49 0
|
26天前
|
开发框架 .NET Java
C#/.NET/.NET Core自学入门指南
C#/.NET/.NET Core自学入门指南
|
2月前
|
图形学 C# 开发者
全面掌握Unity游戏开发核心技术:C#脚本编程从入门到精通——详解生命周期方法、事件处理与面向对象设计,助你打造高效稳定的互动娱乐体验
【8月更文挑战第31天】Unity 是一款强大的游戏开发平台,支持多种编程语言,其中 C# 最为常用。本文介绍 C# 在 Unity 中的应用,涵盖脚本生命周期、常用函数、事件处理及面向对象编程等核心概念。通过具体示例,展示如何编写有效的 C# 脚本,包括 Start、Update 和 LateUpdate 等生命周期方法,以及碰撞检测和类继承等高级技巧,帮助开发者掌握 Unity 脚本编程基础,提升游戏开发效率。
40 0
|
2月前
|
开发者 iOS开发 C#
Uno Platform 入门超详细指南:从零开始教你打造兼容 Web、Windows、iOS 和 Android 的跨平台应用,轻松掌握 XAML 与 C# 开发技巧,快速上手示例代码助你迈出第一步
【8月更文挑战第31天】Uno Platform 是一个基于 Microsoft .NET 的开源框架,支持使用 C# 和 XAML 构建跨平台应用,适用于 Web(WebAssembly)、Windows、Linux、macOS、iOS 和 Android。它允许开发者共享几乎全部的业务逻辑和 UI 代码,同时保持原生性能。选择 Uno Platform 可以统一开发体验,减少代码重复,降低开发成本。安装时需先配置好 Visual Studio 或 Visual Studio for Mac,并通过 NuGet 或官网下载工具包。
56 0
|
2月前
|
机器学习/深度学习 Java TensorFlow
深度学习中的图像识别:从理论到实践Java中的多线程编程入门指南
【8月更文挑战第29天】本文将深入探讨深度学习在图像识别领域的应用,从基础理论到实际应用案例,带领读者一步步理解如何利用深度学习技术进行图像识别。我们将通过一个简单的代码示例,展示如何使用Python和TensorFlow库实现一个基本的图像识别模型。无论你是初学者还是有一定经验的开发者,都能从中获得启发和学习。 【8月更文挑战第29天】在Java世界里,线程是程序执行的最小单元,而多线程则是提高程序效率和响应性的关键武器。本文将深入浅出地引导你理解Java多线程的核心概念、创建方法以及同步机制,帮助你解锁并发编程的大门。
|
5月前
|
开发框架 前端开发 .NET
C#编程与Web开发
【4月更文挑战第21天】本文探讨了C#在Web开发中的应用,包括使用ASP.NET框架、MVC模式、Web API和Entity Framework。C#作为.NET框架的主要语言,结合这些工具,能创建动态、高效的Web应用。实际案例涉及企业级应用、电子商务和社交媒体平台。尽管面临竞争和挑战,但C#在Web开发领域的前景将持续拓展。
168 3
|
5月前
|
SQL 开发框架 安全
C#编程与多线程处理
【4月更文挑战第21天】探索C#多线程处理,提升程序性能与响应性。了解C#中的Thread、Task类及Async/Await关键字,掌握线程同步与安全,实践并发计算、网络服务及UI优化。跟随未来发展趋势,利用C#打造高效应用。
181 3