1

【.NET 6】多线程的几种打开方式和代码演示 - WeskyNet

 1 year ago
source link: https://www.cnblogs.com/weskynet/p/16391095.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【.NET 6】多线程的几种打开方式和代码演示

前言:

多线程无处不在,平常的开发过程中,应该算是最常用的基础技术之一了。以下通过Thread、ThreadPool、再到Task、Parallel、线程锁、线程取消等方面,一步步进行演示多线程的一些基础操作。欢迎大家围观。如果大佬们有其他关于多线程的拓展,也欢迎在评论区进行留言,大佬们的知识互助,是.net生态发展的重要一环,欢迎大佬们进行留言,帮助更多的人。

本文章为了防爬虫,特此放上原文链接,如果大家在其他地方(博客园与CSDN以外的地方)搜索到,可以点以下链接,跳转回原文:

https://www.cnblogs.com/weskynet/p/16391095.html

以下博客内容使用的一些环境:

系统环境:WIN 10

.NET 环境: .NET 6

VS 环境:VS 2022

其他:没了

以下正文:

1、先创建一个.NET 6控制台项目,用来当做该博客文章的实验使用。

1995789-20220619173951035-1559459225.png

2、快速创建一个线程。ParameterizedThreadStart是一个委托,传入的参数是一个object类型。

1995789-20220619174203004-1430117573.png
ParameterizedThreadStart threadStart = new((obj) => {
    Console.WriteLine($"当前线程 的 ID = {Thread.CurrentThread.ManagedThreadId}");
});

Thread thread = new Thread(threadStart);
thread.Start();
Console.WriteLine($"线程ID  = {thread.ManagedThreadId}");
Console.ReadLine();

3、以上代码执行结果下图所示

1995789-20220619174245499-1695192362.png

4、新建一个类TestThread,以及一个测试方法,用来做测试使用。

1995789-20220619174351988-895328050.png

5、在program里面,把输出改成调用上面的方法再进行测试一下。

1995789-20220619174430647-1040246743.png

6、执行以后的输出结果,如下图所示

1995789-20220619174545222-920748302.png

7、线程的等待(睡眠)。最简单的方式,是直接 Thread.Sleep(毫秒);

1995789-20220619174619019-1368937872.png

8、Thread的Join方法。代表线程执行完毕以后,才可以继续执行后续的代码。如下图所示,在thread线程内部执行完成以后,很快就接着执行最后的打印输出方法了。可以和以上的第7点进行比较输出结果。

1995789-20220619174708098-1800142566.png

9、Thread的Join方法,还可以传入参数,参数是毫秒值。代表等下当前线程执行多长时间,如果超出设定的毫秒数,就不等了,直接执行后续的代码。

1995789-20220619174930302-864998656.png

10、新增一个Test2方法,用来测试线程池ThreadPool使用。

1995789-20220619175059001-253461188.png

11、WaitCallback也是一个委托。传入需要在线程池内执行的方法名称。以下代码内,“线程池”字符串为执行的方法对应的参数。

1995789-20220619175200463-963332021.png
using MultiThread;

Console.WriteLine("Hello, World!");


ThreadPool.QueueUserWorkItem(new WaitCallback(TestThread.Test2),"线程池");

Console.ReadLine();

12、除了直接传入回调方法,也可以直接在线程池开启的方法内,直接写代码块来当做多线程执行的部分。如下图所示,睡眠1000ms以及执行的方法,在线程池内运行。

1995789-20220619175218948-101441979.png

13、线程池内,可以通过设置Manual信号量,来识别线程池内的线程时候执行完成。一般用 .Set(); 和 .WaitOne(); 结对进行,如下图代码、注释部分以及执行结果。(可以对比输出时间)

1995789-20220619175548938-1906070785.png

14、使用Task快读创建一个线程。如下图所示。最简单的方法:Task.Run(()=>{ 代码块; });  

1995789-20220619175845939-1224906935.png

