【Unity】多线程(一)

Thread类是.NET中用于创建和管理线程的类。

使用多线程处理的应用程序可以更快地响应用户输入,因为在单独的线程上执行处理器密集型任务时,用户界面将保持活动状态。如果需要更好地控制应用程序线程的行为,可以自己管理线程。

1、创建多线程

通过创建System.Threding.Thread类的新实例来创建新线程。

将要在创建的新线程上执行的方法名提供给构造构造。

启动线程的时候调用Thread.Start方法。

using System;
using System.Threading;

public class ServerClass
{
    // The method that will be called when the thread is started.
    public void InstanceMethod()
    {
        Console.WriteLine(
            "ServerClass.InstanceMethod is running on another thread.");

        // Pause for a moment to provide a delay to make
        // threads more apparent.
        Thread.Sleep(3000);
        Console.WriteLine(
            "The instance method called by the worker thread has ended.");
    }

    public static void StaticMethod()
    {
        Console.WriteLine(
            "ServerClass.StaticMethod is running on another thread.");

        // Pause for a moment to provide a delay to make
        // threads more apparent.
        Thread.Sleep(5000);
        Console.WriteLine(
            "The static method called by the worker thread has ended.");
    }
}

public class Simple
{
    public static void Main()
    {
        ServerClass serverObject = new ServerClass();

        // Create the thread object, passing in the
        // serverObject.InstanceMethod method using a
        // ThreadStart delegate.
        Thread InstanceCaller = new Thread(
            new ThreadStart(serverObject.InstanceMethod));

        // Start the thread.
        InstanceCaller.Start();

        Console.WriteLine("The Main() thread calls this after "
            + "starting the new InstanceCaller thread.");

        // Create the thread object, passing in the
        // serverObject.StaticMethod method using a
        // ThreadStart delegate.
        Thread StaticCaller = new Thread(
            new ThreadStart(ServerClass.StaticMethod));

        // Start the thread.
        StaticCaller.Start();

        Console.WriteLine("The Main() thread calls this after "
            + "starting the new StaticCaller thread.");
    }
}
// The example displays the output like the following:
//    The Main() thread calls this after starting the new InstanceCaller thread.
//    The Main() thread calls this after starting the new StaticCaller thread.
//    ServerClass.StaticMethod is running on another thread.
//    ServerClass.InstanceMethod is running on another thread.
//    The instance method called by the worker thread has ended.
//    The static method called by the worker thread has ended.

2、暂停、zhongda线程

使用Thread.Sleep(t)方法暂停指定时间t内的当前线程。

调用Thread.Interrupt来中断受阻的线程。

3、Thread的属性

IsActive:如果此线程已启动但尚未正常终止或中止,则返回 true

IsBackground:获取或设置布尔值,该值指示线程是否为后台线程。

Name:获取或设置线程的名称。 最常用于在调试时查找各个线程。

Priority:获取或设置由操作系统用来确定线程计划优先顺序的 ThreadPriority 值。

ThreadState:线程的当前状态。

4、计划线程

线程在初始时会被分配默认的优先级Normal,如果要获取/修改,可以使用priority属性。

5、取消线程

以协作的方式取消线程

在 .NET Framework 4 之前,.NET 不提供内置方法在线程启动后以协作方式取消线程。 不过,从 .NET Framework 4 开始,可以使用 System.Threading.CancellationToken 来取消线程,就像使用它们取消 System.Threading.Tasks.Task 对象或 PLINQ 查询一样。 虽然 System.Threading.Thread 类不提供对取消标记的内置支持,但可以使用采用 ParameterizedThreadStart 委托的 Thread 构造函数将一个标记传递给一个线程过程。 下面的示例演示如何执行此操作。

using System;
using System.Threading;

public class ServerClass
{
    public static void StaticMethod(object? obj)
    {
        if (obj is null)
            return;

        CancellationToken ct = (CancellationToken)obj;
        Console.WriteLine("ServerClass.StaticMethod is running on another thread.");

        // Simulate work that can be canceled.
        while (!ct.IsCancellationRequested)
        {
            Thread.SpinWait(50000);
        }
        Console.WriteLine("The worker thread has been canceled. Press any key to exit.");
        Console.ReadKey(true);
    }
}

