C# 线程基础

参考《C#多线程编程实战》(原书第二版),第1章“线程基础”。

本文不是全部照搬原书,记录号主认为比较关键的部分,并加上自己的感悟。

本章将涵盖C#中使用线程的基本操作.

  • 使用C#创建线程
  • 暂停线程
  • 线程等待
  • 终止线程
  • 检测线程状态
  • 线程优先级
  • 前台线程和后台线程
  • 向线程传参
  • 使用C#中的lock关键字
  • 使用Monitor类锁定资源
  • 处理异常

1.1 简介

请记住线程会消耗大量的操作系统资源。多个线程共享一个物理处理器将导致操作系统忙于管理这些线程,而无法运行程序。

试图在单核CPU上并行执行计算任务是没有意义的,因为这比顺序运行会花费更多的时间。然而,当处理器拥有多核时,过去的应用程序则不能使用这个优势,因为它们只使用了一个处理核心。

为了有效地利用现代处理器的计算能力,使用某种方式让程序能使用不止一个处理核心是非常重要的。这需要组织多个线程间的通信和相互同步。

1.2 使用C#创建线程

编写如下代码:

using System;
using System.Threading;

namespace First
{
  class Program
	{
		static void Main(string[] args)
		{
			new First().TestThread();
		}
	}
  
	public class First
	{
		public void TestThread()
		{
			Thread t = new Thread(PrintNumbers);
			t.Start();
			PrintNumbers();
		}

		public void PrintNumbers()
		{
			Console.WriteLine("Starting...");
			for (int i = 1; i < 10; i++)
			{
				Console.WriteLine(i);
			}
		}
	}
}

程序运行结果:

Starting...
Starting...
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9

结果两组范围为1到10的数字会随机交叉输出(上面的结果,可能是号主电脑配置太高,只是第一行Starting...在两个线程里先后打印了)。这说明PrintNumbers方法同时运行在主线程和另一个线程中。

改下代码:

using System;
using System.Threading;

namespace First
{
	class Program
	{
		static void Main(string[] args)
		{
			new First().TestThread();
		}
	}
	public class First
	{
		public void TestThread()
		{
			Thread t = new Thread(PrintNumbers);
			t.Start();
			PrintNumbers();
		}

		public void PrintNumbers()
		{
			Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:Starting...");
			for (int i = 1; i < 10; i++)
			{
				Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:{i}");
			}
		}
	}
}

重新运行输出为:

1:Starting...
4:Starting...
4:1
4:2
4:3
4:4
1:1
1:2
1:3
1:4
1:5
1:6
1:7
1:8
1:9
4:5
4:6
4:7
4:8
4:9

效果更明显了,前面打印了线程ID,明显数字随机交叉输出。

这一章比较基础,我们快速略过。

1.3 暂停线程

暂停线程,即在线程中处理耗时操作时,让线程休眠一段时间(Thread.Sleep)而不用消耗操作系统资源(或者说占用尽可能少的CPU时间),代码如下:

public void PrintNumbersWithDelay()
{
  Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:Starting...");
  for (int i = 1; i < 10; i++)
  {
    Thread.Sleep(TimeSpan.FromSeconds(2));
    Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:{i}");
  }
}

1.4 线程等待

本节将展示如何让程序等待另一个线程中的计算完成,然后在代码中使用该线程的计算结果。使用Thread.Sleep行不通,因为并不知道执行计算需要花费的具体时间。

代码如下:

Console.WriteLine("Starting...");
Thread thread = new Thread(PrintNumbersWithDelay);
thread.Start();
thread.Join();
Console.WriteLine("Thread complete");

运行结果:

Starting...
4:Starting...
4:1
4:2
4:3
4:4
4:5
4:6
4:7
4:8
4:9
Thread complete

使用Thread.Join(),该方法允许我们等待直到线程thread完成,才执行主线程打印Thread complete代码。借助该技术可以实现在两个线程间同步执行步骤。

1.5 终止线程

关键代码就是thread.Abort(),该方法不推荐使用,就不贴事例代码了,说说原因:

调用thread.Abort()方法,这给线程注入了ThreadAbortException方法,导致线程被终结。这非常危险,因为该异常可以在任何时刻发生并可能彻底摧毁应用程序。另外,使用该技术也不一定总能终止线程。目标线程可能通过处理该异常并调用Thread.ResetAbort方法来拒绝被终止。因此并不推荐使用Abort方法来关闭线程。可优先使用一些其他方法,比如提供一个CancellationToken方法来取消线程的执行。在第3章中我们会讨论该方法。

1.6 检测线程状态

获取线程是否已经启动或是否处于阻塞状态等相应信息是非常有用的。请注意由于线程是独立运行的,所有其状态可以在任何时候被改变。

