69

我有一个 C# 程序需要每 X 分钟分派一个线程,但前提是先前分派的线程(从 X 分钟开始)当前尚未运行

一个普通的旧的Timer单独是行不通的(因为它每 X 分钟分派一个事件,无论先前分派的进程是否已经完成)。

将要分派的进程在执行其任务所需的时间上有很大的不同——有时可能需要一秒钟,有时可能需要几个小时。如果它从上次启动时仍在处理,我不想再次启动该过程。

任何人都可以提供一些有效的 C# 示例代码吗?

4

16 回答 16

60

在我看来,在这种情况下的方法是使用System.ComponentModel.BackgroundWorker类,然后在IsBusy每次要调度(或不调度)新线程时简单地检查它的属性。代码很简单;这是一个例子:

class MyClass
{    
    private BackgroundWorker worker;

    public MyClass()
    {
        worker = new BackgroundWorker();
        worker.DoWork += worker_DoWork;
        Timer timer = new Timer(1000);
        timer.Elapsed += timer_Elapsed;
        timer.Start();
    }

    void timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        if(!worker.IsBusy)
            worker.RunWorkerAsync();
    }

    void worker_DoWork(object sender, DoWorkEventArgs e)
    {
        //whatever You want the background thread to do...
    }
}

在这个例子中,我使用System.Timers.Timer了 ,但我相信它也应该与其他计时器一起使用。该类BackgroundWorker还支持进度报告和取消,并使用事件驱动模型与调度线程进行通信,因此您不必担心易失性变量之类的...

编辑

这是更详细的示例,包括取消和进度报告:

class MyClass
{    
    private BackgroundWorker worker;

    public MyClass()
    {
        worker = new BackgroundWorker()
        {
            WorkerSupportsCancellation = true,
            WorkerReportsProgress = true
        };
        worker.DoWork += worker_DoWork;
        worker.ProgressChanged += worker_ProgressChanged;
        worker.RunWorkerCompleted += worker_RunWorkerCompleted;

        Timer timer = new Timer(1000);
        timer.Elapsed += timer_Elapsed;
        timer.Start();
    }

    void timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        if(!worker.IsBusy)
            worker.RunWorkerAsync();
    }

    void worker_DoWork(object sender, DoWorkEventArgs e)
    {
        BackgroundWorker w = (BackgroundWorker)sender;

        while(/*condition*/)
        {
            //check if cancellation was requested
            if(w.CancellationPending)
            {
                //take any necessary action upon cancelling (rollback, etc.)

                //notify the RunWorkerCompleted event handler
                //that the operation was cancelled
                e.Cancel = true; 
                return;
            }

            //report progress; this method has an overload which can also take
            //custom object (usually representing state) as an argument
            w.ReportProgress(/*percentage*/);

            //do whatever You want the background thread to do...
        }
    }

    void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        //display the progress using e.ProgressPercentage and/or e.UserState
    }

    void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if(e.Cancelled)
        {
            //do something
        }
        else
        {
            //do something else
        }
    }
}

然后,为了取消进一步的执行,只需调用worker.CancelAsync(). 请注意,这是完全由用户处理的取消机制(它不支持线程中止或类似的开箱即用的东西)。

于 2013-01-30T00:17:57.027 回答
23

您可以只维护一个 volatile bool 来实现您的要求:

private volatile bool _executing;

private void TimerElapsed(object state)
{
    if (_executing)
        return;

    _executing = true;

    try
    {
        // do the real work here
    }
    catch (Exception e)
    {
        // handle your error
    }
    finally
    {
        _executing = false;
    }
}
于 2012-12-02T17:19:09.817 回答
10

您可以在其经过的回调中禁用和启用您的计时器。

public void TimerElapsed(object sender, EventArgs e)
{
  _timer.Stop();

  //Do Work

  _timer.Start();
}
于 2012-09-24T17:59:43.577 回答
7

您可以在处理数据/方法之前使用System.Threading.Timer并将其设置为TimeoutInfinite然后在完成后重新启动Timer准备下一次调用。

    private System.Threading.Timer _timerThread;
    private int _period = 2000;

    public MainWindow()
    {
        InitializeComponent();

        _timerThread = new System.Threading.Timer((o) =>
         {
             // Stop the timer;
             _timerThread.Change(-1, -1);

             // Process your data
             ProcessData();

             // start timer again (BeginTime, Interval)
             _timerThread.Change(_period, _period);
         }, null, 0, _period);
    }

    private void ProcessData()
    {
        // do stuff;
    }
