多线程开发(1)

多线程开发

异步:Async与Await简单用法

简单写法:

  1. 创建方法使用Async进行修饰,
  2. 在方法内部耗时操作前面使用Await修饰。

代码如下:

        public Form2()
        {
            InitializeComponent();         
        }

        private async void TaskTestFun()
        {
            var task = await Task.Run(Fun).ConfigureAwait(true);
            Console.WriteLine(task.ToString());
            var d = 1;
            return;
        }

        public string Fun()
        {
            Thread.Sleep(5000);
            return "Sleep 5000 has Finished";
        }

        private void button1_Click(object sender, EventArgs e)
        {
            TaskTestFun();
        }

运行现象描述:

当主进程进入TaskTestFun方法后,执行到创建Task方法后,就空闲了(表现为可以被拖动),这个时候,程序的运行指针还在var task那行代码上。

Task方法执行的时候,自己创建了一个工作子线程,用来执行动作,当动作执行完了以后,通知主进程获取var task的结果,让主线程继续执行下去。

图解如下:

  1. 当前主线程进入TaskTestFun()

image-20230414154446594

  1. 主线程执行Task.Run方法(创建一个新的工作线程)

image-20230414154514709

  1. 当前主界面可以拖动()说明主线程空闲,没有被阻塞=非阻塞

  2. 当Task任务结束即将以后

    image-20230414154840180

  3. 执行task完Return以后,工作子线程被销毁

    image-20230414154959061

  4. 最后输出子线程返回的信息

    image-20230414155045435

多线程并发(所有线程全结束了,才能继续往下运行)

要点:

  1. 创建List集合
  2. 使用WhenAll方法,等待所有的线程完成。

特点:这里跑的快的线程,先结束,然后要等待跑得慢的线程。大家一起结束才能继续往下面进行

    public partial class Form2 : Form
    {

        public Form2()
        {
            InitializeComponent();
            InitializeList();
        }

        private void InitializeList()
        {
            for (int i = 0; i < 9; i++)
            {
                Targetlist.Add($" 任务{i} ");
            }
        }

        private List<string> Targetlist = new List<string>();

        List<Task<string>> Tasks = new List<Task<string>>();

        private async void TaskTestFun()
        {
            Targetlist.ForEach(ItemActivation => Tasks.Add(Task.Run(Fun))) ;
            var task = await Task.WhenAll(Tasks).ConfigureAwait(false);
            foreach (var item in task)
            {
                Console.WriteLine(task.ToString());
            }
            return;
        }

        public string Fun()
        {
            Thread.Sleep(1000);
            return "This Task has Finished";
        }

        private void button1_Click(object sender, EventArgs e)
        {
            TaskTestFun();
        }
    }

异步回调:多线程多线程并发(快的线程,先结束,先往下跑)(主线程不等待所有线程都结束)

  1. 要点:

不再使用async 和await关键字

使用ContinueWith关键字

  1. 出现的问题:

    UI线程不再等待工作主线程任务是否结束,自己私下跑了

    public partial class Form2 : Form
    {

        public Form2()
        {
            InitializeComponent();
            InitializeList();
        }

        private void InitializeList()
        {
            for (int i = 0; i < 9; i++)
            {
                Targetlist.Add($" 任务{i} ");
            }
        }

        private List<string> Targetlist = new List<string>();

        List<Task<string>> Tasks = new List<Task<string>>();

        private void TaskTestFun()
        {
            int idx = 0;
            foreach (string item in Targetlist)
            {
              Task.Run(Fun).ContinueWith(t => {Console.WriteLine($"{idx++} 当前任务结束"); });
            }
            Console.WriteLine("所有任务已经结束");
            return;
        }

        public string Fun()
        {
            Thread.Sleep(1000);
            return "This Task has Finished";
        }

        private void button1_Click(object sender, EventArgs e)
        {
            TaskTestFun();
        }
    }

运行结果:

image-20230414163741230

异步回调:多线程多线程并发(快的线程,先结束,先往下跑)(主线程等待所有线程都结束)

要点:

  1. 使用 Async 和Await 关键字标记UI主线程等待执行
  2. 任务方法的内部,编写事件。在外层注册事件和绑定回调的方法

情景介绍:幼儿园,有很多孩子在吃饭,吃完饭后自己要洗碗。当最后一个孩子把碗洗碗以后。老师会安排孩子们午休

输出结果:

image-20230414173454355

代码逻辑:

编写自定义事件EatEvent,然后让Eat完成以后调用EatEvent注册的WashDishes方法。

主线程收到await Task.WhenAll(Tasks)影响,所以暂时不会继续往下走。

本质:任务动作方法,有编写触发的事件

    public partial class Form2 : Form
    {
        /// <summary>
        ///  创建孩子集合
        /// </summary>
        List<Child> children = new List<Child>();



        public Form2()
        {
            InitializeComponent();
            InitializeList();
        }

        private void InitializeList()
        {
            /// 初始化6个孩子
            for (int i = 0; i < 6; i++)
            {
                Child child = new Child();
                ///注册洗碗事件
                child.EatFinish_Event += WashDishes;
                children.Add(new Child());
            }
        }

        /// <summary>
        /// 执行洗碗动作
        /// </summary>
        private void WashDishes(object sender, EatEvent e)
        {
            Console.WriteLine("洗碗完成了");
        }


        private void button1_Click(object sender, EventArgs e)
        {
            TaskTestFun();
        }

        private async void TaskTestFun()
        { 

            List<Task> Tasks = new List<Task>();
            foreach (var child in children)
            {
                Tasks.Add(Task.Run(child.Eat));
            }
            await Task.WhenAll(Tasks);
            Console.WriteLine("最后一个孩子吃完饭并且完成洗碗了");
            Console.WriteLine("老师让孩子去午休了");
            return;
        }
    }


    /// <summary>
    /// 自定义事件类
    /// </summary>
    public class EatEvent : EventArgs
    {
    }


    /// <summary>
    /// 孩子
    /// </summary>
    public class Child
    {
        /// <summary>
        /// 吃饭用时
        /// </summary>
        public int Duration;

        public void Eat() {
            Thread.Sleep(Duration);
            Console.WriteLine("完成吃饭动作,将要洗碗");
            EatFinish_Event?.Invoke(this,new EatEvent());
        }
        public Child() {

            Random rd = new Random();
            int n = rd.Next(5,10);
            Duration = n * 1000;
        }

        public Action<object, EatEvent> EatFinish_Event;
    }

ConfigureAwait用法

使用场合:含有UI的程序。(不含有UI界面的程序是没有影响的)

代码:ConfigureAwait(false)

    public partial class Form2 : Form
    {
        public Form2()
        {
            InitializeComponent();
        }

        Stopwatch stopwatch = new Stopwatch();
        private void button1_Click(object sender, EventArgs e)
        {
            stopwatch.Start();
            Console.WriteLine("main1 thread:" + Thread.CurrentThread.ManagedThreadId);
            TestConfigureAwait();
            Console.WriteLine("main2 thread:" + Thread.CurrentThread.ManagedThreadId);
        }
        public async Task TestConfigureAwait()
        {
            await Task.Run(() =>
            {
                Thread.Sleep(1000);
                Console.WriteLine("Task thread :" + Thread.CurrentThread.ManagedThreadId);
            }).ConfigureAwait(false);
            Thread.Sleep(3000);
            Console.WriteLine("TestConfigureAwait thread : " + Thread.CurrentThread.ManagedThreadId);
            stopwatch.Stop();
            Console.WriteLine($"消耗时间{stopwatch.Elapsed.TotalSeconds.ToString()}");
        }
    }

输出结果:

image-20230414225754202