获取线程状态的关键代码是CurrentThread.ThreadState.ToString(),没啥好说的,直接贴出线程状态码枚举吧:

namespace System.Threading
{
	//
	// 摘要:
	//     Specifies the execution states of a System.Threading.Thread.
	[Flags]
	public enum ThreadState
	{
		//
		// 摘要:
		//     The thread has been started and not yet stopped.
		Running = 0,
		//
		// 摘要:
		//     The thread is being requested to stop. This is for internal use only.
		StopRequested = 1,
		//
		// 摘要:
		//     The thread is being requested to suspend.
		SuspendRequested = 2,
		//
		// 摘要:
		//     The thread is being executed as a background thread, as opposed to a foreground
		//     thread. This state is controlled by setting the System.Threading.Thread.IsBackground
		//     property.
		Background = 4,
		//
		// 摘要:
		//     The System.Threading.Thread.Start method has not been invoked on the thread.
		Unstarted = 8,
		//
		// 摘要:
		//     The thread has stopped.
		Stopped = 16,
		//
		// 摘要:
		//     The thread is blocked. This could be the result of calling System.Threading.Thread.Sleep(System.Int32)
		//     or System.Threading.Thread.Join, of requesting a lock - for example, by calling
		//     System.Threading.Monitor.Enter(System.Object) or System.Threading.Monitor.Wait(System.Object,System.Int32,System.Boolean)
		//     - or of waiting on a thread synchronization object such as System.Threading.ManualResetEvent.
		WaitSleepJoin = 32,
		//
		// 摘要:
		//     The thread has been suspended.
		Suspended = 64,
		//
		// 摘要:
		//     The System.Threading.Thread.Abort(System.Object) method has been invoked on the
		//     thread, but the thread has not yet received the pending System.Threading.ThreadAbortException
		//     that will attempt to terminate it.
		AbortRequested = 128,
		//
		// 摘要:
		//     The thread state includes System.Threading.ThreadState.AbortRequested and the
		//     thread is now dead, but its state has not yet changed to System.Threading.ThreadState.Stopped.
		Aborted = 256
	}
}

1.8 前台线程和后台线程

设置关键代码Thread.IsBackground=true,下面是事例代码:

using System;
using System.Threading;

namespace First
{
	class Program
	{
		static void Main(string[] args)
		{
			var sampleForeground = new ThreadSample(10);
			var sampleBackground = new ThreadSample(20);

			var threadOne = new Thread(sampleForeground.CountNumbers);
			threadOne.Name = "ForegroundThread";
			var threadTwo = new Thread(sampleBackground.CountNumbers);
			threadTwo.Name = "BackgroundThread";
			threadTwo.IsBackground = true;

			threadOne.Start();
			threadTwo.Start();
		}
	}
  
	public class ThreadSample
	{
		private readonly int _iterations;

		public ThreadSample(int iterations)
		{
			_iterations = iterations;
		}

		public void CountNumbers()
		{
			for(int i = 0; i < _iterations;i++)
			{
				Thread.Sleep(TimeSpan.FromSeconds(0.5));
				Console.WriteLine($"{Thread.CurrentThread.Name} prints {i}");
			}
		}
	}
}

程序输出结果:

ForegroundThread prints 0
BackgroundThread prints 0
BackgroundThread prints 1
ForegroundThread prints 1
BackgroundThread prints 2
ForegroundThread prints 2
ForegroundThread prints 3
BackgroundThread prints 3
ForegroundThread prints 4
BackgroundThread prints 4
ForegroundThread prints 5
BackgroundThread prints 5
ForegroundThread prints 6
BackgroundThread prints 6
ForegroundThread prints 7
BackgroundThread prints 7
ForegroundThread prints 8
BackgroundThread prints 8
ForegroundThread prints 9
BackgroundThread prints 9

F:\dotnet\CsharpMultyThread\src\First\bin\Debug\net5.0\First.exe (进程 13676)已退出,代码为 0。
按任意键关闭此窗口. . .

说明:

当主程序启动时定义了两个不同的线程。默认情况下,显示创建的线程是前台线程。通过手动的设置threadTwo对象的IsBackground属性为true来创建一个后台线程。通过配置来实现第一个线程比第二个线程先完成,然后运行程序。

第一个线程完成后,程序结束并且后台线程threadTwo被终结。这是前台线程与后台线程的主要区别:进程会等待所有的前台线程完成后再结束工作,但是如果只剩下后台线程,则会直接结束工作。

一个重要注意事项是如果程序定义了一个不会完成的前台线程,主程序并不会正常结束。

太晚了,下次接着看1.9 向线程传递参数

除非注明,文章均由 Dotnet9 整理发布,欢迎转载。

转载请注明:
作者:乐趣课堂
链接:https://dotnet9.com/17150.html
来源:Dotnet9
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

发表评论

登录后才能评论