15、也可以用以下方式,手动进行start启动,如图的代码所示。

1995789-20220619180027592-478142089.png

16、也可以使用Task.Factory创建一个任务工厂来实现。

1995789-20220619180206958-360065559.png

17、如果需要等待子线程执行完毕,才执行后续操作,可以使用Wait(); 来实现。

1995789-20220619180304716-968154227.png

18、如果只想等待子线程执行指定的时间,可以通过使用 Wait(毫秒数); 来实现。这样等待,例如500ms以后,不管子线程是不是还在浪,都不会等待,直接继续执行后续代码。

1995789-20220619180509115-1866167718.png

19、 如果要在等待一段时间以后执行某些当做,可以使用Task.Delay(时间毫秒数).ContinuwWith( 要执行的代码块);如下图所示的代码、注释以及运行输出结果。

1995789-20220619180625345-689136003.png

20、如果有多个任务在执行期间,在任意一个线程执行完毕以后进行执行某种操作,可以使用 ContinueWhenAny来进行。如下图所示的代码、注释和运行结果,以及图后附有源码。

1995789-20220619180808946-770014677.png
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")} >>>Hello, World!");

Task[] tasks = new Task[3];
TaskFactory factory = new();
tasks[0] = factory.StartNew(x => {
    Thread.Sleep(1000);
    Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")} >>> tasks 0");
},null);

tasks[1] = factory.StartNew(x => {
    Thread.Sleep(2000);
    Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")} >>> tasks 1");
}, null);

tasks[2] = factory.StartNew(x => {
    Thread.Sleep(3000);
    Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")} >>> tasks 2");
}, null);

factory.ContinueWhenAny(tasks, x =>
{
    Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")} >>> 我不晓得要打印啥子 ~ ~ ");

});

Console.ReadLine();

21、如果要等任务全部执行完毕以后才执行某个代码块,可以使用ContinueWhenAll。

1995789-20220619181013623-968268104.png

22、使用TaskWaitAny() 也可以实现任意任务执行完毕以后,执行后续动作。但是会占用主线程资源。如图所示代码,大佬们应该可以看出来为什么了。

1995789-20220619181108909-1341888429.png

23、同样的,Task也可以在等待全部任务执行完毕以后进行执行后续动作。如下图演示。

1995789-20220619181240552-1320038744.png

24、Parallel允许线程并行执行。同时最大线程执行数量,类似于ThreadPool可以设置最大并发数量类似。其他不多说,看以下的代码和演示效果。

1995789-20220619181339764-174742639.png
using MultiThread;

Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")} >>>Hello, World!");

ParallelOptions parallelOptions = new();
parallelOptions.MaxDegreeOfParallelism = 3;
Parallel.Invoke(parallelOptions,
    () =>
    {
        Thread.Sleep(1000);
        Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")} >>>  para1");
    },
    () =>
    {
        Thread.Sleep(2000);
        Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")} >>> para2");
    },
    () =>
    {
        Thread.Sleep(3000);
        Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")} >>> para3");
    });
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")} >>> 我不晓得要打印啥子 ~ ~ ");

Console.ReadLine();

25、Parallel也可以遍历执行。

1995789-20220619181831928-643276326.png
using MultiThread;

Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")} >>>Hello, World!");

ParallelOptions parallelOptions = new();
parallelOptions.MaxDegreeOfParallelism = 3;
Parallel.For(0, 10,parallelOptions, s =>
{
    Thread.Sleep(100);
    Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")} >>>  para{s}");
});

Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")} >>> 我不晓得要打印啥子 ~ ~ ");

Console.ReadLine();

26、新增一个方法,用来测试多线程锁使用。

1995789-20220619181919655-49293905.png

27、在不加锁的情况下执行执行以下代码,方法体几乎同时被执行。但是实际上方法体如果只允许被同时一个线程访问的话,那么这样搞肯定是会乱子的,所以需要锁。

1995789-20220619182001698-1153893861.png

