C# 编程/The .NET Framework/线程
线程是能够与其他线程并发运行并共享数据的任务。当您的程序启动时,它会为程序的入口点创建一个线程,通常是 Main
函数。因此,您可以将“程序”视为由线程组成的。.NET Framework 允许您在程序中使用线程以并行运行代码。这通常出于以下两个原因
- 如果运行图形用户界面的线程执行时间过长的工作,您的程序可能看起来没有响应。使用线程,您可以创建新线程来执行任务并将进度报告给 GUI 线程。
- 在拥有多个 CPU 或 CPU 拥有多个内核的计算机上,线程可以最大限度地利用计算资源,从而加快任务速度。
The System.Threading.Thread
类公开了使用线程的基本功能。要创建线程,只需使用 ThreadStart
或 ParameterizedThreadStart
委托 创建 Thread
类的实例,该委托指向线程应开始运行的代码。例如
using System;
using System.Threading;
public static class Program
{
private static void SecondThreadFunction()
{
while (true)
{
Console.WriteLine("Second thread says hello.");
Thread.Sleep(1000); // pause execution of the current thread for 1 second (1000 ms)
}
}
public static void Main()
{
Thread newThread = new Thread(new ThreadStart(SecondThreadFunction));
newThread.Start();
while (true)
{
Console.WriteLine("First thread says hello.");
Thread.Sleep(500); // pause execution of the current thread for half a second (500 ms)
}
}
}
您应该看到以下输出
Second thread says hello. First thread says hello. First thread says hello. Second thread says hello. First thread says hello. First thread says hello. ...
请注意,需要使用 while 关键字,因为一旦函数 返回,线程就会退出或终止。
The void ParameterizedThreadStart(object obj)
委托允许您向新线程传递参数
using System;
using System.Threading;
public static class Program
{
private static void SecondThreadFunction(object param)
{
while (true)
{
Console.WriteLine("Second thread says " + param.ToString() + ".");
Thread.Sleep(500); // pause execution of the current thread for half a second (500 ms)
}
}
public static void Main()
{
Thread newThread = new Thread(new ParameterizedThreadStart(SecondThreadFunction));
newThread.Start(1234); // here you pass a parameter to the new thread
while (true)
{
Console.WriteLine("First thread says hello.");
Thread.Sleep(1000); // pause execution of the current thread for a second (1000 ms)
}
}
}
输出为
First thread says hello. Second thread says 1234. Second thread says 1234. First thread says hello. ...
虽然我们可以使用 ParameterizedThreadStart
向线程传递参数,但这不类型安全且使用起来很笨拙。我们可以利用匿名委托在线程之间共享数据,但是
using System;
using System.Threading;
public static class Program
{
public static void Main()
{
int number = 1;
Thread newThread = new Thread(new ThreadStart(delegate
{
while (true)
{
number++;
Console.WriteLine("Second thread says " + number.ToString() + ".");
Thread.Sleep(1000);
}
}));
newThread.Start();
while (true)
{
number++;
Console.WriteLine("First thread says " + number.ToString() + ".");
Thread.Sleep(1000);
}
}
}
请注意匿名委托的正文如何访问局部变量 number
。
使用匿名委托会导致很多语法、作用域混淆以及缺乏封装。但是,使用 lambda 表达式,可以缓解其中一些问题。您可以使用异步委托来传递和返回数据,而不是匿名委托,所有这些都是类型安全的。需要注意的是,当您使用异步委托时,实际上是将新线程排队到线程池。此外,使用异步委托会强迫您使用异步模型。
using System;
public static class Program
{
delegate int del(int[] data);
public static int SumOfNumbers(int[] data)
{
int sum = 0;
foreach (int number in data) {
sum += number;
}
return sum;
}
public static void Main()
{
int[] numbers = new int[] { 1, 2, 3, 4, 5 };
del func = SumOfNumbers;
IAsyncResult result = func.BeginInvoke(numbers, null, null);
// I can do stuff here while numbers is being added
int sum = func.EndInvoke(result);
sum = 15
}
}
在共享数据示例中,您可能已经注意到,通常,如果不是一直的话,您会得到以下输出
First thread says 2. Second thread says 3. Second thread says 5. First thread says 4. Second thread says 7. First thread says 7.
人们会期望至少数字是按升序打印的!此问题源于两段代码同时运行的事实。例如,它打印了 3、5,然后打印了 4。让我们检查一下可能发生的事情
- 在“第一个线程说 2”之后,第一个线程对
number
进行递增,使其变为 3,并将其打印出来。 - 然后,第二个线程对
number
进行递增,使其变为 4。 - 就在第二个线程有机会打印
number
之前,第一个线程对number
进行递增,使其变为 5,并将其打印出来。 - 然后,第二个线程打印了第一个线程递增之前
number
的值,即 4。请注意,这可能是由于控制台输出缓冲引起的。
此问题的解决方案是同步这两个线程,确保它们的代码不会像以前那样交错。C# 通过 lock 关键字支持这一点。我们可以将代码块放在此关键字下
using System;
using System.Threading;
public static class Program
{
public static void Main()
{
int number = 1;
object numberLock = new object();
Thread newThread = new Thread(new ThreadStart(delegate
{
while (true)
{
lock (numberLock)
{
number++;
Console.WriteLine("Second thread says " + number.ToString() + ".");
}
Thread.Sleep(1000);
}
}));
newThread.Start();
while (true)
{
lock (numberLock)
{
number++;
Console.WriteLine("First thread says " + number.ToString() + ".");
}
Thread.Sleep(1000);
}
}
}
需要变量 numberLock
,因为 lock 关键字只对引用类型起作用,而不是值类型。这一次,您将获得正确的输出
First thread says 2. Second thread says 3. Second thread says 4. First thread says 5. Second thread says 6. ...
The lock 关键字的工作原理是尝试对传递给它的对象(numberLock
)获得独占锁。它只会在代码块执行完毕后(即在 }
之后)释放锁。如果对象已经被锁定,而另一个线程试图对同一个对象获得锁,则该线程将阻塞(挂起执行)直到锁被释放,然后锁定该对象。这样,就可以防止代码段交错。
The Join
方法 of the Thread
类允许线程等待另一个线程,可以选择指定超时
using System;
using System.Threading;
public static class Program
{
public static void Main()
{
Thread newThread = new Thread(new ThreadStart(delegate
{
Console.WriteLine("Second thread reporting.");
Thread.Sleep(5000);
Console.WriteLine("Second thread done sleeping.");
}));
newThread.Start();
Console.WriteLine("Just started second thread.");
newThread.Join(1000);
Console.WriteLine("First thread waited for 1 second.");
newThread.Join();
Console.WriteLine("First thread finished waiting for second thread. Press any key.");
Console.ReadKey();
}
}
输出为
Just started second thread. Second thread reporting. First thread waited for 1 second. Second thread done sleeping. First thread finished waiting for second thread. Press any key.