08-05 多线程之Task高级篇

25 篇文章 2 订阅

一、Parallel

1、推出时间

Parallel是.netframework 4.5版本推出
  1. Parallel 并发执行多个Action,多线程执行,线程id也不相同
  2. 主线程参与计算,所以会阻塞主线程,卡顿界面
  3. 相当于Task.WaitAll()
/// <summary>
/// .netframework 4.5版本推出parallel
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button7_Click(object sender, EventArgs e)
{
    Console.WriteLine($"==================button7_Click start  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}========================");

    {
        Parallel.Invoke(() => DoSomething("button7_Click1"),
                        () => DoSomething("button7_Click2"),
                        () => DoSomething("button7_Click3"),
                        () => DoSomething("button7_Click4"),
                        () => DoSomething("button7_Click5"),
                        () => DoSomething("button7_Click6"));
    }

    {
        Parallel.For(0, 5, i => DoSomething($"button7_Click{i + 1}"));
    }

    {
        int[] arr = new int[] { 1, 2, 3, 4, 5 };
        Parallel.ForEach(arr, i => DoSomething($"button7_Click{i}"));
    }
    Console.WriteLine($"==================button7_Click end  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}========================");
}

三种方式执行结果相同,都类似下图(区别就在于线程顺序不同)
在这里插入图片描述

2、控制线程数量

{
    //控制线程数量
    ParallelOptions options = new ParallelOptions();
    //设置最大线程数
    options.MaxDegreeOfParallelism = 3;
    Parallel.For(0, 10, options, i => DoSomething($"button7_Click{i}"));
}

由结果可以看出,始终是三个线程,并且某个线程结束后,又会重新被利用,其中主线程01也参与计算,被重复利用
在这里插入图片描述

  • 思考:由于上面的结果是主线程参与计算,造成界面卡顿,如何让主线程不参与计算?
  • 思路:不在主线程中控制子线程的数量,在一个子线程中控制线程数量,并执行相关Action
{
    //如何让主线程参与计算,将控制线程数量放到一个子线程中
    Task.Run(() =>
    {
        Console.WriteLine($"当前线程id为: {Thread.CurrentThread.ManagedThreadId.ToString("00")} start");
        //控制线程数量
        ParallelOptions options = new ParallelOptions();
        //设置最大线程数
        options.MaxDegreeOfParallelism = 3;
        Parallel.For(0, 10, options, i => DoSomething($"button7_Click{i}"));
        Console.WriteLine($"当前线程id为: {Thread.CurrentThread.ManagedThreadId.ToString("00")} end");
    });
}

结果显示,主线程没有参与计算
在这里插入图片描述

二、多线程进阶

1、多线程异常获取

在子线程内部,发生异常之后,异常会被吞掉,线程自动中断(类似于Thread thread = null; thread.Abort()

/// <summary>
/// 进阶篇
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button8_Click(object sender, EventArgs e)
{
    Console.WriteLine($"==================button8_Click start  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}========================");
    #region 多线程异常处理
    {
        //在子线程内部,发生异常之后,异常会被吞掉,线程自动中断
        //Thread thread = null;
        可以停止线程:抛出了System.Threading.ThreadAbortException
        //thread.Abort();  
        for (int i = 0; i < 20; i++)
        {
            string buttonName = $"button8_Click{i}";
            Task.Run(() =>
            {
                if (buttonName == "button8_Click4")
                {
                    throw new Exception("button8_Click4 出错了。。。");
                }
                else if (buttonName == "button8_Click6")
                {
                    throw new Exception("button8_Click6 出错了。。。");
                }
                else if (buttonName == "button8_Click15")
                {
                    throw new Exception("button8_Click15 出错了。。。");
                }
                Console.WriteLine($"{buttonName}执行成功!线程id 为{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
            });
        }
    }
    #endregion

    Console.WriteLine($"==================button8_Click end  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}========================");
}

编译之后,运行编译成功的exe文件,得到如下结果
在这里插入图片描述
思考:如何捕捉子线程中的异常呢?
(1)需要将所有的Task存入一个集合
(2)需要使用Task.WaitAll()等待,就可以捕捉到异常
(3)可以通过多个catch获取异常,捕捉异常的原则是:先具体再全部
(4)AggregateException是特指多线程的异常,因此可以通过该异常的InnerExceptions对象获取所有子线程的异常

try
{
    List<Task> taskList = new List<Task>();
    for (int i = 0; i < 20; i++)
    {
        string buttonName = $"button8_Click{i}";
        taskList.Add(Task.Run(() =>
        {
            if (buttonName == "button8_Click4")
            {
                throw new Exception("button8_Click4 出错了。。。");
            }
            else if (buttonName == "button8_Click6")
            {
                throw new Exception("button8_Click6 出错了。。。");
            }
            else if (buttonName == "button8_Click15")
            {
                throw new Exception("button8_Click15 出错了。。。");
            }
            Console.WriteLine($"{buttonName}执行成功!线程id 为{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        }));
    }
    Task.WaitAll(taskList.ToArray());
}
catch (AggregateException aex)
{
    //通过InnerExceptions获取到所有子线程中的异常
    foreach (var item in aex.InnerExceptions)
    {
        Console.WriteLine(item.Message);
    }
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

在这里插入图片描述
在这里插入图片描述
如果直接抓取全部类型异常:
在这里插入图片描述
抓取结果如下
在这里插入图片描述

2、线程取消

场景分析:如果多线程处理业务时,有一个线程异常了,对整个业务完整性就造成了破坏,就需要由一些应对策略,比如将其他的线程也停止,重新操作

(1)全局变量法

bool ok = true;
for (int i = 0; i < 20; i++)
{
    string buttonName = $"button8_Click{i}";
    Task.Run(() =>
    {
        
        if (buttonName == "button8_Click4")
        {
            ok = false;
            throw new Exception("button8_Click4 出错了。。。");
        }
        else if (buttonName == "button8_Click6")
        {
            throw new Exception("button8_Click6 出错了。。。");
        }
        else if (buttonName == "button8_Click15")
        {
            throw new Exception("button8_Click15 出错了。。。");
        }
        if (!ok)
        {
            throw new AggregateException();
        }
        Console.WriteLine($"{buttonName}执行成功!线程id 为{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
    });
}

结果是只有一个线程执行成功,其他的都被取消了(原本是之后出现异常的线程会自动中断),PS:每此执行的结果不同,因为每次线程顺序不同
在这里插入图片描述

(2)CancellationTokenSource对象

  • IsCancellationRequested默认值为false
  • 只要在执行了Cancel()方法,IsCancellationRequested值就指定为trueCancel()方法可以重复使用。
  • CancellationTokenSource是线程安全的,Cancel()被调用后IsCancellationRequested值就指定为true是不能被重置为false的
  • 如果在Cancel之前已经进入业务处理的线程是不能停止的,所以在最后再判断一次,不让业务正常结束即可
CancellationTokenSource source = new CancellationTokenSource();
for (int i = 0; i < 20; i++)
{
    string buttonName = $"button8_Click{i}";
    Task.Run(() =>
    {
        Thread.Sleep(1000);
        if (!source.IsCancellationRequested)
        {
            Console.WriteLine($"{buttonName} start  {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        }
        else
        {
            Console.WriteLine($"{buttonName} 失败  {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        }
        if (buttonName == "button8_Click4")
        {
            source.Cancel(); //IsCancellationRequested值就指定为true
                throw new Exception("button8_Click4 出错了。。。");
        }
        else if (buttonName == "button8_Click6")
        {
            source.Cancel(); //IsCancellationRequested值就指定为true
                throw new Exception("button8_Click6 出错了。。。");
        }
        else if (buttonName == "button8_Click15")
        {
            source.Cancel(); //IsCancellationRequested值就指定为true
                throw new Exception("button8_Click15 出错了。。。");
        }

            // 如果有线程被取消,就取消业务逻辑执行
            if (source.IsCancellationRequested)
        {
            Console.WriteLine($"{buttonName}取消执行业务逻辑 {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        }
        else
        {
            Console.WriteLine($"{buttonName}执行业务逻辑 {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        }
    });
}

当某个线程出现异常后,在其之后的线程都会被取消(但是已经成功的线程不能再被取消了)
在这里插入图片描述

PS:这里不局限于出现异常再Cancel,也可以根据业务场景判断 Cancel线程

<1> Token属性

思考:多个线程,其中一个启动了,其他的还没启动,刚好启动的这个线程异常了,还没有启动的线程还有必要启动吗?答案是没必要,那么如何实现?
思路: 利用CancellationTokenSource对象的Token属性,就可以解决上述问题

为了测试效果,我们做下面三步工作(实际开发中不需要):

  • 将所有Task存储到一个集合tasks
  • Task.WaitAll(tasks.ToArray())等待所有线程
  • try catch 抓取异常

重点

  1. 在异常中或者业务逻辑需求中取消当前线程source.Cancel();
  2. 将source.Token传递给Task.Run()
try
{
    CancellationTokenSource source = new CancellationTokenSource();
    List<Task> tasks = new List<Task>();
    for (int i = 0; i < 20; i++)
    {
        string buttonName = $"button8_Click{i}";
        //为了效果,随机休眠
        Thread.Sleep(new Random().Next(100, 300));
        tasks.Add(Task.Run(() =>
        {
            Console.WriteLine($"{buttonName} start  {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
            try
            {
                if (buttonName == "button8_Click4")
                {
                    throw new Exception("button8_Click4 出错了。。。");
                }
                else if (buttonName == "button8_Click6")
                {
                    throw new Exception("button8_Click6 出错了。。。");
                }
                else if (buttonName == "button8_Click15")
                {
                    throw new Exception("button8_Click15 出错了。。。");
                }
            }
            catch (Exception ex)
            {
                //加入线程中有异常,则取消该线程 (关键)
                source.Cancel();
                throw ex;
            }
            Console.WriteLine($"{buttonName}执行业务逻辑 {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        }, source.Token)); //将ource.Token给Task.Run,则有线程出现异常,后面的线程都不会开启 (关键)
    }
    Task.WaitAll(tasks.ToArray());
}
catch (AggregateException ex)
{
    foreach (var item in ex.InnerExceptions)
    {
        Console.WriteLine(item.Message);
    }
}

在这里插入图片描述
运行结果符合预期:出现异常的线程后面的线程全部不会开启
在这里插入图片描述
去掉外层try catch
在这里插入图片描述
在这里插入图片描述
这跟不带Token的区别就在于:

  • 不带Token的是异常线程后面的线程会开启,然后人为阻止业务模块运行
  • 而带Token的是异常线程后面的线程不会被开启,自然不会执行业务模块

3、临时变量

for (int i = 0; i < 5; i++)
{
    //定义临时变量,作用域当前作用域
    int k = i;
    Task.Run(() =>
    {
        Console.WriteLine($"线程id: {Thread.CurrentThread.ManagedThreadId.ToString("00")},i={i},k={k}");
    });
}

仔细观察ik的值,为什么会不一样?
在这里插入图片描述
原因:

  1. 线程开启是的非阻塞的,而且会延迟启动
  2. 这里循环了多次,因为线程开启是非阻塞的,又延迟启动,而循环有非常快,所以当线程真正开启的时候,已经循环了很多次了,那么i就是线程真正开启的时候的i的值 。(具体每个线程的i值是多少,跟循环次数和电脑性能有关,有可能所有线程的i值一样,也可能不一样)
  3. 临时变量k是每一次循环都重新定义,只作用于当前的作用域,所以k值就是真正的线程索引

4、线程安全

线程安全:单线程的计算结果与多线程计算结果完全一致,则为线程安全,反之则为线程不安全

常见的线程安全场景:

  • 全局变量(共享变量)的操作
  • 硬盘文件的操作
  • 多线程都能访问和修改的公共数据
int syncCount = 0;
int asyncCount = 0;
//单线程
for (int i = 0; i < 10000; i++)
{
    syncCount++;
}
List<Task> tasks = new List<Task>();
//多线程
for (int i = 0; i < 10000; i++)
{
    tasks.Add(Task.Run(() =>
    {
        asyncCount++;
    }));
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine($"syncCount={syncCount},asyncCount={asyncCount}");

思考:以上代码输出什么结果?是syncCount=10000,asyncCount=10000吗?

下图是点击多次之后输出的多次执行结果:

  • syncCount始终等于10000,因为syncCount是单线程运算
  • asyncCount每一次的执行结果都不相同,为什么?因为线程安全问题。
    在这里插入图片描述
    思考:为什么会出现线程不安全?
    因为多线程操作,操作同时进行,可能会出现覆盖(栈),所以导致线程不安全

(1)如何解决线程安全问题

<1>推荐方法
  1. 引入System.Collections.Concurrent 命名空间–线程安全数据结构
  2. 把非线程安全的数据结构替换成下面的数据结构即可
  • BlockingCollection<T> 为实现 IProducerConsumerCollection<T> 的线程安全集合提供阻塞和限制功能。
  • ConcurrentBag<T> 表示对象的线程安全的无序集合。
  • ConcurrentDictionary<TKey, TValue> 表示可由多个线程同时访问的键值对的线程安全集合。
  • ConcurrentQueue<T> 表示线程安全的先进先出 (FIFO) 集合。
  • ConcurrentStack<T> 表示线程安全的后进先出 (LIFO) 集合。
  • OrderablePartitioner<TSource> 表示将一个可排序数据源拆分成多个分区的特定方式。
  • Partitioner 提供针对数组、列表和可枚举项的常见分区策略。
  • Partitioner<TSource> 表示将一个数据源拆分成多个分区的特定方式。
List<int> list = new List<int>();
//线程安全集合
BlockingCollection<int> blockings = new BlockingCollection<int>();
//线程安全包
ConcurrentBag<int> concurentbags = new ConcurrentBag<int>();
//线程安全字典
ConcurrentDictionary<string, int> concurrentDictionary = new ConcurrentDictionary<string, int>();
//线程安全队列
ConcurrentQueue<int> concurrentQueue = new ConcurrentQueue<int>();
//线程安全栈
ConcurrentStack<int> concurrentStack = new ConcurrentStack<int>();
for (int i = 0; i < 10000; i++)
{
    int k = i;
    Task.Run(() =>
    {
        list.Add(k); ;
        blockings.Add(k);
        concurentbags.Add(k);
        concurrentDictionary.TryAdd($"concurrentDictionary_{k}", k);
        concurrentQueue.Enqueue(k);
        concurrentStack.Push(k);
    });
}
Console.WriteLine($"list.count={list.Count}");
Console.WriteLine("===========================================");
Console.WriteLine($"blockings.count={blockings.Count}");
Console.WriteLine("===========================================");
Console.WriteLine($"concurentbags.count={concurentbags.Count}");
Console.WriteLine("===========================================");
Console.WriteLine($"concurrentDictionary.count={concurrentDictionary.Count}");
Console.WriteLine("===========================================");
Console.WriteLine($"concurrentQueue.count={concurrentQueue.Count}");
Console.WriteLine("===========================================");
Console.WriteLine($"concurrentStack.count={concurrentStack.Count}");

在这里插入图片描述

<2>lock–锁(不推荐)

因为锁具有排他性(独占性),是反多线程的,所以不推荐使用

  1. 定义标准锁(全局变量)private static readonly object lockObj = new object(); 要用引用类型,不要用string类型,string类型可能会冲突,也不要用int,this等
  2. 然后用lock加锁
//定义私有全局变量
private static readonly object lockObj = new object();

{
    List<int> list = new List<int>();
    for (int i = 0; i < 10000; i++)
    {
        int k = i;
        Task.Run(() =>
        {
            //加锁之后,只允许一个线程操作,以独占的方式控制线程操作,一个线程操作完成之后,另一个线程才能操作,其实就是变成了单线程操作
            lock (lockObj)
            {
                list.Add(k);
            }
        });
    }
    Console.WriteLine($"list.Count={list.Count}");
}

结果:
在这里插入图片描述

<3>其他方案

思路:多线程操作会出现线程安全问题,那么可以拆分数据源,然后每一个线程操作单独的某个数据块,多线程操作完毕后,在合并数据。

例子待续。。。。

三、本文代码

多线程Task

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值