public class Simple
{
    public static void Main()
    {
        // The Simple class controls access to the token source.
        CancellationTokenSource cts = new CancellationTokenSource();

        Console.WriteLine("Press 'C' to terminate the application...\n");
        // Allow the UI thread to capture the token source, so that it
        // can issue the cancel command.
        Thread t1 = new Thread(() =>
        {
            if (Console.ReadKey(true).KeyChar.ToString().ToUpperInvariant() == "C")
                cts.Cancel();
        });

        // ServerClass sees only the token, not the token source.
        Thread t2 = new Thread(new ParameterizedThreadStart(ServerClass.StaticMethod));
        // Start the UI thread.

        t1.Start();

        // Start the worker thread and pass it the token.
        t2.Start(cts.Token);

        t2.Join();
        cts.Dispose();
    }
}
// The example displays the following output:
//       Press 'C' to terminate the application...
//
//       ServerClass.StaticMethod is running on another thread.
//       The worker thread has been canceled. Press any key to exit.

#1# 如何通过轮询侦听取消请求

下面的示例展示了一种方便用户代码定期轮询取消令牌,以确定是否已通过调用线程发出取消请求的方式。 此示例使用 System.Threading.Tasks.Task 类型,但相同的模式适用于 System.Threading.ThreadPool 类型或 System.Threading.Thread 类型直接创建的异步操作。

若要轮询,必须有某种循环或递归代码,可用于定期读取布尔 IsCancellationRequested 属性的值。 如果使用的是 System.Threading.Tasks.Task 类型,且正在等待任务在调用线程上完成,可以使用 ThrowIfCancellationRequested 方法来检查属性并抛出异常。 通过使用此方法,可确保抛出正确的异常来响应请求。 如果使用的是 Task,那么调用此方法优于手动抛出 OperationCanceledException。 如果无需抛出异常,可以直接检查属性,并通过方法返回结果(如果属性是 true 的话)。

using System;
using System.Threading;
using System.Threading.Tasks;

public struct Rectangle
{
   public int columns;
   public int rows;
}

class CancelByPolling
{
   static void Main()
   {
      var tokenSource = new CancellationTokenSource();
      // Toy object for demo purposes
      Rectangle rect = new Rectangle() { columns = 1000, rows = 500 };

      // Simple cancellation scenario #1. Calling thread does not wait
      // on the task to complete, and the user delegate simply returns
      // on cancellation request without throwing.
      Task.Run(() => NestedLoops(rect, tokenSource.Token), tokenSource.Token);

      // Simple cancellation scenario #2. Calling thread does not wait
      // on the task to complete, and the user delegate throws
      // OperationCanceledException to shut down task and transition its state.
      // Task.Run(() => PollByTimeSpan(tokenSource.Token), tokenSource.Token);

      Console.WriteLine("Press 'c' to cancel");
      if (Console.ReadKey(true).KeyChar == 'c') {
          tokenSource.Cancel();
          Console.WriteLine("Press any key to exit.");
      }

      Console.ReadKey();
      tokenSource.Dispose();
  }

   static void NestedLoops(Rectangle rect, CancellationToken token)
   {
      for (int col = 0; col < rect.columns && !token.IsCancellationRequested; col++) {
         // Assume that we know that the inner loop is very fast.
         // Therefore, polling once per column in the outer loop condition
         // is sufficient.
         for (int row = 0; row < rect.rows; row++) {
            // Simulating work.
            Thread.SpinWait(5_000);
            Console.Write("{0},{1} ", col, row);
         }
      }

      if (token.IsCancellationRequested) {
         // Cleanup or undo here if necessary...
         Console.WriteLine("\r\nOperation canceled");
         Console.WriteLine("Press any key to exit.");

         // If using Task:
         // token.ThrowIfCancellationRequested();
      }
   }
}

调用 ThrowIfCancellationRequested 非常快,不会在循环中产生很大的开销。

如果调用的是 ThrowIfCancellationRequested,那么如果除了抛出异常之外,还要执行其他工作来响应取消,只需显式检查 IsCancellationRequested 属性即可。 在此示例中,可以看到代码实际访问属性两次:一次是显式访问,另一次是在 ThrowIfCancellationRequested 方法中。 不过,由于读取 IsCancellationRequested 属性在每次访问时只涉及一个易失读取指令,因此从性能角度来看,双重访问的意义并不大。 最好仍调用此方法,而不是手动抛出 OperationCanceledException

