• 「C#」异步编程玩法笔记-Thread、ThreadPool、Task


    异步编程

    从 .NET Framework 4 开始,使用多线程的推荐方法是使用任务并行库 (TPL) 和并行 LINQ (PLINQ)
    任务并行库(TPL)说的是 System.Threading 和 System.Threading.Tasks 空间中的一组公共类型和 API。较为常见的就是Thread、ThreadPool、Task等
    LINQ (PLINQ) 是语言集成查询 (LINQ) 模式的并行实现,可以理解为对LINQ的一些扩充方法,类似于“IEnumerable .AsParallel()”方法。

    1、Thread和ThreadPoool

    1.1、线程Thread

    创建和控制线程,设置其优先级并获取其状态。
    使用举例:

    public static void ThreadProc()
    {
        for (int i = 0; i < 10; i++)
        {
            Console.WriteLine("ThreadProc: {0}", i);
            Thread.Sleep(1);
        }
    }
    
    public static void MainThread()
    {
        Console.WriteLine("Main thread Start.");
        Console.WriteLine("Creat a second thread: ThreadProc.");
        Thread t = new Thread(new ThreadStart(ThreadProc));
        Console.WriteLine("ThreadProc start.");
        t.Start();
    
        for (int i = 0; i < 4; i++)
        {
            Console.WriteLine("Main thread: Do some work.");
            Thread.Sleep(1);
        }
    
        Console.WriteLine("Main thread: Call Join(), to wait until ThreadProc ends.");
        t.Join();
        Console.WriteLine("Main thread: ThreadProc.Join has returned.  Press Enter to end program.");
        Console.ReadLine();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    ThreadStart为系统自带无参委托类型。
    Thread的构造函数有如下几种:

    public Thread(ThreadStart start)...    
    public Thread(ParameterizedThreadStart start)...
    public Thread(ThreadStart start, int maxStackSize)...
    public Thread(ParameterizedThreadStart start, int maxStackSize)...
    
    • 1
    • 2
    • 3
    • 4

    ParameterizedThreadStart为带一个可控object参数的委托类型。后两种构造函数中的maxStackSize指定线程的最大堆栈大小。
    Thread.Start()即启动线程
    Thread.Join()即将线程加入到当前线程中使同步,或者说就是等线程结束后继续执行主线程。
    Thread.Abort()是强制结束线程。
    其他方法参考:Thread类

    1.2、线程池ThreadPoool

    先说说进程,进程是一种正在执行的程序,操作系统使用进程来分隔正在执行的应用程序。

    可以把线程池比喻成公路,一个进程只有一条公路,一条公路(线程池)上可以有多个车道。即是说一个进程只能有一个线程池,而线程池中可以有多个线程,而具体可以有多少线程呢,是受到计算机内存等限制的。

    C#中可以使用 ThreadPool.GetMaxThreads 和 ThreadPool.SetMaxThreads 方法来控制最大线程数。
    使用示例:

    public static void ThreadProc(object stateInfo)
    {
        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine("ThreadProc: {0},stateInfo: {1}", i, stateInfo);
            Thread.Sleep(1);
        }
    }
    public static void MainThreadPool()
    {
        ThreadPool.QueueUserWorkItem(ThreadProc, "state");
        Console.WriteLine("Main thread does some work, then sleeps.");
        Thread.Sleep(1000);
    
        Console.WriteLine("Main thread exits.");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    输出:

    Main thread does some work, then sleeps.
    ThreadProc: 0,stateInfo: state
    ThreadProc: 1,stateInfo: state
    ThreadProc: 2,stateInfo: state
    ThreadProc: 3,stateInfo: state
    ThreadProc: 4,stateInfo: state
    Main thread exits.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ThreadPool为静态类,只能操作当前进程的线程池。
    常用方法除了GetMaxThreads() 和 SetMaxThreads()外,还有上面使用示例中出现的:
    QueueUserWorkItem(WaitCallback callBack, object state)
    QueueUserWorkItem(WaitCallback callBack)
    即将方法排入队列以便执行。 此方法在有线程池线程变得可用时执行。
    参数WaitCallBack是一个内置委托(如下👇),需要传递参数时使用第一种方法。

    delegate void WaitCallback(object? state);
    
    • 1

    2、Task

    这是 .NET Framework 4 中引入的基于任务的异步模式的中心组件之一,也是一部编程中优先推荐使用的。

    2.1、Task、Thread和ThreadPool

    前面已经简单说了Thread和ThreadPool了,而Task和这两个又有什么关系呢。

    首先这些都是多线程异步执行,Thread是创建新线程,ThreadPool是在已有的线程上处理事情。Task是借助ThreadPool(线程池)处理。线程池即为程序进程开辟的线程池子,可以将异步任务加入到已有的线程池中的线程上执行,线程池中的线程可以复用。理解为线程池是公路,线程是路上的车道,Task为运输车(任务),一个运输任务Task在一条车道上跑过后可以有别的Task去跑。而Thread则是每次都新挖一条车道跑完再把车道毁了。

    2.2、Task使用

    Task是更为灵活方便且被优先推荐的异步编程方式。这里说的Task包括Task(无返回值)和有返回值的 Task
    先说前者,
    用一个简单的例子说明使用方法:

    static void MainConstruct()
    {
        Action<object> action = (object obj) =>
        {
            Console.WriteLine("obj={0}, TaskId={1}, ThreadId={2}",
            obj, Task.CurrentId, 
            Thread.CurrentThread.ManagedThreadId);
        };
    
        Task t1 = new Task(action, "t1");
        Task t2 = Task.Factory.StartNew(action, "t2");
        t2.Wait();
    
        t1.Start();
        Console.WriteLine("t1 has been launched. (Main Thread={0})",Thread.CurrentThread.ManagedThreadId);
        t1.Wait();
    
        String taskData = "t3";
        Task t3 = Task.Run(() => {
            Console.WriteLine("obj={0}, TaskId={1}, ThreadId={2}",
                              taskData, Task.CurrentId,
                               Thread.CurrentThread.ManagedThreadId);
        });
        t3.Wait();
    
        Task t4 = new Task(action, "t4");
        t4.RunSynchronously();
        t4.Wait();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    输出如下:

    obj=t2, TaskId=1, ThreadId=3
    t1 has been launched. (Main Thread=1)
    obj=t1, TaskId=2, ThreadId=3
    obj=t3, TaskId=3, ThreadId=3
    obj=t4, TaskId=4, ThreadId=1
    
    • 1
    • 2
    • 3
    • 4
    • 5

    实例化任务

    Task任务的实例化可以使用构造函数以及 Task.Factory.StartNew(Action action, object? state)和Task.Run(Action action)静态方法来完成。

    不同的是:使用Task构造函数创建实例实际上是将任务的创建与执行分开,构造函数仅完成创建,任务的执行则需要Task实例通过Start()方法完成;而Task.Factory.StartNew()和Task.Run()在实例化任务时即让任务开始执行。

    执行任务

    前面说的Task.Factory.StartNew()和Task.Run()在实例化时即执行。除此之外:

    Task.Sart()是对已实例化但还没有开始执行,且线程状态时可以执行的任务进行启动。意义很简单,但条件较为严格。实例化的任务,如果已经开始或取消或执行结束,则不能调用Start()方法。其次Start()方法只能在任务实例化的上下文中调用。

    Task.RunSynchronously()方法也是启动,但不同的是,他将实例化任务强制已同步执行。

    任务结束

    如示例中的Task.Wait(),即在主线程(调用任务的线程)中等待任务结束。对于Task,Task.Result即等待任务结束并获取任务TResult类型的返回值。

    其次,Task.ContinueWith()是设置一个在任务结束后接下来要做的另一个任务,也是已异步方式运行的。

    取消任务

    使用Task执行异步时,取消没有像Thread.Abort()那样粗暴的方法。
    需要CancellationToken,即一个消息令牌。它由CancellationTokenSource 通知。

    public static void MainCancel()
    {
        CancellationTokenSource source = new CancellationTokenSource();
        CancellationToken token = source.Token;
        token.Register(() => { Console.WriteLine("Canceled"); });
        Task task = Task.Run(() =>
        {
            Console.WriteLine("Task Running...");
            long v = 0;
            for (int i = 0; i < 100; i++)
            {
                v = i * i;
                Thread.Sleep(200);
                Console.WriteLine("v = {0}", v);
                if (token.IsCancellationRequested)
                    break;
            }
            Console.WriteLine("Task End...");
        }, token);
        Thread.Sleep(1000);
        Console.WriteLine("Cancel Task");
        source.Cancel();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    CancellationToken可以理解为一个全局的变量,它有CancellationTokenSource创建并管理。
    但为什么不直接使用自定义的全局变量来控制线程结束呢?
    一方面CancellationToken是很多支持取消的内置异步方法必要参数(或者说指定取消方法),其次CancellationToken是单次的,即取消后不可重置,其他自定义变量是要具有set()方法就有可能被其他线程修改。并且当多线程操作同一数据时是有死锁的可能。

    CancellationTokenSource是线程安全的,最后CancellationTokenSource提供了较为灵活且丰富的其他方法,比如示例中的token.Register()注册取消回调方法等。

  • 相关阅读:
    电子电器架构——智能座舱设备终端
    业务“小程序化”或许是OA数字化升级突破口
    Spring七大模块详解
    Golang复习
    华为机试:最小传输时延
    破局「二次创业」:合思的新解法
    kubeadm部署k8s及高可用
    with recursive用法
    netty 拆包/粘包
    ✔ ★ 算法基础笔记(Acwing)(六)—— 贪心【java版本】
  • 原文地址:https://blog.csdn.net/Raink_LH/article/details/128003430