28、加了锁以后,查看到执行的结果,时间间隔基本上是1s左右,说明该方法体确实一次只被一个线程调用了。

1995789-20220619182125900-1913306315.png

29、另一种锁(原子锁),可以定义一个变量来进行原子交换。它的使用场景,一般是在轮询进行处理某些业务的时候,并且同时只允许一个线程进来,就可以使用这种锁。

和lock锁区别:lock锁是代码还没执行完,线程会一直等待,等执行完了就会继续进来。如果线程一直被创建,lock外边会堆积越来越多的线程和资源,最严重的情况会导致系统内存不断飙升直到爆满;原子锁的作用是,用于验证代码块是不是执行完了,还没执行完,就不鸟他了,线程也不会等待下去,而是直接跳过这部分的代码,继续执行后续的操作。如果后续没事情做了,那该干嘛干嘛了。

1995789-20220619182229485-2097962195.png

30、原子锁执行效果如下,一部分线程判断到代码被锁住,就跳过不管了,所以就不会有输出。

1995789-20220619182750893-257754468.png

31、测试线程取消。先开启一些线程,以及有关的操作,如下图所示。

1995789-20220619182849616-1295284123.png

32、然后执行。结果比较尴尬,显示都是第100号线程,这是因为Task是多线程,在创建过程中,可能已经让i都执行到头了,所以再次获取到的i都是最后的值,即100.

1995789-20220619182939614-1870605310.png

33、在创建任务之前,引入一个中间变量,用来代替被遍历的i。然后执行结果和其他代码说明,如图所示。

1995789-20220619183115788-321901264.png

34、看不到异常信息,那改成Task直接走一波,然后通过Task.WaitAll();进行捕捉异常信息。如代码注释和演示截图所示。

1995789-20220619183216427-1323264948.png
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")} >>>Hello, World!");
try
{
    Task[] tasks = new Task[100];
    CancellationTokenSource cancellation = new CancellationTokenSource();
    for (int i = 0; i < 100; i++)
    {
        string str = i.ToString();
        tasks[i]= Task.Run(() =>
        {
            Thread.Sleep(100);
            try
            {
                if (str == "10")
                {
                    throw new Exception($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")}  >>> 第 -{str}- 号线程开始放弃治疗~~  线程ID  = {Thread.CurrentThread.ManagedThreadId}");
                }
            }
            catch (Exception ex)
            {
                cancellation.Cancel(); // 捕获异常,线程后续所有的线程都取消操作
                Console.WriteLine(ex.Message);
            }
            cancellation.Token.ThrowIfCancellationRequested();
            if (cancellation.IsCancellationRequested == false)  // 默认为false,代表正常
            {
                Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")}  >>> 第 -{str}- 号线程执行正常~~  线程ID  = {Thread.CurrentThread.ManagedThreadId}");
            }
            else
            {
                Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")}  >>> 第 -{str}- 号线程执行异常~~  线程ID  = {Thread.CurrentThread.ManagedThreadId}");
            }
        }, cancellation.Token);
    }
    Task.WaitAll(tasks);
    Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")} >>> 我不晓得要打印啥子 ~ ~ ");

}
catch (AggregateException ae)
{
    foreach (var ex in ae.InnerExceptions)
    {
        Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff")} >>> {ex.Message}");
    }
}

Console.ReadLine();

35、以上就是该偏文章的全部内容。如果对你有帮助,欢迎点赞、转发、或留言。如需转发,请注明出处:https://www.cnblogs.com/weskynet/p/16391095.html

如果想和我一起吹牛谈人生聊技术,或者和其他小伙伴一起吹牛谈人生聊技术,也可以扫以下的二维码加我微信好友,我不会介意的:

1995789-20220619183549602-988186522.png

或者也可以在该文章的原文【https://www.cnblogs.com/weskynet/p/16391095.html】里面,点击最下方的QQ群组链接,加入QQ群,我也不介意。

没了,最后祝大家撸码愉快~~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK