(18)线程的实例认识:线程的控制,暂停,继续,停止,线程相互控制,协作

话不多,但比较中肯,本文参照c# 线程暂停继续的实现方式_哔哩哔哩_bilibili

一、老方式

1、这是一个老的实现方式,基本不推荐, 背后控制的原理需要了解。

界面:三个button一个textbox

代码:

cs 复制代码
        private volatile bool isPause = false;//f

        private void BtnStart_Click(object sender, EventArgs e)
        {
            Task.Run(() =>
            {
                int i = 0;
                while (true)
                {
                    if (isPause)//a
                    { continue; }
                    i++;
                    Invoke(new Action(() =>
                    {
                        TxtInfo.AppendText($"[{i.ToString("000]").PadRight(8, '-')}{DateTime.Now.TimeOfDay}{Environment.NewLine}");
                    }));//b
                    Thread.Sleep(50);//c
                    //线程要做事放这里...
                }
            });
        }

        private void BtnPause_Click(object sender, EventArgs e)
        { this.isPause = true; }

        private void BtnContinue_Click(object sender, EventArgs e)
        { this.isPause = false; }

上面一直在异步线程中无限循环,由UI线程的isPause来控制异步线程中的循环分支。

1、问:f处为什么要用volatile?

答:volatile关键字用于修饰isPause变量,它的作用是确保多个线程对该变量的读取和写入操作都是从主内存中进行的,而不是从线程的本地缓存中进行的。这样可以保证多个线程对该变量的操作是可见的,避免了由于线程之间的数据不一致性而引发的问题。

在多线程环境中,为了提高性能,每个线程会将共享变量存储在自己的线程本地缓存中,而不是直接从主内存中读取和写入。这样可以减少对主内存的访问,提高了程序的执行效率。

然而,这也带来了一个问题,即线程之间的共享变量可能存在数据不一致性的问题。当一个线程修改了共享变量的值时,其他线程可能无法立即感知到这个变化,因为它们仍然在使用自己本地缓存中的旧值。这就导致了线程之间的数据不一致,可能会引发错误的行为。

为了解决这个问题,可以使用volatile关键字来修饰共享变量。它的作用是告诉编译器和处理器,这个变量可能会被其他线程修改,因此每次读取和写入该变量时都要直接从主内存中进行操作,而不是使用线程本地缓存。这样可以确保线程之间对共享变量的操作是可见的,避免了数据不一致性带来的问题。

如果在程序中使用volatile关键字修饰了isPause变量,那么异步CPU不会一直访问该变量。volatile关键字的作用是保证共享变量的可见性,当一个线程修改了volatile变量的值时,其他线程能够立即感知到这个变化。这样可以避免线程一直访问旧值,提高程序的执行效率。

上面明显的,Task创建的异步线程一直在访问isPause,而UI线程也在访问修改isPause,所以有必要加上volatile。

2、问:a处为什么占用CPU比较高?

答:对于使用volatile关键字来实现暂停或取消操作的方式,每次都需要在异步线程中判断isPause变量的值,这会导致CPU不断进行判断操作,从而占用较高的CPU资源。

优化的方案就是用CancellationTokensource,一个控制线程标志位的类,它是.NET中提供的一种用于取消操作的机制。通过使用CancellationTokensource,可以创建一个CancellationToken对象,用于监视和取消异步操作。

CancellationTokensource可以降低对CPU的占用,主要是因为它使用了一种基于事件的机制来实现取消操作。在异步操作中,可以通过检查CancellationToken对象的IsCancellationRequested属性来判断是否需要取消操作。当调用CancellationTokensource的Cancel方法时,会触发CancellationToken对象的取消事件,从而通知异步操作需要取消。

相比于使用volatile关键字来取消异步操作,CancellationTokensource的机制更加高效。因为CancellationTokensource使用了事件来通知取消操作,而不是简单地通过轮询检查volatile变量的值。这样可以避免不必要的CPU占用,提高程序的性能。

总结起来,使用CancellationTokensource可以降低对CPU的占用,因为它使用了基于事件的机制来实现取消操作,相比于使用volatile关键字来取消操作更加高效。

3、问:没触发时,还不是要Cpu来处理,就不占用cpu了?这时是谁来处理呢?

答:在使用CancellationTokensource的情况下,当没有触发取消事件时,异步操作并不会一直占用CPU资源。相反,异步操作会进入等待状态,直到有新的任务或事件触发。

具体来说,当没有触发取消事件时,异步操作会进入等待状态,并且不会占用CPU资源。当取消事件被触发时,异步操作会被唤醒,并且开始处理取消操作。这个过程是由系统的线程调度器来管理和控制的。

在异步操作等待的过程中,CPU资源会被释放,可以用于处理其他任务。这样可以避免不必要的CPU占用,提高系统的整体性能。

因此,使用CancellationTokensource的方式可以在没有取消事件发生时,避免异步操作一直占用CPU资源,而是由系统的线程调度器来控制异步操作的执行。这样可以更好地利用CPU资源,提高程序的性能。

4、问:为什么c处要加入延时?

答:b处的操作,在UI线程中执行耗时的操作会导致UI线程阻塞,使得界面无法响应用户的操作,甚至可能导致界面卡顿或崩溃。

为了避免UI线程的阻塞,可以将耗时的操作放在后台线程中执行,然后通过合适的方式将结果传递给UI线程进行更新。在这种情况下,可以使用异步操作或线程池来执行耗时的操作,并在操作完成后通过合适的方式通知UI线程进行更新。

通过在c处使用等待来缓解UI线程,可以让UI线程有空闲的时间来进行绘制、移动等操作,从而提高界面的响应性能。等待的时间可以根据实际情况来设置,以确保UI线程有足够的空闲时间来处理其他任务。

通过将耗时的操作放在后台线程中执行,并采取合适的方式来缓解UI线程的压力,可以确保UI线程的响应性能,提高用户体验。这是在处理耗时操作时常用的一种策略。

去除c句或将c处改为Thread.Sleep(500)时,用鼠标拖动主界面form1会有明显的反应效果。

5、问:当异步线程不处于"暂停"时,关闭form1会出错?

答:当程序关闭时,如果异步操作仍在进行中,可能会导致一些问题,例如在追加文本的操作中出现错误(b处)。

如果在程序关闭时,异步操作仍在执行追加文本的操作,可能会导致文件句柄被关闭或文件访问被中断,从而导致错误的发生。这是因为程序关闭时会终止所有的线程,包括异步操作所在的线程。

为了避免这种情况,可以在程序关闭之前,确保所有的异步操作都已经完成。可以通过等待异步操作完成,或者取消异步操作来确保程序关闭时不会出现错误。

后面其实实现方式中,可以在程序关闭时,先调用CancellationTokensource的Cancel方法来取消异步操作,然后等待异步操作完成。这样可以确保在程序关闭之前,异步操作已经被取消或完成,避免出现错误。

总之,为了避免在程序关闭时出现错误,需要确保所有的异步操作都已经完成或取消。可以通过适当的方式来等待异步操作完成,或者在程序关闭时取消异步操作。这样可以确保程序关闭时的稳定性和正确性。