代码:ConfigureAwait(true)

         public partial class Form2 : Form
    {
        public Form2()
        {
            InitializeComponent();
        }

        Stopwatch stopwatch = new Stopwatch();
        private void button1_Click(object sender, EventArgs e)
        {
            stopwatch.Start();
            Console.WriteLine("main1 thread:" + Thread.CurrentThread.ManagedThreadId);
            TestConfigureAwait();
            Console.WriteLine("main2 thread:" + Thread.CurrentThread.ManagedThreadId);
        }
        public async Task TestConfigureAwait()
        {
            await Task.Run(() =>
            {
                Thread.Sleep(1000);
                Console.WriteLine("Task thread :" + Thread.CurrentThread.ManagedThreadId);
            }).ConfigureAwait(true);
            Thread.Sleep(3000);
            Console.WriteLine("TestConfigureAwait thread : " + Thread.CurrentThread.ManagedThreadId);
            stopwatch.Stop();
            Console.WriteLine($"消耗时间{stopwatch.Elapsed.TotalSeconds.ToString()}");
        }
    }

image-20230414225826392

简单说明一下:

使用ConfigureAwait(false)时,下一句会从线程池里面取出一个线程执行下面的语句。ConfigureAwait(true)时,下面的代码会等到主线程,然后由主线程继续运行下去。

总结:对于带有UI的程序有效,为了减少时间损耗,建议尽量使用ConfigureAwait(false)。

Task Wait/WaitAny/WaitAll

Wait 阻塞

让调用的线程阻塞在Wait处。

只有三种方法可以让调用线程结束等待:

  1. 线程执行完毕
  2. 线程被取消
  3. 线程当前引发异常

代码案例:

    public partial class Form2 : Form
    {
        public Form2()
        {
            InitializeComponent();
            InitializeList();
        }
        List<People> peoples = new List<People>();
        int count = 10;
        private void InitializeList()
        {
            for (int i = 0; i < count; i++)
            {
                peoples.Add(new People());
            }
        }


        private void button1_Click(object sender, EventArgs e)
        {
            for (int i = 0; i < count; i++)
            {
                Task task = Task.Run(() =>
                {
                    peoples[i].Sleep();
                });
                task.Wait();
                Console.WriteLine("完成当前线程任务");

            }
            Console.WriteLine("已经完成所有任务");
        }
    }

    public class People
    {
        public People() { }

        public void Sleep()
        {
            Thread.Sleep(5000);
        }
    }

当前效果:

主要线程一直等待(阻塞),直到任务线程完成动作以后才能执行下面的语句。

image-20230415005234052

WaitAny 阻塞部分线程

调用线程也会在线程阻塞。但是当有一个线程完成任务以后,调用线程就能被释放,继续下面的语句

代码:

    public partial class Form2 : Form
    {

        Stopwatch Stopwatch = new Stopwatch();
        public Form2()
        {
            InitializeComponent();
            InitializeList();
        }
        List<People> peoples = new List<People>();
        int count = 10;
        private void InitializeList()
        {
            for (int i = 0; i < count; i++)
            {
                peoples.Add(new People());
            }
        }


        private void button1_Click(object sender, EventArgs e)
        {
            Stopwatch.Start();
            List<Task> Tasks = new List<Task>();
            for (int i = 0; i < count-1; i++)
            {
                Tasks.Add(
                   Task.Run(() =>
                      {
                          peoples[i].Sleep();
                      }
                      ));
            }
            Task.WaitAny(Tasks.ToArray());
            Console.WriteLine("当前已经有一个任务已经完成");
        }

        public class People
        {
            public People() { }

            public void Sleep()
            {
                Console.WriteLine("执行睡觉任务");
                Thread.Sleep(3000);
            }
        }
    }

当创建完所有的任务以后

image-20230415010925529

此时先执行到Task.waitAny

image-20230415011037386

主线程阻塞到Task.waitAny上

image-20230415011145891

