C#每隔X分钟运行一个线程,但前提是该线程尚未运行

时间:2012-09-24 17:58:41

标签: c# timer

我有一个C#程序需要每隔X分钟调度一个线程,但是只有先前调度的线程(从X分钟开始)以前还没有运行

单独一个普通的Timer将不起作用(因为无论是否每个X分钟发送一个事件,或者先前发送的进程是否已经完成)。

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

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

16 个答案:

答案 0 :(得分:54)

在我看来,在这种情况下的方法是使用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类还支持进度报告和取消,并使用事件驱动的调度模式与调度线程,因此您不必担心volatile变量等...

修改

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

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()。请注意,这是完全由用户处理的取消机制(它不支持线程中止或类似于开箱即用的任何内容)。

答案 1 :(得分:21)

你可以保持一个挥发性的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;
    }
}

答案 2 :(得分:8)

您可以在已用完的回调中禁用和启用计时器。

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

  //Do Work

  _timer.Start();
}

答案 3 :(得分:5)

您可以使用System.Threading.Timer并在处理数据/方法之前将Timeout设置为Infinite,然后在完成重新启动后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;
    }

答案 4 :(得分:4)

使用帖子here

中的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的“跳过事件”行为,请不要使用同步选项并实现Monitor.TryEnter,就像Bob在此处所做的那样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的好处是返回了一个可以与所有TPL API一起使用的Task,例如: Task.Wait,延续等等。

答案 5 :(得分:3)

您可以在任务完成之前停止计时器,并在任务完成后再次启动计时器,这可以使你的表演在平均时间间隔内进行。

public void myTimer_Elapsed(object sender, EventArgs e)
{
    myTimer.Stop();
    // Do something you want here.
    myTimer.Start();
}

答案 6 :(得分:3)

如果您希望计时器的回调在后台线程上触发,则可以使用System.Threading.Timer。此Timer类允许您“指定Timeout.Infinite以禁用定期信令。”作为constructor的一部分,导致计时器仅触发一次。

然后,当第一个计时器的回调触发并完成时,您可以构造一个新的计时器,防止多个计时器被安排,直到您准备好它们为止。

这里的优点是你不会创建计时器,然后反复取消它们,因为你从来没有安排过多次“下​​一次事件”。

答案 7 :(得分:3)

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

创建一个线程是一项昂贵的练习,所以为什么不创建一个并让它在后台运行,因为它将花费大部分时间IDLE,它不会导致系统真正耗尽。定期醒来并做好工作,然后回去睡觉一段时间。无论任务需要多长时间,在完成任务之前,您将始终至少等待“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);

    }

答案 8 :(得分: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);
    }

答案 9 :(得分: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);
        }
    }

答案 10 :(得分:2)

如果我理解正确,你实际上只是想在发送另一个线程之前确保你的线程没有运行。假设您在类中定义了一个线程,就像这样

private System.Threading.Thread myThread;

你可以这样做:

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

然后像这样添加callBack

private void timerCallBackMethod(object state)
{
     if(myThread.ThreadState == System.Threading.ThreadState.Stopped || myThread.ThreadState == System.Threading.ThreadState.Unstarted)
     {
        //dispatch new thread
     }
}

答案 11 :(得分:2)

这个问题已经有很多好的答案,包括基于TPL中某些功能的a slightly newer one。但我觉得这里缺乏:

  1. 基于TPL的解决方案a)并非完全包含在此处,而是指另一个答案,b)未显示如何使用async / await来实现时序单个方法中的机制,以及c)引用的实现相当复杂,这有点模糊了这个特定问题的基础相关点。
  2. 这里的原始问题对于所需实现的具体参数有些模糊(尽管其中一些在评论中已经阐明)。与此同时,其他读者可能有类似但不完全相同的需求,没有一个答案可以解决可能需要的各种设计方案。
  3. 我特别喜欢以这种方式使用Taskasync / await来实现定期行为,因为它简化了代码。 async / await功能特别是所以在获取代码时非常有用,否则这些代码会被延续/回调实现细节破坏,并保留其自然的线性逻辑。单一方法。但这里没有答案证明这种简单。
  4. 所以,基于这个理由激励我为这个问题添加另一个答案......


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

    1. 在任务运行时甚至不运行计时器。
    2. 运行计时器(这个和我在这里提出的其余选项都假定计时器在执行任务期间继续运行),但是如果任务需要的时间超过计时器周期,请立即再次运行任务从上一个计时器刻度完成。
    3. 仅在计时器滴答声中启动任务的执行。如果任务花费的时间超过计时器周期,则在执行当前任务时不要启动新任务,即使当前任务完成,也不要在下一个计时器开始之前启动新任务。
    4. 如果任务花费的时间超过计时器间隔,则不仅在完成任务后立即再次运行任务,而是根据需要多次运行任务,直到任务“赶上”。即随着时间的推移,尽最大努力为每个计时器滴答执行一次任务。
    5. 根据评论,我的印象是#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;
              }
          }
      }
      

      最后一点注意事项:我在跟随它作为另一个问题的副本之后遇到了这个Q&amp; A.在另一个问题中,与此不同的是,OP特别指出他们正在使用System.Windows.Forms.Timer类。当然,这个类的使用主要是因为它具有在UI线程中引发Tick事件的好功能。

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

答案 12 :(得分: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

答案 13 :(得分:1)

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

请注意我使用canExecute.WaitOne(0)的方式。零意味着WaitOne将立即返回WaitHandleMSDN)的状态。如果省略零,那么最终每次调用ExecuteTaskCallback最终都会运行任务,这可能是相当灾难性的。

另一个重要的事情是能够彻底结束处理。我选择阻止TimerStopTaskCallbacks中执行任何其他方法,因为在其他工作可能正在进行的情况下这样做似乎更可取。这样可以确保不会进行任何新的工作,并且对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();
}

答案 14 :(得分:1)

前段时间我遇到了同样的问题,我所做的就是使用lock{}语句。有了这个,即使Timer想要做任何事情,他也不得不等待,直到Lock-Block结束。

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

这是一个很好的方法,确保您的流程能够在不中断的情况下一直工作。

答案 15 :(得分: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; }
        }
    }
}

如果有帮助,请告诉我。