C#基础系列-多线程与异步

一、进程与线程

1、进程(Process):一个正在运行的应用程序在操作系统中被视为一个进程,包含着一个应用程序所需的资源,进程可以包含一个或多个线程。进程之间相互独立,一个进程无法访问另外一个进程的数据。不同应用程序之间的通讯、数据的请求等都可以看成不同进程间通讯。

2、线程(Thread):进程中的基本执行单元,操作系统分配CPU时间的基本单位,在进程入口运行第一个线程视为进程的主线程。

3、多线程:CPU运行速度过快,其他硬件(IO)处理速度跟不上,所以操作系统进行分时间片管理,宏观是多线程并发。由于硬件多核多CPU,一个CPU在一个时刻只能运行一个线程,多个CPU在同一时刻可以运行多个线程。

二、同步与异步方法

同步与异步是用于修饰方法,当一个方法调用的时候,调用者需要等待该方法执行完毕并且返回才能继续执行,该方法就是同步方法。当一个方法被调用时候立即返回(获取一个线程执行该方法内部业务)调用者不用等待该方法执行完毕,该方法就是异步方法。

三、线程的使用

.NET FrameWork使用线程进化过程由使用Thread类进行创建管理,因为Thread创建多个线程,系统开销大,所以使用池化技术;定义ThreadPool类,使用线程池的方式管理线程资源减少Thread的创建数量和管理Thread的成本,因为ThreadPool不能控制线程的执行顺序,并且不能获取线程池内取消/异常/完成的通知;基于上述的缺点NET4.0提出Task(任务)由ThreadPool进行封装,通过Task拥有线程池的优点,同时解决其不易控制的缺点,创建一个任务,执行一个任务,本质开启一个线程,通过任务的形式更好的控制线程的执行过程。.NET5.0通过关键字async/await可以像编写同步代码一样编写异步编程,其基于Task。

 1 /// <summary>
 2 /// Thread
 3 /// </summary>
 4 private static void StartThread()
 5 {
 6     // 创建一个线程、并且执行操作
 7     Thread thread = new Thread((obj) => {
 8         Console.WriteLine("开启一个线程操作");
 9         Console.WriteLine(obj);
10     });
11     thread.Start();
12     Console.WriteLine("主线程");
13 }
14 
15 /// <summary>
16 /// ThreadPool
17 /// </summary>
18 private static void StartThreadPool()
19 {
20     // 创建一个线程池、获取线程池线程、执行操作
21     // 不能控制线程池中线程的执行顺序,也不能获取线程池内线程取消/异常/完成的通知
22     for (var i = 0; i < 10; i++)
23     {
24         ThreadPool.QueueUserWorkItem(new WaitCallback((obj) =>
25         {
26             Console.WriteLine($"第{obj}任务");
27         }), i);
28     }
29     Console.WriteLine("主线程");
30 }
31 
32 /// <summary>
33 /// 先执行主线程、说明创建的线程不会阻止(不带返回值的task)
34 /// </summary>
35 private static void StartTask()
36 {
37     // 通过Task实例化一个线程,执行操作
38     Task task = new Task(() =>
39     {
40         Thread.Sleep(100);
41         Console.WriteLine($"hello, task的线程ID为{Thread.CurrentThread.ManagedThreadId}");
42     });
43     task.Start();
44 
45     //2.Task.Factory.StartNew(Action action)创建和启动一个Task
46     Task task1 = Task.Factory.StartNew(() =>
47     {
48         Thread.Sleep(100);
49         Console.WriteLine($"hello, task1的线程ID为{Thread.CurrentThread.ManagedThreadId}");
50     });
51 
52     //3.Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task
53     Task task2 = Task.Run(() =>
54     {
55         Thread.Sleep(100);
56         Console.WriteLine($"hello, task2的线程ID为{Thread.CurrentThread.ManagedThreadId}");
57     });
58     Console.WriteLine("主线程");
59 }
60 
61 /// <summary>
62 /// 带返回值的Task
63 /// </summary>
64 private static void StartTaskReturn()
65 {
66     Task<string> task = new Task<string>(() =>
67     {
68         Thread.Sleep(100);
69         return $"hello, task的线程ID为{Thread.CurrentThread.ManagedThreadId}";
70     });
71     task.Start();
72 
73     Task<string> task1 = Task.Factory.StartNew<string>(() =>
74     {
75         Thread.Sleep(100);
76         return $"hello, task1的线程ID为{Thread.CurrentThread.ManagedThreadId}";
77     });
78 
79     Task<string> task2 = Task.Run<string>(() =>
80     {
81         Thread.Sleep(100);
82         return $"hello, task2的线程ID为{Thread.CurrentThread.ManagedThreadId}";
83     });
84 
85     // 注意task.Resut获取结果时会阻塞线程,即如果task没有执行完成,会等待task执行完成获取到Result,然后再执行后边的代码
86     Console.WriteLine(task.Result);
87     Console.WriteLine(task2.Result);
88     Console.WriteLine(task1.Result);
89     Console.WriteLine("主线程");
90 }

 四、线程的阻塞

线程的阻塞是阻塞主线程的执行,执行完其他线程在执行主线程,在Thread中通过使用Join的方法,如果多个线程,多个线程调用Join,最后执行主线程。Thread的阻塞方式要依次调用Join方法,线程执行完成或者有一个执行完成,不能立即解除阻塞,Task中通过Wait(单个执行完毕)等同于Join、WaitAll(全部执行完毕)、WaitAny(存在一个执行完毕)来阻塞主线程。

