如何暂停/恢复线程

16

如何暂停/恢复线程?一旦我执行了Join(),就无法重新启动它。 所以当按下“暂停”按钮时,如何启动一个线程并使其暂停,并在按下继续按钮时恢复它?

该线程唯一的作用是在标签控件中显示一些随机文本。


1
你能否在你的问题中添加你的代码? - Adil
目前还没有代码。我还在思考如何实现我脑海中的这个项目。我能告诉你的是,根据我处理线程的经验,一旦一个线程与主线程合并,就无法恢复/重新启动它。 - Yustme
4个回答

25

也许ManualResetEvent是一个不错的选择。 下面是一个简短的示例:

private static EventWaitHandle waitHandle = new ManualResetEvent(initialState: true); 

// Main thread
public void OnPauseClick(...) {
   waitHandle.Reset();
}

public void OnResumeClick(...) {
   waitHandle.Set();
}

// Worker thread
public void DoSth() {
   while (true) {
     // show some random text in a label control (btw. you have to
     // dispatch the action onto the main thread)
     waitHandle.WaitOne(); // waits for the signal to be set
   }
}

while(true) // 这不是一个好的做法,必须有一种方法来使线程休眠并在需要时唤醒线程,以便它不会消耗处理能力。 - Hammad Sajid

7
我建议你阅读Joe Albahari的C#多线程编程, 特别是挂起和恢复部分:

线程可以通过已弃用的Thread.Suspend和Thread.Resume方法被显式地挂起和恢复。这个机制完全独立于阻塞机制。两个系统是独立的并且并行运行。

一个线程可以挂起自己或另一个线程。调用Suspend会导致线程暂时进入SuspendRequested状态,然后在到达对垃圾回收安全的点后进入Suspended状态。从那里,它只能通过另一个调用其Resume方法的线程来恢复。Resume只能在挂起的线程上工作,而不能在阻塞的线程上工作。

从.NET 2.0开始,Suspend和Resume已经被弃用,他们的使用不被鼓励,因为任意挂起另一个线程所固有的危险性。如果一个持有关键资源锁的线程被挂起,整个应用程序(或计算机)都可能死锁。这比调用Abort更危险 - 后者会导致任何这样的锁定通过finally块中的代码被释放(至少理论上如此)。

SuspendRequested state


你的块引用区分了挂起线程和阻塞线程,但图表似乎并没有进行这种区分。这是有意为之吗? - Tung
@Tung 当线程因某些原因暂停执行,例如通过Sleeping或等待另一个线程结束(通过Join或EndInvoke),则被视为阻塞。在代码中:bool blocked = (someThread.ThreadState & ThreadState.WaitSleepJoin) != 0;。那么为什么图表中没有区分呢?请参见此处 - gliderkite
@gliderkite,我知道,所以我才问如何做到这一点,也正因为如此目前没有代码可以展示,因为我还没有开始。只是提醒一下,在你的第一条回答的第一行中,你指出了“没有可用的代码”,而我正在解释原因。 - Yustme
@Yustme 我明白了,只是误解了。 - gliderkite
@Tung 可能是这样的(“如果通过(已弃用的)Suspend方法暂停其执行,则不认为线程被阻塞”)。 - gliderkite

1

以下是我使用过的两种方法。两种方法都假定工作线程有自己的处理循环。

  1. 让线程调用回调函数请求继续执行的权限
  2. 让父线程调用线程类的方法来发出信号

下面的控制台应用程序示例展示了这两种方法,使用回调函数来暂停/继续,使用工作方法来停止。回调方法的另一个优点是,在检查是否有权限继续执行时,它也方便传递状态更新。

using System;
using System.Threading;

namespace ConsoleApplication7
{
    class Program
    {
        static bool keepGoing;
        static void Main(string[] args)
        {
            keepGoing = true;
            Worker worker = new Worker(new KeepGoingDelegate(KeepGoing));
            Thread thread = new Thread(worker.DoWork);
            thread.IsBackground = true;
            thread.Start();

            while (thread.ThreadState != ThreadState.Stopped)
            {
                switch (Console.ReadKey(true).KeyChar)
                {
                    case 'p':
                        keepGoing = false;
                        break;
                    case 'w':
                        keepGoing = true;
                        break;
                    case 's':
                        worker.Stop();
                        break;
                }
                Thread.Sleep(100);
            }
            Console.WriteLine("Done");
            Console.ReadKey();
        }

        static bool KeepGoing()
        {
            return keepGoing;
        }
    }

    public delegate bool KeepGoingDelegate();
    public class Worker
    {
        bool stop = false;
        KeepGoingDelegate KeepGoingCallback;
        public Worker(KeepGoingDelegate callbackArg)
        {
            KeepGoingCallback = callbackArg;
        }

        public void DoWork()
        {
            while (!stop)
            {
                Console.Write(KeepGoingCallback()?"\rWorking":"\rPaused ");

                Thread.Sleep(100);
            }
            Console.WriteLine("\nStopped");
        }

        public void Stop()
        {
            stop = true;
        }
    }
}

嗨,我真的很难理解委托。我可以找出如何停止线程,但我不理解如何重新启动它? - Zev
@Zev - 委托只是方法的占位符,以便您可以将其传递到另一个类的实例中,以便它可以执行它。在我的示例中,父对象将KeepGoing()方法传递给工作线程,因此该线程定期调用该方法与父对象进行检查,以查看是否应继续运行。工作线程不会每次重新启动 - 它只启动一次并一直运行,直到收到停止信号。 - Ed Power

1

手动挂起和恢复线程并不是最好的选择。但是,您可以使用线程同步原语(如ManualResetEvent)轻松模拟此行为。

看一下这个问题,您可能会发现它有帮助。

但我相信您可以通过使用计时器轻松实现“按时间基础在标签控件中显示随机文本”的目标。

这里是一个使用DispatcherTimer的快速示例。

var timer = new DispatcherTimer(); 
timer.Tick += (s, e) => Label.Text = GetRandomText(); 
timer.Interval = TimeSpan.FromMilliseconds(500); 
timer.Start();

您可以通过调用 timer.Stop() 来暂停它,然后再次调用 timer.Start() 来恢复。


嗨,这看起来不错,我可以把这个代码放在一个单独的线程中吗?因为我不想阻塞GUI线程。 - Yustme
据我所知,DispatcherTimer 的代码在 GUI 线程上执行。如果您想要一个单独的线程,请使用 System.Threading.Timer - Tudor
Tudor是正确的,所以如果你有一些需要在计时器滴答声上执行的“重型”代码,请使用单独的线程。但是,如果您只需要更新文本块,则使用DispatcherTimer就可以了,不会阻塞您的UI。 - Andrew Khmylov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接