C#学习(十三)——多线程与异步

一、什么是线程

程序执行的最小单元
一次页面的渲染、一次点击事件的触发、一次数据库的访问、一次登录操作都可以看作是一个一个的进程

在一个进程中同时启用多个线程并行操作,就叫做多线程
由CPU来自动处理
线程有运行、阻塞、就绪三态

代码示例:

class Program
{
    static void Main(string[] args)
    {
        Thread thread = new Thread(() =>
        {
            Print1();
        });

        thread.Start();

        for(int i = 0; i < 1000; i++)
        {
            Console.Write(0);
        }

        Console.Read();
    }
    
    static void Print1()
    {
        for (int i = 0;i < 1000; i++)
        {
            Console.Write(1);
        }
    }

}

运行结果为
示例代码运行结果
可以看到,在结果中,0和1的输出是交织在一起的,原因为两个线程交替着被运行,不断反复直到结束。
另外一个常用操作为Sleep();让时间暂停,使得线程进入静默状态。

二、前台线程、后台线程与线程池托管

代码举例:

class Program
{
    static void Main(string[] args)
    {
        Thread thread = new Thread(PrintHello);
        thread.Start();
        Console.WriteLine("退出主程序");
    }

    private static void PrintHello(object? obj)
    {
        while (true)
        {
            Thread.Sleep(1000);
            Console.WriteLine("Hello from PrintHello!");
        }
    }
}

运行会发现,即使主线程运行结束了,子线程依旧在持续运行;持续运行的子线程就称为前台线程
一般来说,只有等待前台线程运行完毕后,程序才可以进行关闭。
与之对应的是 后台线程,可以通过thread.IsBackground = true;//切换为后台线程将前台 线程切换到后台线程,这样再次运行会发现,当主线程结束后,后台线程就会被强制结束。

一般来说,前台线程用于需要时间比较长的等待业务,比如监听客户端请求,而后台线程适用于时较短的业务比如执行客户端发来的请求,后台进程不会影响程序的终止。
托管在线程池中的线程全部为后台线程
所有使用new Thread创建的线程默认均为前台线程

三、线程池

示例代码

for(int i = 0;i < 100; i++)
{
    ThreadPool.QueueUserWorkItem((o) =>
    {
        Console.WriteLine($"循环次数{i} 线程id {Thread.CurrentThread.ManagedThreadId}");
    });
}

线程池执行结果
可以看到执行结果出现了id重复的状况,原因就是线程池会重复使用已经完成的线程,极大节约硬件资源。
另外,可以看到,for循环有100次,但是从输出结果来看,只执行了十几次,原因为线程池创建的线程均为后台线程,只要主程序退出,线程池的后台线程就会被停止,而主程序main执行的时间很短,因此线程池内线程没有来得及执行就被停止了。

对于重要的并发量小的线程,需要手动创建管理,对于并发量大而又不太重要的线程,最好托管到线程池中。

四、结束线程与CancellationToken

不管程序有多少个进程,进程内部的资源都是被共享的。所以C#对进程的取消代码作了更高层次的抽象,把进程的取消过程封装成为了Token的形式,也就是CancellationToken(取消令牌)。不仅可以使用在多线程中,还可以用于异步操作。

class Program
{
    static void Main(string[] args)
    {
        CancellationTokenSource cts = new CancellationTokenSource();

        Thread thread = new Thread(() => { PrintHello(cts.Token); });
        thread.Start();

        //下载文件
        Thread.Sleep(5000);

        //关闭子进程
        //cts.Cancel();
        cts.CancelAfter(3000);//在下载完成后3s失效


        Console.WriteLine("退出主程序");
    }

    private static void PrintHello(CancellationToken tokenSource)
    {
        while (!tokenSource.IsCancellationRequested)
        {
            Thread.Sleep(1000);
            Console.WriteLine("Hello from PrintHello!");
        }
    }
}

五、Join与IsAlive

对于子线程执行时间不确定的情况,需要使用Join的方法,加入至主程序执行中,或者使用IsAlive方法进行判断