当有一个任务线程执行完成以后:主线程继续往下面执行,其他线程释放

WaitAll 阻塞全部的线程

当所有的工作线程都执行完成以后,主线程才能从阻塞的状态继续往下执行

task Result()

这个现在没学明白,后面再写

死锁

借用某个案例中的代码。

有可能的触发函数:ContinueWith(true) 切换主线程执行

原理:当主线程遇到task.result这类方法被阻塞线程时,子线程执行完成触发ContinueWith,调度主线程回来,但是主线程已经被阻塞。所以触发死锁

关键代码如下:

主线程函数:

        private void BtnDeadlock_Click(object sender, EventArgs e)
        {
            #region 死锁演示
            Stopwatch sw = Stopwatch.StartNew();    //计时器
            resultTextBox.Clear();
            AppendLine("Wait开始......");

            for (int i = 0; i < Data.Books.Count; i++)
            {
                var book = Data.Books[i];
                var idx = i + 1;
                var task = book.SearchAsync();
                AppendLine($"{task.Result}  {task.Id}.{task.Status}");
            }

            sw.Stop();
            AppendLine($"Wait完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");
            #endregion
        }

任务线程的执行函数:

        public async Task<string> SearchAsync()
        {
            Stopwatch sw = Stopwatch.StartNew();
            await Task.Delay(Duration * 1000).ConfigureAwait(true);
            sw.Stop();
            return Result(sw.ElapsedMilliseconds);
        }

当主线程到达SearchAsync方法后,遇到await关键字,知道当前方法为异步方法。所以继续往下执行,然后遇到task.Result需要获取线程返回结果,就被阻塞住了。与此同时,任务线程完成Delay任务,通过ConfigureAwait(true)方法来让主线程切换回来,切换失败。所以导致了死锁。

如果修改ConfigureAwait(false),那么导致的结果将是主线程一直阻塞,表现为界面卡住。

分析如下:ConfigureAwait(false),任务完成以后,当前系统从线程池里面抽取一个子线程来完成后续任务。但是当主线程只是在task.Result上等到需要的返回值以后,才能继续往下执行。所以表现为主线程能够往下执行,流程正确,但是主线程阻塞,界面卡住。

解决方法:

第一步:ConfigureAwait(false)

第二部:因为主线程需要接收到子线程的返回值来执行方法。

那么把子线程的执行方法封装为异步回调ContinueWith(),让主线程不要去接触这些内容,就能让主线程避免死锁

        private void BtnDeadlock_Click(object sender, EventArgs e)
        {
            #region 死锁演示
            Stopwatch sw = Stopwatch.StartNew();    //计时器
            resultTextBox.Clear();
            AppendLine("Wait开始......");

            for (int i = 0; i < Data.Books.Count; i++)
            {
                var book = Data.Books[i];
                var idx = i + 1;
                var task = book.SearchAsync().ContinueWith(t => InvworkAppendLine($"{idx}.{t.Result}"));
            }

            sw.Stop();
            AppendLine($"Wait完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");
            #endregion
        }

线程的暂停,继续和取消

ManualResetEvent

ManualResetEvent是一个通过信号机制,实现线程间状态同步的类。常用的方法有以下三个:

WaitOne:阻止当前线程,直到收到信号

Reset:将事件状态设置为非终止状态,导致线程阻止

Set:将事件状态设置为终止状态,从而允许继续执行一个或多个等待线程

CancellationTokenSource

CTS是用来发出取消标记,线程和任务根据CTS发出的信号,在自行决定在合适的位置取消线程或任务。

CTS是发出取消标记方式有2种 自动和手动:

1、手动通过调用Cancled()发出取消标记。