于 2013-01-30T22:09:06.237 回答
4

使用我在这里的帖子中的 PeriodicTaskFactory

CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

Task task = PeriodicTaskFactory.Start(() =>
{
    Console.WriteLine(DateTime.Now);
    Thread.Sleep(5000);
}, intervalInMilliseconds: 1000, synchronous: true, cancelToken: cancellationTokenSource.Token);

Console.WriteLine("Press any key to stop iterations...");
Console.ReadKey(true);

cancellationTokenSource.Cancel();

Console.WriteLine("Waiting for the task to complete...");

Task.WaitAny(task);

下面的输出显示,即使间隔设置为 1000 毫秒,每次迭代都不会开始,直到任务操作的工作完成。这是使用synchronous: true可选参数完成的。

Press any key to stop iterations...
9/6/2013 1:01:52 PM
9/6/2013 1:01:58 PM
9/6/2013 1:02:04 PM
9/6/2013 1:02:10 PM
9/6/2013 1:02:16 PM
Waiting for the task to complete...
Press any key to continue . . .

更新

如果您想要 PeriodicTaskFactory 的“跳过事件”行为,只需不要使用同步选项并像 Bob 在这里所做的那样实现 Monitor.TryEnter https://stackoverflow.com/a/18665948/222434

Task task = PeriodicTaskFactory.Start(() =>
{
    if (!Monitor.TryEnter(_locker)) { return; }  // Don't let  multiple threads in here at the same time.

    try
    {
        Console.WriteLine(DateTime.Now);
        Thread.Sleep(5000);
    }
    finally
    {
        Monitor.Exit(_locker);
    }

}, intervalInMilliseconds: 1000, synchronous: false, cancelToken: cancellationTokenSource.Token);

PeriodicTaskFactory好处是返回的 Task 可以与所有 TPL API 一起使用,例如Task.Wait,延续等。

于 2013-09-06T17:06:16.897 回答
3

您可以在任务之前停止计时器并在任务完成后重新启动它,这可以使您的拍摄在均匀的时间间隔内定期执行。

public void myTimer_Elapsed(object sender, EventArgs e)
{
    myTimer.Stop();
    // Do something you want here.
    myTimer.Start();
}
于 2012-09-24T18:00:33.543 回答
3

如果您希望计时器的回调在后台线程上触发,您可以使用System.Threading.Timer。这个 Timer 类允许您“指定Timeout.Infinite禁用周期性信号”。作为构造函数的一部分,这会导致计时器仅触发一次。

然后,您可以在第一个计时器的回调触发并完成时构造一个新计时器,从而防止多个计时器被调度,直到您准备好它们发生。

这里的优点是您无需创建计时器,然后反复取消它们,因为您一次安排的时间永远不会超过“下一个事件”。

于 2012-09-24T18:06:18.833 回答
3

至少有 20 种不同的方法可以实现这一点,从使用计时器和信号量,到 volatile 变量,再到使用 TPL,再到使用 Quartz 等开源调度工具。

创建线程是一项昂贵的工作,所以为什么不直接创建 ONE 并让它在后台运行,因为它将大部分时间都用于空闲,它不会对系统造成真正的消耗。定期醒来并做工作,然后在一段时间内重新入睡。无论任务需要多长时间,在开始新任务之前,您总是会在完成后至少等待“waitForWork”时间跨度。

    //wait 5 seconds for testing purposes
    static TimeSpan waitForWork = new TimeSpan(0, 0, 0, 5, 0);
    static ManualResetEventSlim shutdownEvent = new ManualResetEventSlim(false);
    static void Main(string[] args)
    {
        System.Threading.Thread thread = new Thread(DoWork);
        thread.Name = "My Worker Thread, Dude";
        thread.Start();

        Console.ReadLine();
        shutdownEvent.Set();
        thread.Join();
    }

    public static void DoWork()
    {
        do
        {
            //wait for work timeout or shudown event notification
            shutdownEvent.Wait(waitForWork);

            //if shutting down, exit the thread
            if(shutdownEvent.IsSet)
                return;

            //TODO: Do Work here


        } while (true);

    }
于 2013-01-28T21:37:37.080 回答
3

您可以使用 System.Threading.Timer。诀窍是仅设置初始时间。当上一个间隔完成或作业完成时再次设置初始时间(当作业花费的时间比间隔长时会发生这种情况)。这是示例代码。

class Program
{


    static System.Threading.Timer timer;
    static bool workAvailable = false;
    static int timeInMs = 5000;
    static object o = new object(); 

    static void Main(string[] args)
    {
        timer = new Timer((o) =>
            {
                try
                {
                    if (workAvailable)
                    {
                        // do the work,   whatever is required.
                        // if another thread is started use Thread.Join to wait for the thread to finish
                    }
                }
                catch (Exception)
                {
                    // handle
                }
                finally
                {
                    // only set the initial time, do not set the recurring time
                    timer.Change(timeInMs, Timeout.Infinite);
                }
            });

        // only set the initial time, do not set the recurring time
        timer.Change(timeInMs, Timeout.Infinite);
    }
于 2013-01-31T06:15:57.547 回答
3

为什么不使用计时器Monitor.TryEnter()?如果OnTimerElapsed()在前一个线程完成之前再次调用,它将被丢弃,并且在计时器再次触发之前不会再次发生另一次尝试。

private static readonly object _locker = new object();

    private void OnTimerElapsed(object sender, ElapsedEventArgs e)
    {
        if (!Monitor.TryEnter(_locker)) { return; }  // Don't let  multiple threads in here at the same time.

        try
        {
            // do stuff
        }
        finally
        {
            Monitor.Exit(_locker);
        }
    }
于 2013-09-06T20:18:00.657 回答
3

这个问题已经有很多很好的答案,包括一个基于 TPL 中一些特性的稍新的答案。但我觉得这里缺乏:

  1. 基于 TPL 的解决方案 a) 并没有真正完全包含在这里,而是指另一个答案,b) 没有显示如何使用async/await以单一方法实现计时机制,以及 c) 引用的实现是相当复杂,这有点混淆了这个特定问题的潜在相关点。
  2. 这里的原始问题对所需实现的具体参数有些模糊(尽管其中一些在评论中得到了澄清)。同时,其他读者可能有相似但不相同的需求,没有一个答案可以解决可能需要的各种设计选项。
  3. Task我特别喜欢使用and async/await这种方式实现周期性行为,因为它简化了代码。async/await特性在获取本来会被延续/回调实现细节破坏的代码并在单个方法中保留其自然的线性逻辑方面特别有价值。但是这里没有答案可以证明这种简单性。

所以,有了这个理由,我想为这个问题添加另一个答案……


对我来说,首先要考虑的是“这里需要什么确切的行为?” 这里的问题从一个基本前提开始:定时器启动的周期任务不应该并发运行,即使任务花费的时间比定时器周期长。但是有多种方式可以满足前提,包括:

  1. 甚至不要在任务运行时运行计时器。
  2. 运行定时器(这个和我在这里展示的其余选项都假设定时器在任务执行期间继续运行),但如果任务花费的时间超过定时器周期,请在任务完成后立即再次运行上一个计时器滴答声。
  3. 仅在计时器滴答时启动任务的执行。如果任务花费的时间超过定时器周期,则在当前任务执行时不要启动新任务,即使当前任务完成后,也不要在下一个定时器滴答时启动新任务。
  4. 如果任务花费的时间超过了计时器间隔,不仅要在任务完成后立即再次运行任务,还要根据需要多次运行,直到任务“赶上”。即随着时间的推移,尽最大努力为每个计时器滴答执行一次任务。

根据评论,我的印象是#3 选项最接近 OP 的原始请求,尽管听起来#1 选项可能也可以工作。但是选项 #2 和 #4 可能比其他人更可取。