class Program
{
    static void Main(string[] args)
    {
        Thread thread = new Thread(() => { PrintHello(); });
        thread.Start();

        //方法一
        //thread.Join();

        //方法二
        while(thread.IsAlive)
        {
            Console.WriteLine("子线程仍在工作");
            Thread.Sleep(100);
        }

        Console.WriteLine("退出主程序");
    }

    private static void PrintHello()
    {
        int i = 0;
        while (i++ < 10)
        {
            Thread.Sleep(new Random().Next(100, 1000));
            Console.WriteLine("Hello from PrintHello!");
        }
    }
}

六、资源竞争与线程锁lock

使用线程可以并发的在CPU的核心中执行任务,最大化CPU的利用率,但是并发执行任务也可能产生各种各样的资源竞争问题。

举例:

    private static void AddText()
    {
        File.AppendAllText(@"D:\test.txt", $"开始{Thread.CurrentThread.ManagedThreadId}");
        Thread.Sleep(100);
        File.AppendAllText((@"D:\test.txt", $"结束{Thread.CurrentThread.ManagedThreadId}");
    }

当两个线程同时需要使用同一个文件资源时,产生资源竞争,导致系统崩溃。
因此必须保证同一时刻只能有一个线程访问资源,避免出现资源恶性竞争。
使用线程锁就可以解决

class Program
{
    static object lockedObj = new object();
    static void Main(string[] args)
    {
        for(int i = 0; i < 10; i++)
        {
            var t = new Thread(AddText); 
            t.Start();
        }

        Console.WriteLine("退出主程序");
    }

    private static void AddText()
    {
        lock(lockedObj)
        {
            File.AppendAllText(@"D:\test.txt", $"开始{Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(100);
            File.AppendAllText(@"D:\test.txt", $"结束{Thread.CurrentThread.ManagedThreadId}");
        }           
    }
}

七、异步

在之前项目中,我们实现的所有操作都是同步进行的,然而当有同时10000个请求发生时,会使得用户有很长的等待,服务器会等待数据库的响应,完成后反馈至用户。
而异步操作要实现,不要等待数据库,继续执行下一个请求,当数据返回数据以后,再回头继续处理上一个请求。
然而对于更高级别的数量请求,仅仅依靠异步也是不够的,因此需要:
异步服务+每个机器多开进程+多个机器组合实现;
K8s, Kubernetess容器化分布式部署;
.NET Core对容器化非常非常友好、支持度极高

八、异步编程Task

我们使用异步处理并行,使用多线程处理并发。

异步逻辑是要基于方法没有依赖关系的,例如

 class Program
 {
     static void Main(string[] args)
     {

         Calculate();
         Console.Read();

     }

     static void Calculate()
     {
         //Task->异步,Thread->线程
         Task.Run(() =>
         {
             Calculate1();
         });
         Task.Run(() =>
         {
             Calculate2();
         });
         Task.Run(() =>
         {
             Calculate3();
         });
     }

     static int Calculate1()
     {
         var result = 3;
         Console.WriteLine($"Calculate1: {result}");
         Task.Delay(2000);
         return result;
     }

     static int Calculate2()
     {
         var result = 4;
         Console.WriteLine($"Calculate2: {result}");
         Task.Delay(3000);
         return result;
     }

     static int Calculate3()
     {
         var result = 5;
         Console.WriteLine($"Calculate3: {result}");
         Task.Delay(1000);
         return result;
     }
 }

但如果是具有依赖关系的,例如,Caculate2()需要Caculate1()的结果,Caculate3需要Caculate1()和Caculate2()的结果,那么就需要做如下调整

class Program
{
    static void Main(string[] args)
    {

        Calculate();
        Console.Read();

    }

    static void Calculate()
    {
        //Task->异步,Thread->线程
        var task1 = Task.Run(() =>
        {
            return Calculate1();
        });

        var awaiter1 = task1.GetAwaiter();//获得异步等待对象
        awaiter1.OnCompleted(() =>
        {
            var result1 = awaiter1.GetResult();//获得异步逻辑的最终计算结果
            var task2 = Task.Run(() =>
            {
                return Calculate2(result1);
            });
            var awaiter2 = task2.GetAwaiter();
            awaiter2.OnCompleted(() =>
            {
                var result2 = awaiter2.GetResult();
                var result = Calculate3(result1, result2);
                Console.WriteLine(result);
            });
        });          
    }

    static int Calculate1()
    {
        var result = 3;
        Console.WriteLine($"Calculate1: {result}");
        Task.Delay(2000);
        return result;
    }

    static int Calculate2(int a)
    {
        var result = a * 2;
        Console.WriteLine($"Calculate2: {result}");
        Task.Delay(3000);
        return result;
    }

    static int Calculate3(int a, int b)
    {
        var result = a + b;
        Console.WriteLine($"Calculate3: {result}");
        Task.Delay(1000);
        return result;
    }
}

九、C#的异步 async/await

可以看到,上面的异步操作代码非常复杂繁琐,接下来使用async/await化解上面操作

同步方法
指程序调用某个方法,需要等待执行完成以后才进行下一步操作
异步方法
指程序调用某个方法的时候,不做任何等待,在处理完成之前就返回该方法,继续执行接下来的操作,即函数在执行完成前就可以先返回调用方,然后继续执行接下来的逻辑完成任务的函数

举例:

public async Task<int> DoSomethingAsync()
{
    //创建一个计算1万毫秒的任务
    Task<int> longRunningTask = LongRunningTaskAsync();
    //使用await执行这个任务
    int result = await longRunningTask;
    return result;
}
//假装计算1w毫秒,输出为1
private async Task<int> LongRunningTaskAsync()
{
    await Task.Delay(10000);//延迟10s
    return 1;
}

1.需要使用async关键词
2.返回类型为:voidTaskTask<T>IAsyncEnumerable<T>
3.命名规范:Async结尾
4.需要有await表达式
5.要有返回值
6.async函数只能被async函数调用

共有三个部分:
第一部分异步调用:Task<int> longRunningTask = LongRunningTaskAsync();
第二部分执行异步:int result = await longRunningTask;
第三部分异步方法:private async Task<int> LongRunningTaskAsync(){}

注:

  • [ 在函数声明中,async关键字要放到返回类型之前 ]
  • [ async函数本身不创建异步操作,只有在调用await的时候才会进行异步操作 ]

下面对之前的异步代码进行优化:

class Program
{
    static void Main(string[] args)
    {
        Calculate();
        Console.Read();
    }

    static async void Calculate()
    {
        var result1 = await Calculate1Async();

        var result2 = await Calculate2Async(result1);

        var result = await Calculate3Async(result1, result2);

        Console.WriteLine(result);          
    }

    static async Task<int> Calculate1Async()
    {
        var result = 3;
        Console.WriteLine($"Calculate1: {result}");
        await Task.Delay(2000);
        return result;
    }

    static async Task<int> Calculate2Async(int a)
    {
        var result = a * 2;
        Console.WriteLine($"Calculate2: {result}");
        await Task.Delay(3000);
        return result;
    }

    static async Task<int> Calculate3Async(int a, int b)
    {
        var result = a + b;
        Console.WriteLine($"Calculate3: {result}");
        await Task.Delay(1000);
        return result;
    }
}

十、Task VS. Thread

异步不是多线程!!!
异步用来处理并行,多线程用于处理并发

class Program
{
    static void Main(string[] args)
    {
        TaskTest();
        ThreadTest();
        
        Console.Read();
    }

    static void TaskTest()
    {
        var sw = new Stopwatch();
        sw.Start();

        for(int i = 0; i < 100; i++)
        {
            Task.Factory.StartNew(() => { });
        }

        sw.Stop();
        Console.WriteLine($"Task {sw.ElapsedMilliseconds}");
    }

    static void ThreadTest()
    {
        var sw = new Stopwatch();
        sw.Start();

        for (int i = 0; i < 100; i++)
        {
            new Thread(() => { }).Start();
        }

        sw.Stop();
        Console.WriteLine($"Thread {sw.ElapsedMilliseconds}");
    }
}

执行结果为
Task VS. Thread执行结果
可以看到Task的执行速度要远高于Thread!
异步并不会创建线程,只是通过主线程来执行,同时开出一条分路来执行其他任务,非同步分别执行。但是在最后会创建一个非常轻量级的Worker Thread,用于通知主程序异步结束,也称为回调Call Back.

  • 25
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值