#2# 注册取消请求的回调

了解如何注册当 IsCancellationRequested 属性为 true 时调用的委托。 对创建令牌的对象调用 Cancel 时,值将从 false 更改为 true。 使用此技术可取消本地不支持统一取消框架的异步操作,也可取消阻止可能等待异步操作结束的方法。

在下面的示例中,CancelAsync 方法注册为在通过取消标记请求取消时要调用的方法。

using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

class CancelWithCallback
{
    static void Main()
    {
        using var cts = new CancellationTokenSource();
        var token = cts.Token;

        _ = Task.Run(async () =>
        {
            using var client = new WebClient();

            client.DownloadStringCompleted += (_, args) =>
            {
                if (args.Cancelled)
                {
                    Console.WriteLine("The download was canceled.");
                }
                else
                {
                    Console.WriteLine("The download has completed:\n");
                    Console.WriteLine($"{args.Result}\n\nPress any key to continue.");
                }
            };

            if (!token.IsCancellationRequested)
            {
                using CancellationTokenRegistration ctr = token.Register(() => client.CancelAsync());

                Console.WriteLine("Starting request\n");
                await client.DownloadStringTaskAsync(new Uri("http://www.contoso.com"));
            }
        }, token);

        Console.WriteLine("Press 'c' to cancel.\n\n");
        if (Console.ReadKey().KeyChar == 'c')
        {
            cts.Cancel();
        }

        Console.WriteLine("\nPress any key to exit.");
        Console.ReadKey();
    }
}

如果注册回调时已经请求了取消,那么仍然要保证调用回调。 在此特定情况下,如果当前未执行异步操作,则 CancelAsync 方法将不进行任何操作,因此调用此方法始终是安全的。

#3# 如何侦听具有等待句柄的取消请求

如果方法在等待事件收到信号时受阻止,既无法检查取消令牌的值,也无法及时响应。 第一个示例展示了如何在使用 System.Threading.ManualResetEvent 等不本机支持统一取消框架的事件时解决此问题。 第二个示例展示了一种更简化的方法,即使用确实支持统一取消的 System.Threading.ManualResetEventSlim

下面的示例使用 ManualResetEvent 展示了如何取消阻止不支持统一取消的等待句柄。

using System;
using System.Threading;
using System.Threading.Tasks;

class CancelOldStyleEvents
{
    // Old-style MRE that doesn't support unified cancellation.
    static ManualResetEvent mre = new ManualResetEvent(false);

    static void Main()
    {
        var cts = new CancellationTokenSource();

        // Pass the same token source to the delegate and to the task instance.
        Task.Run(() => DoWork(cts.Token), cts.Token);
        Console.WriteLine("Press s to start/restart, p to pause, or c to cancel.");
        Console.WriteLine("Or any other key to exit.");

        // Old-style UI thread.
        bool goAgain = true;
        while (goAgain)
        {
            char ch = Console.ReadKey(true).KeyChar;

            switch (ch)
            {
                case 'c':
                    cts.Cancel();
                    break;
                case 'p':
                    mre.Reset();
                    break;
                case 's':
                    mre.Set();
                    break;
                default:
                    goAgain = false;
                    break;
            }

            Thread.Sleep(100);
        }
        cts.Dispose();
    }

    static void DoWork(CancellationToken token)
    {
        while (true)
        {
            // Wait on the event if it is not signaled.
            int eventThatSignaledIndex =
                   WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
                                      new TimeSpan(0, 0, 20));

            // Were we canceled while waiting?
            if (eventThatSignaledIndex == 1)
            {
                Console.WriteLine("The wait operation was canceled.");
                throw new OperationCanceledException(token);
            }
            // Were we canceled while running?
            else if (token.IsCancellationRequested)
            {
                Console.WriteLine("I was canceled while running.");
                token.ThrowIfCancellationRequested();
            }
            // Did we time out?
            else if (eventThatSignaledIndex == WaitHandle.WaitTimeout)
            {
                Console.WriteLine("I timed out.");
                break;
            }
            else
            {
                Console.Write("Working... ");
                // Simulating work.
                Thread.SpinWait(5000000);
            }
        }
    }
}