在下面的代码示例中,我使用五种不同的方法实现了这些选项(其中两个实现了选项 #3,但方式略有不同)。当然,人们会根据自己的需要选择适当的实现。您可能不需要所有五合一程序!:)

关键在于,在所有这些实现中,它们自然而然地以一种非常简单的方式,以周期但非并发的方式执行任务。也就是说,他们有效地实现了基于计时器的执行模型,同时确保任务一次只能由一个线程执行,根据问题的主要请求。

此示例还说明了如何CancellationTokenSource使用中断周期任务,以await干净、简单的方式处理基于异常的模型。

class Program
{
    const int timerSeconds = 5, actionMinSeconds = 1, actionMaxSeconds = 7;

    static Random _rnd = new Random();

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to interrupt timer and exit...");
        Console.WriteLine();

        CancellationTokenSource cancelSource = new CancellationTokenSource();

        new Thread(() => CancelOnInput(cancelSource)).Start();

        Console.WriteLine(
            "Starting at {0:HH:mm:ss.f}, timer interval is {1} seconds",
            DateTime.Now, timerSeconds);
        Console.WriteLine();
        Console.WriteLine();

        // NOTE: the call to Wait() is for the purpose of this
        // specific demonstration in a console program. One does
        // not normally use a blocking wait like this for asynchronous
        // operations.

        // Specify the specific implementation to test by providing the method
        // name as the second argument.
        RunTimer(cancelSource.Token, M1).Wait();
    }

    static async Task RunTimer(
        CancellationToken cancelToken, Func<Action, TimeSpan, Task> timerMethod)
    {
        Console.WriteLine("Testing method {0}()", timerMethod.Method.Name);
        Console.WriteLine();

        try
        {
            await timerMethod(() =>
            {
                cancelToken.ThrowIfCancellationRequested();
                DummyAction();
            }, TimeSpan.FromSeconds(timerSeconds));
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine();
            Console.WriteLine("Operation cancelled");
        }
    }

    static void CancelOnInput(CancellationTokenSource cancelSource)
    {
        Console.ReadKey();
        cancelSource.Cancel();
    }

    static void DummyAction()
    {
        int duration = _rnd.Next(actionMinSeconds, actionMaxSeconds + 1);

        Console.WriteLine("dummy action: {0} seconds", duration);
        Console.Write("    start: {0:HH:mm:ss.f}", DateTime.Now);
        Thread.Sleep(TimeSpan.FromSeconds(duration));
        Console.WriteLine(" - end: {0:HH:mm:ss.f}", DateTime.Now);
    }

    static async Task M1(Action taskAction, TimeSpan timer)
    {
        // Most basic: always wait specified duration between
        // each execution of taskAction
        while (true)
        {
            await Task.Delay(timer);
            await Task.Run(() => taskAction());
        }
    }

    static async Task M2(Action taskAction, TimeSpan timer)
    {
        // Simple: wait for specified interval, minus the duration of
        // the execution of taskAction. Run taskAction immediately if
        // the previous execution too longer than timer.

        TimeSpan remainingDelay = timer;

        while (true)
        {
            if (remainingDelay > TimeSpan.Zero)
            {
                await Task.Delay(remainingDelay);
            }

            Stopwatch sw = Stopwatch.StartNew();
            await Task.Run(() => taskAction());
            remainingDelay = timer - sw.Elapsed;
        }
    }

    static async Task M3a(Action taskAction, TimeSpan timer)
    {
        // More complicated: only start action on time intervals that
        // are multiples of the specified timer interval. If execution
        // of taskAction takes longer than the specified timer interval,
        // wait until next multiple.

        // NOTE: this implementation may drift over time relative to the
        // initial start time, as it considers only the time for the executed
        // action and there is a small amount of overhead in the loop. See
        // M3b() for an implementation that always executes on multiples of
        // the interval relative to the original start time.

        TimeSpan remainingDelay = timer;

        while (true)
        {
            await Task.Delay(remainingDelay);

            Stopwatch sw = Stopwatch.StartNew();
            await Task.Run(() => taskAction());

            long remainder = sw.Elapsed.Ticks % timer.Ticks;

            remainingDelay = TimeSpan.FromTicks(timer.Ticks - remainder);
        }
    }

    static async Task M3b(Action taskAction, TimeSpan timer)
    {
        // More complicated: only start action on time intervals that
        // are multiples of the specified timer interval. If execution
        // of taskAction takes longer than the specified timer interval,
        // wait until next multiple.

        // NOTE: this implementation computes the intervals based on the
        // original start time of the loop, and thus will not drift over
        // time (not counting any drift that exists in the computer's clock
        // itself).

        TimeSpan remainingDelay = timer;
        Stopwatch swTotal = Stopwatch.StartNew();

        while (true)
        {
            await Task.Delay(remainingDelay);
            await Task.Run(() => taskAction());

            long remainder = swTotal.Elapsed.Ticks % timer.Ticks;

            remainingDelay = TimeSpan.FromTicks(timer.Ticks - remainder);
        }
    }

    static async Task M4(Action taskAction, TimeSpan timer)
    {
        // More complicated: this implementation is very different from
        // the others, in that while each execution of the task action
        // is serialized, they are effectively queued. In all of the others,
        // if the task is executing when a timer tick would have happened,
        // the execution for that tick is simply ignored. But here, each time
        // the timer would have ticked, the task action will be executed.
        //
        // If the task action takes longer than the timer for an extended
        // period of time, it will repeatedly execute. If and when it
        // "catches up" (which it can do only if it then eventually
        // executes more quickly than the timer period for some number
        // of iterations), it reverts to the "execute on a fixed
        // interval" behavior.

        TimeSpan nextTick = timer;
        Stopwatch swTotal = Stopwatch.StartNew();

        while (true)
        {
            TimeSpan remainingDelay = nextTick - swTotal.Elapsed;

            if (remainingDelay > TimeSpan.Zero)
            {
                await Task.Delay(remainingDelay);
            }

            await Task.Run(() => taskAction());
            nextTick += timer;
        }
    }
}