/// <summary>
/// Thread阻塞
/// </summary>
private static void ThreadTest()
{
    Thread th1 = new Thread(() =>
    {
        Thread.Sleep(500);
        Console.WriteLine("线程1执行完毕!");
    });
    th1.Start();

    Thread th2 = new Thread(() =>
    {
        Thread.Sleep(100);
        Console.WriteLine("线程2执行完毕!");
    });
    th2.Start();

    //阻塞主线程
    th2.Join();
    th1.Join();
    Console.WriteLine("主线程执行完毕!");
}

/// <summary>
/// Wait/WaitAny/WaitAll
/// </summary>
private static void TasksTest()
{
    Task task1 = new Task(() => {
        Thread.Sleep(500);
        Console.WriteLine("线程1执行完毕!");
    });
    task1.Start();
    Task task2 = new Task(() => {
        Thread.Sleep(1000);
        Console.WriteLine("线程2执行完毕!");
    });
    task2.Start();

    //Wait(单个)
    //task1.Wait();
    //task2.Wait();

    // WaitAll(全部)
    // task1与task2都执行完成在执行主线程
    // Task.WaitAll(new Task[] { task1, task2 });
    // task1与task2有一个执行完成就执行主线程

    // WaitAny(有一个)
    Task.WaitAny(new Task[] { task1, task2 });
    Console.WriteLine("主线程执行完毕!");
}

五、线程的延续

如果想让线程执行完毕后,然后执行指向后续操作,使用Task的延续WhenAny(存在一个执行完成)、WhenAll(所有线程执行完成)、ContinueWith(单个线程执行完成)、该方法是非阻塞方法,主线程会先执行完成。

/// <summary>
/// Task的延续操作(WhenAny/WhenAll/ContinueWith)
/// </summary>
private static void TasksTest1()
{
    Task task1 = new Task(() => {
        Thread.Sleep(500);
        Console.WriteLine("线程1执行完毕!");
    });
    task1.Start();
    Task task2 = new Task(() => {
        Thread.Sleep(1000);
        Console.WriteLine("线程2执行完毕!");
    });
    task2.Start();

    /*
    Task.WhenAll(new Task[] { task1, task2 }).ContinueWith((t) =>
    {
        Console.WriteLine($"id={t.Id}");
        Thread.Sleep(100);
        Console.WriteLine("task1与task2都执行完成在执行这个操作");
    });
    */

    Task.WhenAny(new Task[] { task1,task2}).ContinueWith((t) =>
    {
        Console.WriteLine($"id={t.Id}");
        Thread.Sleep(100);
        Console.WriteLine("task1与task2有一个执行完成在执行这个操作");
    });
}

六、线程的取消

线程的取消,是线程执行过程中,退出执行,在Thread中通过使用标识符(变量)线程轮询变量,如果状态改变退出执行内容。在Task通过CancellationTokenSource类来取消任务执行。

/// <summary>
/// thread任务的取消
/// </summary>
public static void ThreadCancel()
{
    // 定义取消标识
    bool isStop = false;

    int index = 0;
    Thread th1 = new Thread(() =>
    {
        while (!isStop)
        {
            Thread.Sleep(1000);
            Console.WriteLine($"第{++index}次执行,线程运行中...");
        }
    });
    th1.Start();

    Thread.Sleep(3000);
    isStop = true;
}

/// <summary>
/// 任务取消
/// </summary>
private static void TaskCancel()
{
    CancellationTokenSource source = new CancellationTokenSource();
    int index = 0;
    //开启一个task执行任务
    Task task1 = new Task(() =>
    {
        while (!source.IsCancellationRequested)
        {
            Thread.Sleep(1000);
            Console.WriteLine($"第{++index}次执行,线程运行中...");
        }
    });
    task1.Start();

    //五秒后取消任务执行
    Thread.Sleep(5000);
    //source.Cancel()方法请求取消任务,IsCancellationRequested会变成true
    source.Cancel();
}

/// <summary>
/// Task任务取消并且执行callback
/// </summary>
private static void TaskCancelCallBack()
{
    // 创建任务取消令牌对象,传入任务,进行监控取消任务
    CancellationTokenSource source = new CancellationTokenSource();

    // 注册任务取消事件回调
    source.Token.Register(() =>
    {
        Console.WriteLine("任务取消后执行的操作");
    });

    int index = 0;

    //开启一个task执行任务
    Task task1 = new Task(() =>
    {
        while (!source.IsCancellationRequested)
        {
            Thread.Sleep(1000);
            Console.WriteLine($"第{++index}次执行,线程运行中...");
        }
    });

    task1.Start();
    //延时取消,效果等同于Thread.Sleep(5000);source.Cancel();
    source.CancelAfter(5000);
}
/// <summary>
/// 使用async和await异步读取文件内容
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
private async static Task<string> GetTextAsync(string fileName)
{
    using (FileStream fs = new FileStream(fileName, FileMode.Open))
    {
        var bytes = new byte[fs.Length];
        Console.WriteLine("开始读取文件内容");
        //ReadAync方法异步读取内容,不阻塞线程
        int len = await fs.ReadAsync(bytes, 0, bytes.Length);
        string result = Encoding.UTF8.GetString(bytes);
        
        return result;
    }
}

七、总结

异步方法,是通过创建线程的方式,在不阻塞主线程的情况下执行其他任务,可以用于处理本地IO和网络IO等耗时操作。多线程与异步的关系,多线程是实现异步的方式之一,异步多线程的目的之一,在实现异步的方式还可以通过使用将任务交于其他应用程序即其他进程来处理。多线程不但可以实现异步编程,还可以用于大数据量的分析,提高效率。

上一篇:Markdown复习


下一篇:qt5 qmake开发