6、问:如何解决上面关闭时提示错误的情况?

答:b处应该改为BeginInvoke可以避免关闭时提示错误。

BeginInvoke方法会将指定的方法放入UI线程的消息队列中,由UI线程在适当的时候执行。这样可以确保在程序关闭时,异步操作已经完成或取消,避免出现错误。

复习:invoke与begineinvoke的区别

Invoke方法是同步等待执行的,它会阻塞当前线程,直到被调用的方法执行完成并返回结果。这意味着在UI线程中使用Invoke方法执行耗时的操作时,会导致UI线程阻塞,界面无法响应用户的操作。

而BeginInvoke方法是异步等待执行的,它会将指定的方法放入UI线程的消息队列中,并立即返回。UI线程会在适当的时候执行该方法。这样可以避免UI线程的阻塞,保持界面的响应性能。

另外,BeginInvoke方法还可以通过IAsyncResult接口来实现预先取消的功能。通过调用IAsyncResult接口的AsyncWaitHandle属性的WaitOne方法,可以等待异步操作完成或取消。如果异步操作已经开始执行,但在等待期间被取消(如程序关闭),WaitOne方法会返回一个取消的信号,而不会导致错误。

因此,使用BeginInvoke方法可以实现异步等待执行,并具有预先取消的功能。这样可以确保在程序关闭时,异步操作会被正确地执行或取消,避免出现错误。

7、上面程序有个小bug,在开始线程后,可以暂停或继续,但在暂停后不能再点开始,点了没效果。

因为开始中无法修改isPause,如果此时是暂停或继续将不受影响。因此应该有一个标志来判断当前线程是否开启或未开,再判断现在的状况进行处理。

二、事件方式

1、仍然前面的。界面

程序:

cs 复制代码
        private ManualResetEvent resetEvent = new ManualResetEvent(true);//a

        private void BtnStart_Click(object sender, EventArgs e)
        {
            Task.Run(() =>
            {
                int i = 0;
                while (true)
                {
                    resetEvent.WaitOne();//b
                    i++;
                    BeginInvoke(new Action(() =>
                    {
                        TxtInfo.AppendText($"[{i.ToString("000]").PadRight(8, '-')}{DateTime.Now.TimeOfDay}\r\n");
                    }));
                    Thread.Sleep(100);
                    //线程要做事放这里...
                }
            });
        }

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

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

现在去除占用cpu较高的isPause方式,使用基于事件激活方式的ManualResetEvent。

1、问:什么是ManualResetEvent?

答:ManualResetEvent有点类似保一道门,要么同步阻塞关闭门,要么同步放行打开门。Manual是手动的意思,也就是打开了就一直打开不会自动关闭,关闭了就一直关闭不会自动又去打开,每次都得人工操作。

ManualResetEvent是一个线程同步原语,用于在多线程环境下进行线程间的同步和通信。它提供了一种简单的方式来阻塞一个或多个线程,直到接收到信号或超时。

ManualResetEvent有两种状态:有信号状态(Set)和无信号状态。当处于有信号状态时,调用WaitOne方法的线程会立即继续执行。当处于无信号状态时,调用WaitOne方法的线程会被阻塞,直到接收到信号。

ManualResetEvent的主要方法和属性包括:

  • WaitOne():阻塞当前线程,直到接收到信号。

  • WaitOne(int millisecondsTimeout):阻塞当前线程,直到接收到信号或超时。

  • Set():将ManualResetEvent的状态设置为有信号状态,唤醒等待的线程。

  • Reset():将ManualResetEvent的状态设置为无信号状态。

  • WaitHandle属性:获取ManualResetEvent的WaitHandle对象,可以用于在多个等待句柄之间进行等待。

使用ManualResetEvent可以实现多线程的同步和通信。例如,可以在一个线程中调用Set方法来设置信号,然后在其他线程中调用WaitOne方法来等待信号。当信号被设置时,等待的线程会被唤醒并继续执行。

注意,ManualResetEvent是一次性的,即一旦信号被设置,等待的线程会被唤醒,但信号不会自动重置。需要调用Reset方法来将信号重置为无信号状态,以便下一次等待。

2、问:ManualResetEvent里面有Event,它内部是事件激活机制吗?

答:是的,ManualResetEvent内部使用了事件(Event)来实现线程的同步和通信。

在.NET中,事件(Event)是一种基于委托的异步通信机制,用于线程间的通信。事件由一个事件源(Event Source)和一个或多个事件处理程序(Event Handler)组成。事件源可以触发事件,而事件处理程序可以订阅事件并在事件触发时执行相应的操作。

在ManualResetEvent中,使用了一个内部的ManualResetEventSlim对象来实现事件的激活机制。ManualResetEventSlim是ManualResetEvent的轻量级版本,它使用了更高效的同步机制来实现线程的等待和唤醒。

当调用Set方法时,ManualResetEventSlim会激活内部的事件,通知等待的线程可以继续执行。而调用Reset方法时,ManualResetEventSlim会重新等待事件的触发。

通过使用事件激活机制,ManualResetEvent可以实现线程的同步和通信。一个线程可以在某个条件满足时调用Set方法激活事件,而其他线程可以在事件触发时被唤醒并继续执行。

注意,ManualResetEvent中的事件激活机制是基于内部的ManualResetEventSlim对象实现的,而不是直接使用.NET中的事件(Event)机制。

总之,ManualResetEvent使用了事件激活机制来实现线程的同步和通信。通过激活事件,它可以通知等待的线程可以继续执行。这种机制可以用于实现线程间的协调和通信。

3、问:ManualResetEvent与AutoResetEvent有什么区别?

答:AutoResetEvent和ManualResetEvent是两种不同的线程同步机制,主要区别在于事件的自动重置行为。

AutoResetEvent会在一个线程等待事件后,只允许一个线程通过,并自动将事件状态重置为无信号状态。也就是说,一旦有一个线程通过了事件,事件会自动重置为无信号状态,其他线程需要重新等待事件的触发。

而ManualResetEvent则允许多个线程同时通过事件,并且不会自动重置事件状态。也就是说,一旦有一个线程通过了事件,事件会保持有信号状态,其他线程仍然可以通过事件,而不需要重新等待。

因此,AutoResetEvent适用于一次只允许一个线程通过的场景,例如生产者-消费者模型中的消费者线程。每当一个消费者线程从缓冲区中取走一个数据后,AutoResetEvent会自动将事件状态重置为无信号状态,其它消费者线程需要重新等待。

而ManualResetEvent适用于多个线程可以同时通过的场景,例如某个条件满足时,多个线程需要同时执行某个操作。ManualResetEvent可以保持事件状态为有信号状态,以允许多个线程通过。

总结起来,AutoResetEvent在一个线程通过事件后会自动重置事件状态,而ManualResetEvent允许多个线程通过事件,并且不会自动重置事件状态。选择使用哪种类型的事件取决于具体的需求和场景。

4、问:AutoResetEvent类似自动门,打开后会自动关闭,主要用在什么场景?

答:AutoResetEvent类似为一个自动门。一旦有一个线程通过了门(即事件被激活),门会自动关闭,其他线程需要重新等待门再次打开。