最后一点:在将其作为另一个问题的副本之后,我遇到了这个问答。在另一个问题中,与此处不同的是,OP 特别指出他们正在使用该System.Windows.Forms.Timer课程。当然,使用这个类主要是因为它具有Tick在 UI 线程中引发事件的好特性。

现在,它和这个问题都涉及在后台线程中实际执行的任务,因此该计时器类的 UI 线程相关行为在这些场景中并不是特别有用。这里的代码被实现以匹配“启动后台任务”范例,但它可以很容易地更改,以便taskAction直接调用委托,而不是在 a 中运行Task并等待。除了我上面提到的结构优势之外,使用async/的好处是它保留了类所需的线程相关行为。awaitSystem.Windows.Forms.Timer

于 2015-05-15T07:59:25.970 回答
2

如果我对您的理解正确,您实际上只是想确保您的线程在调度另一个线程之前没有运行。假设您在类中定义了一个线程,如下所示

private System.Threading.Thread myThread;

你可以做:

//inside some executed method
System.Threading.Timer t = new System.Threading.Timer(timerCallBackMethod, null, 0, 5000);

然后像这样添加回调

private void timerCallBackMethod(object state)
{
     if(myThread.ThreadState == System.Threading.ThreadState.Stopped || myThread.ThreadState == System.Threading.ThreadState.Unstarted)
     {
        //dispatch new thread
     }
}
于 2013-01-24T19:15:27.187 回答
2

前段时间我遇到了同样的问题,我所做的只是使用lock{}语句。这样一来,即使 Timer 想要做任何事情,他也被迫等待,直到 lock-Block 结束。

IE

lock
{    
     // this code will never be interrupted or started again until it has finished
} 

这是确保您的过程将一直工作到最后而不会中断的好方法。

于 2013-01-30T16:04:31.777 回答
1

这就是ExecuteTaskCallback方法的胆量。这个位负责做一些工作,但前提是它还没有这样做。为此,我使用了最初设置为在方法中发出信号的ManualResetEvent( ) 。canExecuteStartTaskCallbacks

注意我使用的方式canExecute.WaitOne(0)。零表示WaitOne将立即返回WaitHandle( MSDN ) 的状态。如果省略零,您最终将每次调用ExecuteTaskCallback最终运行该任务,这可能是相当灾难性的。

另一个重要的事情是能够干净地结束处理。我选择阻止Timer执行任何进一步的方法,StopTaskCallbacks因为这样做似乎更可取,而其他工作可能正在进行中。这确保不会进行任何新工作,并且后续调用canExecute.WaitOne();确实会覆盖最后一项任务(如果有的话)。

