线程在C#中是由System.Threading下的Thread类实例来体现。如果我们想要创建一个线程,只需要创建一个Thread的实例,然后调用Start方法来通知操作系统创建一个新线程,并且在新线程中运行指定的代码,接下来我们来拆解一下具体的相关方法,最后看一下完整的示例。

Thread构造函数

  1. public Thread(ParameterizedThreadStart start);
  2. public Thread(ThreadStart start);
  3. public Thread(ParameterizedThreadStart start, int maxStackSize);
  4. public Thread(ThreadStart start, int maxStackSize);

从上面的代码中可以看到,一共有四个构造函数,但可以分为两组,一组是不指定最大栈大小,由操作系统按默认值进行分配,一组是指定栈大小,如果指定栈大小时需要注意,如果指定的栈大小大于默认的栈大小则将忽略指定的值而使用默认值,并且不会抛出异常。
在不指定栈大小的组里面,构造函数分别接收ThreadStart和ParameterizedThreadStart实例,我们再来看一下这两个的定义是什么。

  1. public delegate void ThreadStart();
  2. public delegate void ParameterizedThreadStart(object? obj);

从上面的定义可以看出,两个都是一个委托定义,ThreadStart委托封装了一个没有任何参数和返回值的方法,而ParameterizedThreadStart委托封装了只有一个参数(由于所有类型都是从object继承的,所以此参数实际上可以接收任何类型),仍然没有返回值的方法。
从上面的委托定义可以看出,在线程里面运行的方法,要求都是没有返回值的,可以传递0个或者1个参数。

Thread常用方法

常用静态方法

  1. //获取运行当前代码的线程实例
  2. public static Thread CurrentThread { get; }
  3. //获取当前执行代码的cpu id
  4. public static int GetCurrentProcessorId();
  5. //将执行代码的当前线程进入到沉睡状态,操作系统将立即调度其他线程来运行
  6. //在指定时间过后,操作系统才可能会再次调度本线程来运行,时间不是很精确
  7. //建议生产环境慎用此方法,因为在此期间相当于占用了资源,但什么事也不做,纯属浪费资源
  8. //但Sleep(0)可以用来通知操作系统立即调度其他线程来执行,同时下一个时间片又可以调度本线程
  9. public static void Sleep(int millisecondsTimeout);
  10. public static void Sleep(TimeSpan timeout);

常用实例方法

  1. //开始当前线程的运行,根据构造函数中指定的委托是否需要参数来调用相应的方法
  2. //执行此方法后,操作系统将会产生一个新的线程,并且在新线程里面开始运行指定委托的代码
  3. public void Start();
  4. public void Start(object? parameter);
  5. //等待指定线程执行完成,并且阻塞调用此方法的线程
  6. public void Join();
  7. //在一定时间内等待指定线程执行完成,并且阻塞调用此方法的线程
  8. //如果在指定时间内,指定线程执行完成,则返回true
  9. //如果在指定时间内,指定线程没有执行完成,则返回false
  10. public bool Join(TimeSpan timeout);
  11. public bool Join(int millisecondsTimeout);
  12. //获取当前线程是否还处于存活状态
  13. //只有在已经开始运行,并且没有运行结束或中止运行时返回true,其他情况下返回false
  14. public bool IsAlive { get; }
  15. //获取或设置当前线程是否是后台线程
  16. //操作系统在退出进程时,会等待进程的所有非后台线程的执行结束,但后台线程是不会等待的
  17. public bool IsBackground { get; set; }
  18. //获取运行当前代码的线程是否是线程池中的线程
  19. public bool IsThreadPoolThread { get; }
  20. //获取当前线程的线程id,此id是由操作系统分配的
  21. public int ManagedThreadId { get; }
  22. //获取或设置一个当前线程的名称
  23. public string? Name { get; set; }
  24. //获取或设置当前线程的优先级,优先级越高,在调度时将优先于其他低优先级的线程
  25. public ThreadPriority Priority { get; set; }
  26. //获取当前线程的状态
  27. //Running,StopRequested,SuspendRequested,Background,Unstarted,Stopped,WaitSleepJoin,Suspended,AbortRequested,Aborted
  28. public ThreadState ThreadState { get; }

Thread示例

示例代码

  1. using System;
  2. using System.Threading;
  3. namespace ThreadDemo1Start
  4. {
  5. class Program
  6. {
  7. const int Count = 1000;
  8. static void Main(string[] args)
  9. {
  10. //指定要在新线程中运行的代码,将在控制台中输出+号
  11. ThreadStart threadStart = DoWork;
  12. //创建一个新的线程实例
  13. Thread thread = new Thread(threadStart);
  14. //开始运行新的线程实例
  15. thread.Start();
  16. //此处的代码不会被上面的start阻塞,是会同时运行的
  17. //在控制台中输出-号
  18. for(var i = 0; i < Count; i++)
  19. {
  20. Console.Write("-");
  21. }
  22. //等待线程执行完成
  23. thread.Join();
  24. //全部执行完毕
  25. Console.WriteLine("全部执行完毕");
  26. }
  27. static void DoWork()
  28. {
  29. for(var i = 0; i < Count; i++)
  30. {
  31. Console.Write("+");
  32. }
  33. }
  34. }
  35. }

示例执行结果

image.png
从上面的结果可以看出,+-号是互相交替出现的。全部执行完毕一定是在最后的。

示例优化

从上面的代码中可以看出,主线程中也有输出代码,DoWork中也有输出代码,除了字符以外都是相同的,所以对以上代码进行重构,这次看一下如何给线程传递参数。

  1. using System;
  2. using System.Threading;
  3. namespace ThreadDemo1Start
  4. {
  5. class Program
  6. {
  7. const int Count = 1000;
  8. static void Main(string[] args)
  9. {
  10. //指定要在新线程中运行的代码,将在控制台中输出+号
  11. ParameterizedThreadStart threadStart = DoWork;
  12. //创建一个新的线程实例
  13. Thread thread = new Thread(threadStart);
  14. //开始运行新的线程实例
  15. thread.Start("+");
  16. //此处的代码不会被上面的start阻塞,是会同时运行的
  17. //在控制台中输出-号
  18. DoWork("-");
  19. //等待线程执行完成
  20. thread.Join();
  21. //全部执行完毕
  22. Console.WriteLine("全部执行完毕");
  23. }
  24. static void DoWork(object? obj)
  25. {
  26. string str = obj.ToString();
  27. for(var i = 0; i < Count; i++)
  28. {
  29. Console.Write(str);
  30. }
  31. }
  32. }
  33. }

优化后的执行结果与上面类似。

下期预告

使用上面这种方式来创建线程,有一个问题,就是每次都是重新创建一个新的线程,当运行比较频繁的话,则可能线程的创建和销毁会带来不少的性能损耗,为此可以使用线程池来解决这个问题,下期我们再讲一下线程池的使用,敬请期待。