AutoResetEvent通常用于以下场景:

(1)生产者-消费者模型:多个消费者线程等待生产者线程生产数据,并且一次只能有一个消费者线程取走数据。生产者线程生产完数据后,通过AutoResetEvent激活事件,唤醒一个消费者线程来处理数据。

(2)线程池管理:线程池中的线程等待任务的到来,一旦有任务到达,通过AutoResetEvent激活事件,唤醒一个线程来执行任务。

(3)任务协调:多个线程在某个条件满足时需要同时执行某个操作,通过AutoResetEvent可以实现线程的同步和协调。

例如: 假设有一个餐厅,里面有多个服务员和多个顾客。服务员需要等待顾客点菜,一旦有顾客点菜,服务员就会去为该顾客服务,然后再等待下一个顾客点菜。这个场景可以使用AutoResetEvent来实现。服务员等待的事件可以看作是一个自动门,一旦有顾客点菜,门会自动打开,服务员可以通过门去为顾客服务,然后门会自动关闭,服务员需要重新等待下一个顾客点菜。

总之,AutoResetEvent适用于一次只允许一个线程通过的场景,可以用于实现线程的同步和协调。

如果只有一个线程去操作另一个线程,AutoResetEvent的作用会比较有限。因为AutoResetEvent主要用于线程间的同步和协调,当只有一个线程在操作时,就没有其他线程需要等待或被唤醒的需求。

在同一时间只允许一个线程通过,因此在这个意义上可以将其视为一个线程。它提供了一种线程间的同步机制,确保只有一个线程能够访问共享资源或执行某个操作。尽管AutoResetEvent本身不是并发执行的,但它可以用于实现线程的同步和协调,以确保在同一时间只有一个线程能够执行某个操作。

5、问:a处new ManualResetEvent(true)是什么意思?

答:创建对象时,用true进行构造,也就是给它有信号,相当于Set,让b处开门放行,让后面跑起来。

6、问:b处的resetEvent.WaitOne()是什么意思?

答:它是对当前线程的阻塞,不占用CPU资源,等待事件的激活打开.

resetEvent.WaitOne()是AutoResetEvent类的一个方法,用于使当前线程进入阻塞状态,直到AutoResetEvent对象的状态变为有信号(signaled)。

当调用WaitOne()方法时,如果AutoResetEvent的状态为无信号(nonsignaled),则当前线程会被阻塞,一直等待直到AutoResetEvent的状态变为有信号。这意味着,调用WaitOne()方法的线程会暂时停止执行,并且不会占用CPU资源。它会等待其他线程通过Set()方法将AutoResetEvent的状态设置为有信号。

当AutoResetEvent的状态变为有信号时,调用WaitOne()方法的线程会被唤醒,然后继续执行后续的代码。

因此,调用resetEvent.WaitOne()会使当前线程进入阻塞状态,直到AutoResetEvent的状态变为有信号。在阻塞期间,该线程是空闲的,不会占用CPU资源。一旦AutoResetEvent的状态变为有信号,该线程会被唤醒并继续执行后续的代码。所以可以说,该线程在等待期间是暂时"死"了,但并不是真正的"死",它只是在等待事件的发生。

它与Thread.Sleep()的阻塞是有区别的,前面是基于事件的阻塞,后者是基于时间的阻塞。

三、经典的线程控制模型

1、上面基本成型,微软又进行总结归纳,对线程的暂停,继续,取消, 专门用了相应的类来处理。

CancellationTokenSource + ManualResetEvent/AutoResetEvent 来控制。

界面:

程序:

cs 复制代码
        private CancellationTokenSource cancelTSource;//a
        private ManualResetEvent resetEvent = new ManualResetEvent(true);//b

        private void BtnStart_Click(object sender, EventArgs e)
        {
            cancelTSource = new CancellationTokenSource();
            CancellationToken ct = cancelTSource.Token;
            TxtInfo.Clear();
            Task.Run(() =>
            {
                int i = 0;
                while (!ct.IsCancellationRequested)//e
                {
                    resetEvent.WaitOne();//c
                    i++;
                    BeginInvoke(new Action(() =>
                    {
                        TxtInfo.AppendText($"[{i.ToString("000]").PadRight(8, '-')}{DateTime.Now.TimeOfDay}\r\n");
                    }));
                    Thread.Sleep(100);
                }
            }, ct);//d
        }

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

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

        private void BtnStop_Click(object sender, EventArgs e)
        { cancelTSource.Cancel(); }//f

增加了一个取消,改变了while(true),用更科学归纳的while。

1、问:a处CancellationTokenSource是什么?

答:CancellationTokenSource是用于创建和管理CancellationToken的类。CancellationToken用于在多线程或异步操作中通知取消请求。

CancellationTokenSource相当于交通局,创建和管理整个城市(程序)每个交通路口(异步操作)的信号灯,而CancellationToken就是某个具体路口的交通信号灯,用于通知异步操作是否应该被取消。这样的好处是,避免以前程序员每个人都设置bool型的标志来控制,用一个统一的信号源和信号来统一管理,编写程序和阅读程序都会很轻松。此外,CancellationTokenSource还提供了一些方便的方法,如Cancel()方法来触发取消信号,以及可以注册回调函数来在取消发生时执行特定的操作。

CancellationTokenSource提供了以下几个主要的成员:

(1)CancellationTokenSource():创建一个新的CancellationTokenSource实例。

(2)CancellationTokenSource(int millisecondsDelay):创建一个新的CancellationTokenSource实例,并在指定的延迟时间后自动取消。

(3)CancellationTokenSource(TimeSpan delay):创建一个新的CancellationTokenSource实例,并在指定的延迟时间后自动取消。与(2)相同,仅参数的类型不一样。

(4)CancellationToken Token:获取与CancellationTokenSource关联的CancellationToken实例。通过Token属性,可以将CancellationToken传递给需要取消支持的方法或操作。

(5)void Cancel():请求取消与CancellationTokenSource关联的CancellationToken。一旦调用Cancel()方法,CancellationToken将进入取消状态,可以通过检查IsCancellationRequested属性来判断是否已请求取消。

(6)void CancelAfter(int millisecondsDelay):在指定的延迟时间后请求取消。

(7)void CancelAfter(TimeSpan delay):在指定的延迟时间后请求取消。

CancellationTokenSource和CancellationToken通常用于在长时间运行的操作中实现取消功能。通过在操作中轮询CancellationToken的IsCancellationRequested属性,可以检查是否已请求取消,并相应地终止操作。CancellationTokenSource提供了一种方便的方式来创建和管理CancellationToken,并在适当的时候请求取消。

注意:

CancellationTokenSource是一种信号机制,用于在多线程或异步操作中通知取消请求。它提供了一种更方便和集成的方式来管理取消操作,避免了手动创建多个bool标志(信号)的麻烦。CancellationTokenSource本身并不直接控制线程的暂停或停止继续,它只是提供了一个取消请求的标志。线程可以通过轮询与CancellationToken关联的IsCancellationRequested属性来检查是否已请求取消,并根据需要采取相应的操作。

2、问:如何达到控制多个线程?