private static void ExecuteTaskCallback(object state)
{
    ManualResetEvent canExecute = (ManualResetEvent)state;

    if (canExecute.WaitOne(0))
    {
        canExecute.Reset();
        Console.WriteLine("Doing some work...");
        //Simulate doing work.
        Thread.Sleep(3000);
        Console.WriteLine("...work completed");
        canExecute.Set();
    }
    else
    {
        Console.WriteLine("Returning as method is already running");
    }
}

private static void StartTaskCallbacks()
{
    ManualResetEvent canExecute = new ManualResetEvent(true),
        stopRunning = new ManualResetEvent(false);
    int interval = 1000;

    //Periodic invocations. Begins immediately.
    Timer timer = new Timer(ExecuteTaskCallback, canExecute, 0, interval);

    //Simulate being stopped.
    Timer stopTimer = new Timer(StopTaskCallbacks, new object[]
    {
        canExecute, stopRunning, timer
    }, 10000, Timeout.Infinite);

    stopRunning.WaitOne();

    //Clean up.
    timer.Dispose();
    stopTimer.Dispose();
}

private static void StopTaskCallbacks(object state)
{
    object[] stateArray = (object[])state;
    ManualResetEvent canExecute = (ManualResetEvent)stateArray[0];
    ManualResetEvent stopRunning = (ManualResetEvent)stateArray[1];
    Timer timer = (Timer)stateArray[2];

    //Stop the periodic invocations.
    timer.Change(Timeout.Infinite, Timeout.Infinite);

    Console.WriteLine("Waiting for existing work to complete");
    canExecute.WaitOne();
    stopRunning.Set();
}
于 2013-01-26T10:03:36.803 回答
1

这应该做你想要的。它执行一个线程,然后加入该线程直到它完成。进入计时器循环以确保它不会过早执行线程,然后再次关闭并执行。

using System.Threading;

public class MyThread
{
    public void ThreadFunc()
    {
        // do nothing apart from sleep a bit
        System.Console.WriteLine("In Timer Function!");
        Thread.Sleep(new TimeSpan(0, 0, 5));
    }
};

class Program
{
    static void Main(string[] args)
    {
        bool bExit = false;
        DateTime tmeLastExecuted;

        // while we don't have a condition to exit the thread loop
        while (!bExit)
        {
            // create a new instance of our thread class and ThreadStart paramter
            MyThread myThreadClass = new MyThread();
            Thread newThread = new Thread(new ThreadStart(myThreadClass.ThreadFunc));

            // just as well join the thread until it exits
            tmeLastExecuted = DateTime.Now; // update timing flag
            newThread.Start();
            newThread.Join();

            // when we are in the timing threshold to execute a new thread, we can exit
            // this loop
            System.Console.WriteLine("Sleeping for a bit!");

            // only allowed to execute a thread every 10 seconds minimum
            while (DateTime.Now - tmeLastExecuted < new TimeSpan(0, 0, 10));
            {
                Thread.Sleep(100); // sleep to make sure program has no tight loops
            }

            System.Console.WriteLine("Ok, going in for another thread creation!");
        }
    }
}

应该产生类似的东西:

在定时器功能中!睡一会儿!好的,继续创建另一个线程!在定时器功能中!睡一会儿!好的,继续创建另一个线程!在定时器功能中!……

希望这可以帮助!SR

于 2012-09-24T18:32:05.030 回答
0

我建议使用 Timer 而不是线程,因为它是更轻的对象。为了实现您的目标,您可以执行以下操作。

using System.Timers;

namespace sample_code_1
{
    public class ClassName
    {
        Timer myTimer;
        static volatile bool isRunning;

        public OnboardingTaskService()
        {
             myTimer= new Timer();
             myTimer.Interval = 60000;
             myTimer.Elapsed += myTimer_Elapsed;
             myTimer.Start();
        }

        private void myTimer_Elapsed(object sender, ElapsedEventArgs e)
        {
            if (isRunning) return;
            isRunning = true;
            try
            {
                //Your Code....
            }
            catch (Exception ex)
            {
                //Handle Exception
            }
            finally { isRunning = false; }
        }
    }
}

让我知道它是否有帮助。

于 2017-07-19T19:34:44.167 回答