下面的示例使用 ManualResetEventSlim 展示了如何取消阻止确实支持统一取消的协调基元。 相同的方法可以与其他轻型协调基元(如 SemaphoreSlim 和 CountdownEvent)结合使用。

using System;
using System.Threading;
using System.Threading.Tasks;

class CancelNewStyleEvents
{
   // New-style MRESlim that supports unified cancellation
   // in its Wait methods.
   static ManualResetEventSlim mres = new ManualResetEventSlim(false);

   static void Main()
   {
      var cts = new CancellationTokenSource();

      // Pass the same token source to the delegate and to the task instance.
      Task.Run(() => DoWork(cts.Token), cts.Token);
      Console.WriteLine("Press c to cancel, p to pause, or s to start/restart,");
      Console.WriteLine("or any other key to exit.");

      // New-style UI thread.
         bool goAgain = true;
         while (goAgain)
         {
             char ch = Console.ReadKey(true).KeyChar;

             switch (ch)
             {
                 case 'c':
                     // Token can only be canceled once.
                     cts.Cancel();
                     break;
                 case 'p':
                     mres.Reset();
                     break;
                 case 's':
                     mres.Set();
                     break;
                 default:
                     goAgain = false;
                     break;
             }

             Thread.Sleep(100);
         }
         cts.Dispose();
     }

     static void DoWork(CancellationToken token)
     {

         while (true)
         {
             if (token.IsCancellationRequested)
             {
                 Console.WriteLine("Canceled while running.");
                 token.ThrowIfCancellationRequested();
             }

             // Wait on the event to be signaled
             // or the token to be canceled,
             // whichever comes first. The token
             // will throw an exception if it is canceled
             // while the thread is waiting on the event.
             try
             {
                 // mres is a ManualResetEventSlim
                 mres.Wait(token);
             }
             catch (OperationCanceledException)
             {
                 // Throw immediately to be responsive. The
                 // alternative is to do one more item of work,
                 // and throw on next iteration, because
                 // IsCancellationRequested will be true.
                 Console.WriteLine("The wait operation was canceled.");
                 throw;
             }

             Console.Write("Working...");
             // Simulating work.
             Thread.SpinWait(500000);
         }
     }
 }

#4# 如何侦听多个取消请求

此示例展示了如何同时侦听两个取消令牌,以便在其中任意一个令牌发出请求时取消操作。

在下面的示例中,CreateLinkedTokenSource 方法用于将两个令牌联接为一个令牌。 这样一来,可以将此令牌传递到只需要使用一个取消令牌作为参数的方法中。 此示例展示了以下常见方案:方法必须同时观察从类外部传入的令牌和类内部生成的令牌。

using System;
using System.Threading;
using System.Threading.Tasks;

class LinkedTokenSourceDemo
{
    static void Main()
    {
        WorkerWithTimer worker = new WorkerWithTimer();
        CancellationTokenSource cts = new CancellationTokenSource();

        // Task for UI thread, so we can call Task.Wait wait on the main thread.
        Task.Run(() =>
        {
            Console.WriteLine("Press 'c' to cancel within 3 seconds after work begins.");
            Console.WriteLine("Or let the task time out by doing nothing.");
            if (Console.ReadKey(true).KeyChar == 'c')
                cts.Cancel();
        });

        // Let the user read the UI message.
        Thread.Sleep(1000);

        // Start the worker task.
        Task task = Task.Run(() => worker.DoWork(cts.Token), cts.Token);

        try
        {
            task.Wait(cts.Token);
        }
        catch (OperationCanceledException e)
        {
            if (e.CancellationToken == cts.Token)
                Console.WriteLine("Canceled from UI thread throwing OCE.");
        }
        catch (AggregateException ae)
        {
            Console.WriteLine("AggregateException caught: " + ae.InnerException);
            foreach (var inner in ae.InnerExceptions)
            {
                Console.WriteLine(inner.Message + inner.Source);
            }
        }

        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
        cts.Dispose();
    }
}