答:上面因为是一个线程,所以创建了一个公有变量,如果要控制多个线程,可以创建多个公有变量,这样每个公有变量对应一个异步线程(比如上面的cancelTSource对应的就是目前想控制的异步线程)。比如另外创建一个异步线程时,可以再用创建另一个公有变量来管理控制这个异步线程。

3、问:为什么要用两个类来做事?

答:(1)CancellationTokenSource,从名字上来看,它主要管理取消异步线程的操作。

(2)ManualResetEvent,从名字上看,主要是手动复位事件的操作,即手动开信号和关信号的作用,让异步线程在自己线程内进行暂停和继续的作用,并不能进行取消本异步线程。

综合两者,所以上面用两个类来操作四个:启动,暂停,继续,停止。

4、问:CancellationTokenSource与CancellationToken的区别?

答:CancellationTokenSource可以看作是管理者,负责生成和取消CancellationToken。而CancellationToken则是具体执行落实者,用于请求取消操作并传递取消状态。

CancellationTokenSource(取消标记源)和CancellationToken(取消标记)区别:

(1)CancellationTokenSource:CancellationTokenSource 是一个类,用于创建和管理取消标记。它负责保存取消请求的状态,并提供 Cancel() 方法来触发取消请求。每个 CancellationTokenSource 实例可以创建一个与之关联的 CancellationToken 对象。

(2)CancellationToken:CancellationToken 是一个结构体,代表具体的取消标记。它是 CancellationTokenSource 的 Token 属性的返回值,通过这个 CancellationToken 对象可以检查取消请求的状态。您可以使用 CancellationToken 的 IsCancellationRequested 属性来检查是否发出了取消请求,并在需要时停止执行异步操作。

简言之,CancellationTokenSource 用于创建和管理取消标记,而 CancellationToken 则用于实际检查和响应取消请求的状态。通过 CancellationTokenSource,您可以触发取消请求,并通过 CancellationToken 来检查这些请求,并采取相应的操作。

通常情况下,您将使用 CancellationTokenSource 创建和跟踪取消标记源,以及通过 CancellationToken 实际监听和处理取消请求。这两个组件结合使用,提供了一种有效的方式来控制和取消异步操作。

5、问:CancellationToken介绍?

答:CancellationToken(取消标记)是C#中用于在异步操作中传递取消信号的一种机制。它是一个结构体,用于协调多个线程之间的取消操作。使用CancellationToken方法:

(1)创建 CancellationTokenSource:取消标记的源是 CancellationTokenSource(取消标记源)。您可以通过在代码中创建 CancellationTokenSource 实例来创建取消标记。例如:

CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

(2)获取 CancellationToken:从 CancellationTokenSource 获取 CancellationToken(取消标记)。通过访问 CancellationTokenSource 的 Token 属性,您可以获取与该取消标记源关联的 CancellationToken。例如:

CancellationToken cancellationToken = cancellationTokenSource.Token;

(3)取消操作:调用 CancellationTokenSource 的 Cancel() 方法可以触发取消操作。一旦取消被请求,CancellationToken 检查的操作将会收到一个取消请求,从而可以根据需要中断异步操作。请对照前面代码f处。 (4)监视取消请求:在异步操作中使用 CancellationToken 进行轮询或监视来检查取消请求。您可以使用 CancellationToken.Register() 方法来注册一个回调函数,该函数将在取消请求时被调用。在异步操作中的适当位置检查 CancellationToken 的 IsCancellationRequested 属性(代码中e处),以便根据需要停止操作的执行。

cs 复制代码
        CancellationTokenSource cts = new CancellationTokenSource();//取消标志源
        CancellationToken token = cts.Token;//取消标志

        token.Register(() => Console.WriteLine("Token has been cancelled."));//a 注册回调函数
        Task task = Task.Run(() =>
        {
            try
            {
                while (!token.IsCancellationRequested)//是否取消请求
                {
                    Console.WriteLine("Working...");//b
                    Thread.Sleep(1000);//c
                }//d
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("Task cancelled.");
            }
        },token);
        Thread.Sleep(5000);
        cts.Cancel();//e 取消命令
        task.Wait();
        Console.ReadKey();

在a处注册取消回调函数后,在e处发出取消,因为有第二个参数token的原因,异步线程直接急刹立即取消终止异步线程,然后马上使用前面注册的回调函数。 (5)传播取消:使用 CancellationToken 可以在异步操作的各个层次之间传播取消请求。例如,可以将 CancellationToken 传递给其他方法或在 LINQ 查询中使用它来中断执行。

这里的不同层次,就是不同的位置,把CancellationToken放在不同的位置上实现不同的控制。例如:

cs 复制代码
        CancellationTokenSource cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;

        Task task = Task.Run(() =>
        {
            try
            {
                Thread.Sleep(2000);//做事
                token.ThrowIfCancellationRequested();//检测是否取消,有则抛异步
            }
            catch (Exception)
            {
                Console.WriteLine("Task cancelled.");
            }
        }, token);
        Thread.Sleep(1000);
        cts.Cancel();//取消命令
        task.Wait();

在linq时可以设置takewhile的条件,以便后面检索元素时检测这个取消。

cs 复制代码
        CancellationTokenSource cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;

        IEnumerable<int> en = Enumerable.Range(1, 999);//生成1到999整数
        List<int> list = en.TakeWhile(n => !token.IsCancellationRequested).ToList();
        try
        {
            for (int i = 0; i < list.Count; i++)
            {
                Console.WriteLine(i);
                if (list[i] == 500)
                {
                    cts.Cancel();
                    token.ThrowIfCancellationRequested();
                }
            }
        }
        catch (Exception)
        {
            Console.WriteLine("已经取消");
        }
        Console.ReadKey();

除了这些外还有:

在Where方法中使用CancellationToken来筛选元素:

cs 复制代码
 var query = numbers.Where(num => num % 2 == 0 && !cancellationToken.IsCancellationRequested);

在Select方法中使用CancellationToken来转换元素:

cs 复制代码
   var query = numbers.Select(num => num * 2);

在GroupBy方法中使用CancellationToken来分组元素:

cs 复制代码
var query = numbers.GroupBy(num => num % 2 == 0 && !cancellationToken.IsCancellationRequested);

在OrderBy或OrderByDescending方法中使用CancellationToken来排序元素:

cs 复制代码
  var query = numbers.OrderBy(num => num, new MyComparer(cancellationToken));

在Join方法中使用CancellationToken来连接两个序列:

cs 复制代码
 var query = numbers.Join(otherNumbers, num => num, otherNum => otherNum, (num, otherNum) => num + otherNum, (num, otherNum) => !cancellationToken.IsCancellationRequested);

注意:

LINQ查询的延迟执行特性使得程序能够更加高效地运行,避免了不必要的内存占用和计算开销。

当你定义一个LINQ查询时,实际上只是在创建一个查询计划,而不会立即执行查询。查询计划包含了查询的逻辑和操作序列,但不会立即执行实际的查询操作。只有当你对查询结果进行迭代或调用执行方法时,才会触发查询的执行。

这种延迟执行的机制使得程序能够更加灵活地处理数据。你可以根据需要动态构建查询,只有在需要时才执行查询操作,从而减少了不必要的计算和内存占用。