2、自动方式通过定时器、指定时间后取消信号。

    public partial class FormComm : Form
    {

        private CancellationTokenSource? TokenSource;
        private ManualResetEvent? ManualReset;

        public FormComm()
        {
            InitializeComponent();
        }

        private void Print(string text)
        {
            BeginInvoke(() =>
            {
                richTextBox1.AppendText(text);
                richTextBox1.ScrollToCaret();
                richTextBox1.Refresh();
            });
        }

        private void BtnStart_Click(object sender, EventArgs e)
        {
            TokenSource = new();
            ManualReset = new(true);
            int i = 0;
            Task.Run(() =>
            {
                while (!TokenSource.Token.IsCancellationRequested)
                {
                    ManualReset.WaitOne();  //根据是否收到信号判断是否阻塞当前线程
                    Thread.Sleep(200);
                    Print($"线程【{Environment.CurrentManagedThreadId}】正在运行第{++i}次{Environment.NewLine}");
                }
            }, TokenSource.Token);
        }

        private void BtnPause_Click(object sender, EventArgs e)
        {
            ManualReset?.Reset();
        }

        private void BtnContinue_Click(object sender, EventArgs e)
        {
            ManualReset?.Set();
        }

        private void BtnStop_Click(object sender, EventArgs e)
        {
            TokenSource?.Cancel();
        }
    }

扩展:取消令牌CancellationTokenSource的用法

第一种用法:注册事件

当执行取消动作以后,将会执行注册的事件

        private void button1_Click(object sender, EventArgs e)
        {
            CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(3000);
            CancellationToken cancellationToken = cancellationTokenSource.Token;
            cancellationToken.Register(() => System.Console.WriteLine("我被取消了."));
            System.Console.WriteLine("先等五秒钟.");
            System.Console.WriteLine("手动取消.");
            cancellationTokenSource.Cancel();

        }

image-20230415024024392

第二种用法:定时取消

通过构造函数,传入时间值

//设置3000毫秒(即3秒)后取消
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(3000);
CancellationToken cancellationToken = cancellationTokenSource.Token;
cancellationToken.Register(() => System.Console.WriteLine("我被取消了."));
System.Console.WriteLine("先等五秒钟.");
System.Console.WriteLine("手动取消.")
cancellationTokenSource.Cancel();

image-20230415024221263

CancelAfter方法,传入时间值

CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Token.Register(() => System.Console.WriteLine("我被取消了."));
//五秒之后取消
cancellationTokenSource.CancelAfter(5000);
System.Console.WriteLine("不会阻塞,我会执行.");

image-20230415024412910

第三种用法:关联取消

设置一组关联的CancellationTokenSource,当其中的一个被取消了,那么其他的都被取消

        private void button1_Click(object sender, EventArgs e)
        {
            //声明几个CancellationTokenSource
            CancellationTokenSource tokenSource = new CancellationTokenSource();
            CancellationTokenSource tokenSource2 = new CancellationTokenSource();
            CancellationTokenSource tokenSource3 = new CancellationTokenSource();

            tokenSource2.Token.Register(() => System.Console.WriteLine("tokenSource2被取消了"));

            //创建一个关联的CancellationTokenSource
            CancellationTokenSource tokenSourceNew = CancellationTokenSource.CreateLinkedTokenSource(tokenSource.Token, tokenSource2.Token, tokenSource3.Token);
            tokenSourceNew.Token.Register(() => System.Console.WriteLine("tokenSourceNew被取消了"));
            //取消tokenSource2
            tokenSource2.Cancel();
        }

image-20230415024553342

第四种用法 判断取消(一般作为多线程令牌)

CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken cancellationToken = tokenSource.Token;
//打印被取消
cancellationToken.Register(() => System.Console.WriteLine("被取消了."));
//模拟传递的场景
Task.Run(async ()=> {
    while (!cancellationToken.IsCancellationRequested)
    {
        System.Console.WriteLine("一直在执行...");
        await Task.Delay(1000);
    }
});
//5s之后取消
tokenSource.CancelAfter(5000);
posted @ 2023-04-15 03:29  聆听微风  阅读(141)  评论(0编辑  收藏  举报