class WorkerWithTimer
{
    CancellationTokenSource internalTokenSource = new CancellationTokenSource();
    CancellationToken internalToken;
    CancellationToken externalToken;
    Timer timer;

    public WorkerWithTimer()
    {
        // A toy cancellation trigger that times out after 3 seconds
        // if the user does not press 'c'.
        timer = new Timer(new TimerCallback(CancelAfterTimeout), null, 3000, 3000);
    }

    public void DoWork(CancellationToken externalToken)
    {
        // Create a new token that combines the internal and external tokens.
        this.internalToken = internalTokenSource.Token;
        this.externalToken = externalToken;

        using (CancellationTokenSource linkedCts =
                CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken))
        {
            try
            {
                DoWorkInternal(linkedCts.Token);
            }
            catch (OperationCanceledException)
            {
                if (internalToken.IsCancellationRequested)
                {
                    Console.WriteLine("Operation timed out.");
                }
                else if (externalToken.IsCancellationRequested)
                {
                    Console.WriteLine("Cancelling per user request.");
                    externalToken.ThrowIfCancellationRequested();
                }
            }
        }
    }

    private void DoWorkInternal(CancellationToken token)
    {
        for (int i = 0; i < 1000; i++)
        {
            if (token.IsCancellationRequested)
            {
                // We need to dispose the timer if cancellation
                // was requested by the external token.
                timer.Dispose();

                // Throw the exception.
                token.ThrowIfCancellationRequested();
            }

            // Simulating work.
            Thread.SpinWait(7500000);
            Console.Write("working... ");
        }
    }

    public void CancelAfterTimeout(object? state)
    {
        Console.WriteLine("\r\nTimer fired.");
        internalTokenSource.Cancel();
        timer.Dispose();
    }
}

如果链接令牌抛出 OperationCanceledException,传递到异常的令牌是链接令牌,而不是任意前置任务令牌。 若要确定取消的是哪个令牌,请直接查看前置任务令牌的状态。

虽然 AggregateException 不得引发,但此示例捕获到它的原因是,在实际情况下,任务委托引发的除 OperationCanceledException 外其他任何异常都包装在 AggregateException 中。

6、销毁线程

要终止线程,使用 System.Threading.CancellationToken。

若要强制终止线程的执行,使用Thread.Abort()

.Net Core不支持Thread.Abort方法,如果此时要强制终止,需要在单独的进程中运行该代码并使用Process.Kill

还需要注意,System.Threading.CancellationToken在.Net Framework 4之前的版本中无法使用。

线程一旦中止,将无法重启。

处理 ThreadAbortException

如果应中止线程,无论是由于在自己的代码中调用 Abort 所致,还是由于卸载正在运行线程的应用域(AppDomain.Unload 使用 Thread.Abort 终止线程)所致,线程都必须处理 ThreadAbortException,并在 finally 子句中执行任何最终处理,如下面的代码所示。

try
{  
    // Code that is executing when the thread is aborted.  
}
catch (ThreadAbortException ex)
{  
    // Clean-up code can go here.  
    // If there is no Finally clause, ThreadAbortException is  
    // re-thrown by the system at the end of the Catch clause.
}  
// Do not put clean-up code here, because the exception
// is rethrown at the end of the Finally clause.

清理代码必须位于 catch 子句或 finally 子句中,因为系统会在 finally 子句末尾或 catch 子句(如果没有 finally 子句的话)末尾重新抛出 ThreadAbortException

可以调用 Thread.ResetAbort 方法,以防系统重新抛出异常。 不过,只有在自己的代码导致 ThreadAbortException 抛出时,才应这样做。

7、死锁

两个线程中的每一个线程都尝试锁定另外一个线程已锁定的资源时,就会发生死锁,导致两个线程都无法执行。

托管线程处理类的许多方法都提供了超时锁定,有助于检测死锁。

例如,下面的代码舱室在lockObject对象上获取锁,如果300ms内没有获取锁,则返回false:

if (Monitor.TryEnter(lockObject, 300)) {  
    try {  
        // Place code protected by the Monitor here.  
    }  
    finally {  
        Monitor.Exit(lockObject);  
    }  
}  
else {  
    // Code to execute if the attempt times out.  
}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值