另外,LINQ还提供了一些操作符(如Take、Skip、Where等),它们可以进一步优化查询的性能。这些操作符可以在查询执行之前对数据进行筛选、分页、排序等操作,从而减少了需要处理的数据量,提高了查询的效率。

总而言之,LINQ的延迟执行特性和优化操作符使得程序能够更加高效地处理数据,减少了内存占用和计算开销,提高了程序的性能。

CancellationToken 提供了一种优雅和可靠的方式来处理异步操作的取消。它可以帮助您更好地管理异步代码的执行状态,并在需要时准确地中断操作。通过适当地使用 CancellationToken,您可以避免不必要的计算开销、资源浪费和匹配性能的提高。

6、问:在d处不加上ct一样可以取消,这里为什么要加上?

答:如果将CancellationToken作为第二个参数传递给Task.Run方法,那么当取消操作被触发时,任务会立即响应并停止执行。这就相当于在汽车上安装了紧急刹车系统,一旦发生危险情况,刹车系统会立即发挥作用,以避免事故的发生。

如果不将CancellationToken作为第二个参数传递给Task.Run方法,任务可能会继续执行一段时间,直到它自己检测到取消操作并停止。这就像汽车没有安装紧急刹车系统,需要依靠驾驶员的反应来避免事故。在这种情况下,即使取消操作已经被触发,任务也可能会继续执行多次循环,直到它自己检测到取消操作并停止。

因此,将CancellationToken作为第二个参数传递给Task.Run方法是一种更安全、更可靠的做法,可以确保任务在取消操作被触发时立即停止执行,从而避免潜在的错误和资源浪费。

这种方式可以实现更及时和精确的任务取消。而不是等待整个循环体执行完毕后再判断是否需要取消任务,这样可以避免不必要的计算和延迟任务取消的响应时间。因此,通过将TokenSource.Token传递给Task.Run的第二个参数,可以在每次迭代之前对取消进行监控,提供更好的任务取消控制。

7、问:这"及时和精确的任务取消"如何理解?

答:如果没第二个参数,循环可能多次后才"意识"到取消,而加上第二参数,相当增加了一个监视和眼睛,以便及时终止。

这个终止:

(1)如果取消时,Run中的Action委托在循环体内,那么,需要本次循环完成才进行终止;

cs 复制代码
            while (!token.IsCancellationRequested)
            {
                // 代码A
                //代码B
            }

若在Run中的A处遇到取消,需要执行到B完成(本次循环完成)才能取消

(2)如果没有循环,就会执行执行到本次的逻辑代码块。

cs 复制代码
            Task.Run(() =>
            {
                // 代码块 A
                // 代码块 B
                // 代码块 C
            });

如果取消操作被触发,任务会执行代码块 A,但不会继续执行代码块 B 和代码块 C。任务会在代码块 A 执行完毕后立即返回并取消。

(3)最终,是只认逻辑代码块,比如在循环体内有多个逻辑代码块:

cs 复制代码
            Task.Run(() =>
            {
                for (int i = 0; i < 10; i++)
                {
                    // 逻辑代码块 A
                    // 逻辑代码块 B
                    // 逻辑代码块 C
                }
            });

如果取消操作被触发,任务会执行循环的第一个迭代的逻辑代码块 A,但不会继续执行逻辑代码块 B 和逻辑代码块 C,也不会进行后续的循环迭代。任务会在逻辑代码块 A 执行完毕后立即返回并取消。

8、问:什么叫逻辑代码块?

答:逻辑代码块指的是一组相关的代码语句,通常由花括号{}括起来。在C#中,常见的逻辑代码块包括if语句块、while循环块、for循环块、switch语句块等。例如,以下是一个包含多个逻辑代码块的示例:

cs 复制代码
            if (condition)
            {
                // 逻辑代码块 A
            }
            else
            {
                // 逻辑代码块 B
            }

            while (condition)
            {
                // 逻辑代码块 C
            }

            for (int i = 0; i < 10; i++)
            {
                // 逻辑代码块 D
            }

            switch (variable)
            {
                case value1:
                    // 逻辑代码块 E
                    break;
                case value2:
                    // 逻辑代码块 F
                    break;
                default:
                    // 逻辑代码块 G
                    break;
            }

在上面的示例中,逻辑代码块 A、B、C、D、E、F和G分别是if语句块、while循环块、for循环块和switch语句块中的代码块。当取消操作被触发时,任务会执行当前的逻辑代码块,但不会继续执行后续的代码块。任务会在当前逻辑代码块执行完毕后立即返回并取消。

9、问:Run第二参数添加了监视以便及时取消,必定会消耗更多资源?

答:是的,使用第二个参数来监视取消请求会增加一些资源的耗费。当你在创建任务时传递一个CancellationToken对象作为第二个参数时,任务会定期检查该取消标记是否被触发。

这种定期检查会引入一些额外的开销,因为任务需要花费一些时间和计算资源来检查取消标记。如果取消标记被触发,任务需要执行相应的取消操作。

因此,使用第二个参数来监视取消请求会增加一些资源的耗费,尤其是在长时间运行的任务或高频率的取消请求的情况下。在设计任务时,需要权衡取消请求的频率和任务的执行时间,以确保资源的合理利用和性能的平衡。

如果任务不需要监视取消请求,或者取消请求的频率较低,可以选择不使用第二个参数,以减少资源的耗费。但是需要注意,取消请求仍然可以通过CancellationTokenSource.Cancel()方法来触发,无论是否使用了第二个参数。

四、两个线程由B控制A

1、两个线程,由B线程控制A线程,B接收数据,发出指令,A完成操作

界面:

程序:为了简化,用了前面的公有变量,但程序运行时不要运行前面只测试后面两个按钮.

cs 复制代码
        private ManualResetEvent resetEvent = new ManualResetEvent(true);//b
        private ManualResetEvent resetB = new ManualResetEvent(true);
        private volatile int bState = -1;//B线程状态,-1
        private List<string> list = new List<string>() { "Zero", "Left", "Right", "Up", "Down" };

        private void BtnBtoA_Click(object sender, EventArgs e)//B线程控制A线程
        {
            //线程A
            Task.Run(() =>
            {
                while (true)
                {
                    resetEvent.WaitOne();
                    if (bState >= 0)
                    {
                        BeginInvoke(new Action(() =>
                          {
                              TxtInfo.AppendText($"B线程消息:{list[bState]}\r\n");
                          }));//a
                        bState = -1;//防止第二次进入,必须由B线程修改后进入
                    }
                    else
                    {
                    }
                    Thread.Sleep(50);//防止上面UI操作阻塞
                }
            });
            //线程B
            Task.Run(() =>
            {
                Random r = new Random();
                while (true)
                {
                    resetB.WaitOne();
                    //Thread.Sleep(1000);
                    resetEvent.Reset();//暂停A线程
                    bState = r.Next(5);
                    resetEvent.Set();//产生数据后,才开辟A线程
                }
            });
        }

        private void BtnBPause_Click(object sender, EventArgs e)//控制线程B
        {
            if (BtnBPause.Text == "B线程故障")
            {
                resetB.Reset();
                BtnBPause.Text = "B线程正常";
            }
            else
            {
                resetB.Set();
                BtnBPause.Text = "B线程故障";
            }
        }

B线程正常产生的bState来控制A的运行,通过追加文本来显示。

1、问:为什么点击运行"B控A"就会在a处出错?

答:BeginInvoke是异步执行,把消息发送给UI就返回a处继续向下执行,然后bState就成了-1,在某个时间,UI线程的消息队列更新txtInfo时,提取的list[bState]里的bState可能正好是-1,这样就超出索引范围而出错。

修改BeginInvoke为Invoke,则表示是同步执行,不会发生bState传入UI线程提取问题。

2、问:有一个窗体就有一个UI线程?控件不会有线程?

答:是的,每个窗体通常会有一个与之关联的UI线程。UI线程负责处理窗体的创建、显示、用户交互等操作,并维护窗体中包含的控件的状态和渲染。

控件并不拥有自己的线程。在大多数GUI框架中,包括Windows Forms和WPF,控件的创建、更新和事件处理都是在UI线程上进行的。当您在UI线程上对控件进行操作时,系统会自动通过消息循环将相应的消息传递给控件,并在UI线程中执行相关的操作。

这种单线程模型可以确保界面操作的一致性和安全性。它保证了所有的UI操作都在同一个线程上执行,避免了多线程间的竞态条件和数据同步问题。

但请注意,如果您在非UI线程上尝试直接访问或修改控件的属性,可能会引发线程冲突和异常。为了在非UI线程上进行UI操作,通常需要使用跨线程调用(如Invoke方法)来将操作请求提交给UI线程,由UI线程代为执行。

总结起来,窗体通常对应一个UI线程,而控件没有独立的线程,它们的操作都在UI线程上执行,遵循单线程模型以确保界面操作的一致性和安全性。

3、问:什么是消息循环?

答:消息循环(Message Loop),也称为消息泵(Message Pump),是在图形用户界面(GUI)应用程序中实现事件处理和消息传递的核心机制之一。它是一种用于处理用户输入、窗口事件和异步请求等消息的循环结构。

在Windows操作系统中,每个窗口都有一个与之关联的消息循环。当应用程序启动时,Windows会为每个打开的窗口创建一个UI线程,并在该线程上运行消息循环。消息循环不断地从消息队列中取出消息,并将其分发给对应的窗口进行处理。

基本上,消息循环可以被视为一个无限循环,它由以下几个步骤组成:

(1)接收消息:

消息循环通过调用操作系统提供的函数(如GetMessage或PeekMessage)来接收并获取消息。这些消息可能来自用户的输入、窗口事件、定时器、系统通知等等。

(2)分发消息:

一旦获取到消息,消息循环会将消息传递给对应的窗口进行处理。每个窗口都有一个消息处理函数(称为窗口过程),它会根据消息的类型和内容来执行相应的操作。例如,用户点击按钮时,窗口过程会接收到一个鼠标点击事件的消息,并触发相应的按钮点击事件处理代码。

(3)处理消息:

在窗口过程中,根据消息的类型(如鼠标事件、键盘事件、定时器事件等),窗口会调用相应的处理函数或执行相应的操作。这些操作可能包括更新界面、响应用户输入、执行业务逻辑等。

(4)等待消息:

如果当前没有消息需要处理,消息循环会进入一个等待状态,等待新的消息到达。在等待期间,消息循环会将CPU控制权还给操作系统,让其他程序继续执行。 通过不断循环上述步骤,消息循环实现了对用户输入和系统事件的响应,并保证了应用程序的流畅运行。它是GUI应用程序开发中非常重要的一部分,负责管理和分发消息,确保正确的事件处理和交互。

2、问:在运行期间,如果直接关闭form1会在a处的Invoke出错?

答:是的,运行时用invoke同步更新UI,但这时关闭form1会异步异步线程关闭,一个失去父母的invoke再更新UI会出错。

为了控制取消,因为我们加入前面的CancellationTokenSource,并加入第二个参数参与精确地控制:

cs 复制代码
        private ManualResetEvent resetEvent = new ManualResetEvent(true);
        private ManualResetEvent resetB = new ManualResetEvent(true);
        private volatile int bState = -1;
        private List<string> list = new List<string>() { "Zero", "Left", "Right", "Up", "Down" };
        private CancellationTokenSource cts = new CancellationTokenSource();

        private void BtnBtoA_Click(object sender, EventArgs e)
        {
            CancellationToken ct = cts.Token;
            //线程A
            Task taskA = Task.Run(() =>
            {
                while (!ct.IsCancellationRequested)
                {
                    resetEvent.WaitOne();
                    if (bState >= 0)
                    {
                        Invoke(new Action(() =>
                          {
                              TxtInfo.AppendText($"B线程消息:{list[bState]}\r\n");
                          }));//a
                        bState = -1;
                    }
                    else
                    {
                    }
                    Thread.Sleep(100);
                }
            }, ct);
            //线程B
            Task taskB = Task.Run(() =>
            {
                Random r = new Random();
                while (true)
                {
                    resetB.WaitOne();
                    resetEvent.Reset();
                    bState = r.Next(5);
                    resetEvent.Set();
                }
            });
        }

        private void BtnBPause_Click(object sender, EventArgs e)
        {
            if (BtnBPause.Text == "B线程故障")
            {
                resetB.Reset();
                BtnBPause.Text = "B线程正常";
            }
            else
            {
                resetB.Set();
                BtnBPause.Text = "B线程故障";
            }
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            cts.Cancel();
        }

3、问:上面在运行期间如果关闭form1时a处仍然报错,为什么?

答:上面在台式机上并不报错,但在笔记本上会报错,暂时不清楚什么原因造成。

两种修改:

(1)直接在Task.Run(后面添加"async",哪怕Thread.Sleep(1)设置1毫秒也不会报错:

cs 复制代码
        Task.Run(async () =>
        {
            int idx = 0;
            while (!ct.IsCancellationRequested)
            {
                Invoke(new Action(() =>
                {
                    TxtInfo.AppendText($"[{++idx}]-----\r\n");
                }));//a
                Thread.Sleep(1);//b
            }
        }, ct);

(2)把Thread.Sleep改为异步等待,增加异步线程中的反应响应,也不会报错.

cs 复制代码
        Task.Run(async () =>
        {
            int idx = 0;
            while (!ct.IsCancellationRequested)
            {
                Invoke(new Action(() =>
                {
                    TxtInfo.AppendText($"[{++idx}]-----\r\n");
                }));//a
                await Task.Delay(1);//b
            }
        }, ct);

4、问:IsHandleCreated介绍,与invokerequired的区别?

答:IsHandleCreated是Control类的一个属性,用于检查控件的句柄是否已经创建。在Windows Forms应用程序中,控件的句柄是与控件关联的底层Windows窗口的标识符。当控件被创建后,它的句柄就会被分配。通过检查IsHandleCreated属性,可以确定控件是否已经创建,从而避免在控件还没有创建的情况下对其进行操作。

InvokeRequired是Control类的另一个属性,用于检查当前代码是否正在非UI线程上执行。当代码在非UI线程上执行时,调用Invoke或BeginInvoke方法来更新UI是一种常见的模式。通过检查InvokeRequired属性,可以确定当前代码是否在非UI线程上执行,从而决定是否需要使用Invoke或BeginInvoke方法来在UI线程上执行操作。

这两个属性的区别在于它们的作用和检查的对象。IsHandleCreated属性用于检查控件的句柄是否已经创建,而InvokeRequired属性用于检查当前代码是否在非UI线程上执行。

使用IsHandleCreated属性可以确保在控件已经创建的情况下才对其进行操作,从而避免在控件还没有创建的情况下引发异常。而使用InvokeRequired属性可以确保在非UI线程上执行的代码通过Invoke或BeginInvoke方法在UI线程上执行,从而避免在非UI线程上直接操作UI引发的跨线程访问异常。

IsHandleCreated和InvokeRequired是用于不同目的的属性,但它们在处理UI操作时通常会一起使用,以确保在正确的线程上执行UI操作。

如果你确定在UI上有这个控件,并且你能够确保在异步线程上执行的代码只会在控件已经创建的情况下运行,那么你可能不需要检查IsHandleCreated属性。然而,如果存在控件被销毁的可能性,或者你不能完全控制异步操作的执行时机,那么检查IsHandleCreated属性仍然是一个良好的做法,以确保在正确的时机停止异步操作。

5、问:跨线程操作UI时,invoke与control.invoke有什么区别?

答:Invoke和Control.Invoke方法的效果是相同的,它们都用于在UI线程上执行指定的委托或方法。无论是使用Invoke还是Control.Invoke,都可以确保在UI线程上执行操作,避免跨线程访问UI引发的异常。

Invoke方法是Control类的一个方法,它可以在任何控件上调用,而不仅仅是在Form类或其他派生自Control的类上。当你调用Invoke方法时,你需要传递一个委托或方法,该委托或方法将在UI线程上执行。

Control.Invoke是Control类的一个特殊方法,它是通过继承自Control的类(如Form)上的一个简化的方法。当你在一个派生自Control的类上调用Invoke方法时,它会自动将当前实例作为控件参数传递给Invoke方法。这样,你就不需要显式地指定控件了。

所以,无论是使用Invoke还是Control.Invoke,它们的效果是相同的。它们都用于在UI线程上执行操作,并且都可以达到相同的目的。你可以根据个人喜好和代码的可读性来选择使用哪个方法。

6、问:Task.Run()与Await Task.Run()的返回值有什么区别?

答:Task.Run()方法返回的是一个Task或Task<T>对象,它代表了一个异步操作的执行。当你使用await Task.Run()时,await操作符会等待异步操作完成,并且返回异步操作的结果。

如果异步操作是一个返回void的方法,那么await Task.Run()的返回类型就是Task,因为没有具体的结果可以返回。你可以使用await Task.Run()来等待异步操作的完成,而不需要关心具体的结果。

如果异步操作是一个返回某个具体类型的方法(例如Task<int>),那么await Task.Run()的返回类型就是该具体类型(例如int)。在这种情况下,await操作符会等待异步操作完成,并且返回异步操作的结果。

注意 ,使用await Task.Run()时,如果异步操作抛出了异常,await操作符会将异常重新抛出,而不是返回Task对象或具体的结果。

因此,当Task.Run()返回是void时,对应的await Task.Run()将返回一个Task;而当Task.Run()返回是T时,对应的await Task.Run()将返回一个T类型。这样的设计确实更加合理和灵活,无论哪种情况都可以检查异步操作的状态、是否完成、是否出现异常等

7、问:如果函数返回类型为async Task<int>,内部却没有await是什么情况?

cs 复制代码
            private async Task<int> Test()
            {
                return 3;
            }    

答:上面返回类型是Task<int>,内部构造是这样吗???

cs 复制代码
        private async Task<int> Test()
        {
            return await Task.Run(() =>
            {
                return 3;
            });
        }

注意:必须要用await,一是与前面的async配套,二是如果不加async,它实际返回的是Task<int>,直接报错:这是一个异步方法,返回值是int而不是Task<int>。

因此,我们可以把async Task<int>看作是int类型,实际最终结果也必须是int,否则也会报错。但你不能直接写int,我猜测,它是一种特殊的返回值,尽管最终的结果是int,但必须标注好它的形成的原由。

实际return 3;等效的是return await Task.FromResult(3);它是创建一个"已经成功异步执行后"的结果,所以它的任务状态是 RanToCompletion,并不是先"创建一个异步执行"+"成功返回结果"。

Task.FromResult(3)是直接创建一个已完成的Task<int>对象,并将指定的结果(这里是3)作为该任务的结果。它并不会创建一个异步任务,而是创建一个表示已完成的任务,并将指定的结果作为该任务的返回值。

在使用Task.FromResult()方法时,你可以将其看作是一个快速创建已完成任务的便捷方式。它适用于那些不需要进行真正的异步操作,而只需要返回一个已知结果的情况。

注意,Task.FromResult()方法创建的任务是已完成的,即任务的状态为RanToCompletion,并且不会进行任何实际的异步操作。因此,当你使用Task.FromResult(3)返回一个已完成的任务时,可以立即获取任务的结果,而无需等待异步操作的完成。

五、三线程ABC逐个控制

1、三个线程,A完成后让B完成,B完成后让C完成,象工厂流水线一样逐个完成 。起始给A一个任务。

界面:

程序:

cs 复制代码
        private AutoResetEvent autoResetA = new AutoResetEvent(false);
        private AutoResetEvent autoResetB = new AutoResetEvent(false);
        private AutoResetEvent autoResetC = new AutoResetEvent(false);

        private void BtnStart_Click(object sender, EventArgs e)
        {
            //线程A
            Task.Run(async () =>
            {
                while (true)
                {
                    autoResetA.WaitOne();
                    await Task.Delay(300);
                    AppendMsg("线程A任务结束。");
                    autoResetB.Set();
                }
            });
            //线程B
            Task.Run(async () =>
            {
                while (true)
                {
                    autoResetB.WaitOne();
                    await Task.Delay(300);
                    AppendMsg("线程B任务结束。");
                    autoResetC.Set();
                }
            });
            //线程C
            Task.Run(async () =>
            {
                while (true)
                {
                    autoResetC.WaitOne();
                    await Task.Delay(300);
                    AppendMsg("线程C任务结束。");
                }
            });

            //线程s起点,发出任务
            Task.Run(async () =>
            {
                while (true)
                {
                    autoResetA.Set();
                    AppendMsg("给线程A派发任务=====.");
                    await Task.Delay(2300);
                }
            });
        }

        private void AppendMsg(string s)
        {
            Invoke(new Action(() =>
             {
                 TxtInfo.AppendText($"{s}\r\n");
                 TxtInfo.ScrollToCaret();
                 TxtInfo.Refresh();
             }));
        }

2、问:AutoResetEvent只能控制一个线程吗?

答:AutoResetEvent相当于一个自动门,默认是关闭状态,一旦set打开,它马上又会关闭。即使创建这个对象时用true,那么它创建完成后,马上又会恢复关闭false.

AutoResetEvent 是一个同步原语,它通常用于线程间的同步操作。它的工作方式是,当一个线程调用 WaitOne 方法时,如果事件处于关闭状态,则线程会阻塞等待。而另一个线程调用 Set 方法时,这个事件会打开,唤醒等待的线程,并且在被唤醒的线程继续执行后,事件会自动恢复为关闭状态。

如果你在两个 Task.Run 中使用同一个 AutoResetEvent,确保只有一个线程能够通过 WaitOne 方法,可以避免两个线程同时运行。

cs 复制代码
            AutoResetEvent autoResetEvent = new AutoResetEvent(false);

            Task.Run(() =>
            {
                // 线程1执行的代码
                // ...

                autoResetEvent.Set(); // 打开事件
            });

            Task.Run(() =>
            {
                autoResetEvent.WaitOne(); // 等待事件被打开

                // 线程2执行的代码
                // ...
            });

它的设计初衷是只允许一个线程通过 WaitOne 方法,但实际上也可以由一个信号同时唤醒多个线程。当 AutoResetEvent 被设置为打开状态时,所有正在等待的线程都会被唤醒,并且所有的线程都可以继续执行。

cs 复制代码
            AutoResetEvent autoResetEvent = new AutoResetEvent(false);
            Task.Run(() =>
            {
                autoResetEvent.Set(); // 打开事件
            });

            Task.Run(() =>
            {
                autoResetEvent.WaitOne(); // 等待事件被打开

                // 线程2执行的代码
                // ...
            });

            Task.Run(() =>
            {
                autoResetEvent.WaitOne(); // 等待事件被打开

                // 线程3执行的代码
                // ...
            });

如果你需要确保只有一个线程能够继续执行,可以考虑使用其他的同步原语,例如 Mutex 或 Semaphore,这些原语可以更精确地控制线程的执行。

3、问:什么是同步原语?

答:"原语"一词是"原始操作"(primitive operation)的缩写,指的是一组基本的操作或指令,它们是构建更复杂操作的基础。在计算机科学中,"原语"通常用于描述一组基本的操作或指令,这些操作或指令是不可再分的,不能再进一步分解。

在同步原语的上下文中,"原语"指的是一组基本的操作或指令,用于实现多线程或多进程之间的同步和协作。这些原语提供了一些基本的机制,例如互斥访问、条件等待、信号通知等,用于控制线程或进程之间的操作顺序和并发访问。

因此,"原语"可以理解为一种基本的操作或指令,用于构建更复杂的操作或实现特定的功能。它是计算机科学中的基本术语,用于描述一组基本的操作或指令,以及它们的组合和应用。

4、问:SemaphoreSlim比AutoResetEvent更轻量?

答:是的。

SemaphoreSlim是一个轻量级的信号量实现,用于控制并发访问的数量。它可以用来限制同时访问某个资源的线程数量,或者用于实现线程之间的顺序执行。

SemaphoreSlim提供了两个主要的方法:WaitAsync()和Release()。

  • WaitAsync()方法用于获取信号量,如果当前信号量的计数器大于0,则立即返回并将计数器减1;如果计数器为0,则线程会被阻塞,直到有其他线程调用Release()方法释放信号量。

  • Release()方法用于释放信号量,将计数器加1,并且唤醒一个等待的线程。 await semaphoreA.WaitAsync()表示当前线程正在等待获取semaphoreA信号量。如果semaphoreA的计数器大于0,则当前线程可以继续执行;如果计数器为0,则当前线程会被阻塞,直到有其他线程调用semaphoreA.Release()释放信号量。

通过使用await关键字,可以将异步等待和信号量的使用结合起来,使得代码能够在等待期间继续执行其他任务,而不会阻塞线程。这样可以提高程序的性能和响应性。

SemaphoreSlim比AutoResetEvent更轻量的原因是因为它是基于信号量的同步原语,而AutoResetEvent是基于事件的同步原语。信号量是一种更通用的同步机制,可以用于控制多个线程的访问权限,而事件只能用于控制单个线程的访问权限。由于SemaphoreSlim是基于信号量的轻量级实现,所以在某些情况下,它比AutoResetEvent更高效。

5、问:能用SemophoreSlim来优化上面程序吗?

答:优化后:

cs 复制代码
        private SemaphoreSlim semA = new SemaphoreSlim(0);
        private SemaphoreSlim semB = new SemaphoreSlim(0);
        private SemaphoreSlim semC = new SemaphoreSlim(0);

        private void BtnStart_Click(object sender, EventArgs e)
        {
            //线程A
            Task.Run(async () =>
            {
                while (true)
                {
                    await semA.WaitAsync();
                    await Task.Delay(300);
                    AppendMsg("线程A任务结束。");
                    semC.Release();
                }
            });
            //线程B
            Task.Run(async () =>
            {
                while (true)
                {
                    await semB.WaitAsync();
                    await Task.Delay(300);
                    AppendMsg("线程B任务结束。");
                    semC.Release();
                }
            });
            //线程C
            Task.Run(async () =>
            {
                while (true)
                {
                    await semC.WaitAsync();
                    await Task.Delay(300);
                    AppendMsg("线程C任务结束。");
                }
            });

            //线程s起点,发出任务
            Task.Run(async () =>
            {
                while (true)
                {
                    semA.Release();
                    AppendMsg("给线程A派发任务=====.");
                    await Task.Delay(1300);
                }
            });
        }

        private void AppendMsg(string s)
        {
            Invoke(new Action(() =>
             {
                 TxtInfo.AppendText($"{s}\r\n");
                 TxtInfo.ScrollToCaret();
                 TxtInfo.Refresh();
             }));
        }        

最开始new SemaphoreSlim(0);为0是关闭信号大门,用release对信号加1,当信号量>0时信号大门打开,程序跑起来。另外await semC.WaitAsync();是一个异步阻塞等待,不会影响当前线程的一个等待,同时semC.WaitAsync()见钱眼开,看到有>0的信号来了,就不再阻塞、放行,同时对信号量减1,完成对信号大门的关闭。

相关推荐
向宇it2 小时前
【从零开始入门unity游戏开发之——C#篇25】C#面向对象动态多态——virtual、override 和 base 关键字、抽象类和抽象方法
java·开发语言·unity·c#·游戏引擎
向宇it3 小时前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
坐井观老天8 小时前
在C#中使用资源保存图像和文本和其他数据并在运行时加载
开发语言·c#
pchmi10 小时前
C# OpenCV机器视觉:模板匹配
opencv·c#·机器视觉
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭12 小时前
C#都可以找哪些工作?
开发语言·c#
boligongzhu13 小时前
Dalsa线阵CCD相机使用开发手册
c#
向宇it1 天前
【从零开始入门unity游戏开发之——C#篇23】C#面向对象继承——`as`类型转化和`is`类型检查、向上转型和向下转型、里氏替换原则(LSP)
java·开发语言·unity·c#·游戏引擎·里氏替换原则
sukalot1 天前
windows C#-命名实参和可选实参(下)
windows·c#
小码编匠1 天前
.NET 下 RabbitMQ 队列、死信队列、延时队列及小应用
后端·c#·.net