C# 多线程 01 线程基础
🏷️ 《C# 多线程》
创建线程
csharp
Thread t = new Thread(PrintNumbers);
t.Start();
暂停线程
csharp
Thread.Sleep(TimeSpan.FromSeconds(2));
线程等待
csharp
Thread t = new Thread(PrintNumbersWithDelay);
t.Start();
// 线程等待直到 t 执行结束,主线程才会继续执行
t.Join();
终止线程
csharp
Thread t = new Thread(PrintNumbersWithDelay);
t.Start();
Thread.Sleep(TimeSpan.FromSeconds(6));
// 终止线程
// 这给线程注入了 ThreadAbortException 方法,导致线程被终结。这非常危险,因为该异常可以在任何时刻发生并可能摧毁应用程序
// 另外,使用该技术也不一定总能终止线程。
// 目标线程可以通过处理该异常并调用 Thread.ResetAbort 方法来拒绝被终止。
// 因此并不推荐使用 Abort 方法来终止线程。
// 可优先使用一些其它方法,比如提供一个 CancellationToken 方法来取消线程的执行。
t.Abort();
Console.WriteLine("A thread has been aborted");
检测线程状态
csharp
Console.WriteLine(Thread.CurrentThread.ThreadState.ToString());
ThreadState
Unstarted
未开始时Running
正在执行时WaitSleepJoin
线程 Sleep 时AbortRequested
线程 Abort 时Stopped
正常结束时
线程优先级
csharp
threadOne.Priority = ThreadPriority.Highest;
threadTwo.Priority = ThreadPriority.Lowest;
csharp
Console.WriteLine($"Current thread priority: {Thread.CurrentThread.Priority}");
Console.WriteLine("Running on all cores available");
// 在所有可用的 CPU 核心上启动线程
// 如果拥有一个以上的计算核心,将在两秒内得到初步结果
// 最高优先级的线程通常会计算更多的迭代,但两个值应该很接近
RunThreads();
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("Running on a single core");
// 让操作系统的所有线程运行在单个 CPU 核心(第一核心)上
// 结果会完全不同,计算耗时也会超过 2 秒
// 这是因为 CPU 核心大部分时间在运行高优先级的线程,只留给剩下的线程很少的时间来运行
Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1);
RunThreads();
点击查看执行结果
Current thread priority: Normal
Running on all cores available
ThreadTwo with Lowest priority has a counte = 612,186,937
ThreadOne with Highest priority has a counte = 714,994,619
Running on a single core
ThreadOne with Highest priority has a counte = 6,758,568,718
ThreadTwo with Lowest priority has a counte = 44,751,166
前台线程和后台线程
csharp
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();
// 前台线程终止后,程序结束,并且后台线程被终结
// 进程会等待所有的前台线程完成后再结束工作,但是如果只剩下后台线程,则会直接结束工作。
向线程传递参数
csharp
// 通过类的实例化传递参数
var sample = new ThreadSample(10);
var threadOne = new Thread(sample.CountNumbers);
threadOne.Name = "ThreadOne";
threadOne.Start();
threadOne.Join();
Console.WriteLine("--------------------------------------");
// 通过 Start 方法传参
var threadTwo = new Thread(Count);
threadTwo.Name = "ThreadTwo";
threadTwo.Start(8);
threadTwo.Join();
Console.WriteLine("--------------------------------------");
// 通过 lambda 表达式 传参
var threadThree = new Thread(() => CountNumbers(12));
threadThree.Start();
threadThree.Join();
Console.WriteLine("--------------------------------------");
// 使用lambda表达式引用另一个C#对象的方法被称为闭包。
// 当在lambda表达式中使用任何局部变量时,C#会生成一个类,并将该变量作为该类的一个属性。
// 所以实际上该方法与threadOne中使用的一样,但是我们无需定义该类,C#编译器会自动帮我们实现
// 如果在多个 lambda 表达式中使用相同的变量,它们会共享该变量值。
int i = 10;
var threadFour = new Thread(() => PrintNumber(i));
i = 20;
var threadFive = new Thread(() => PrintNumber(i));
threadFour.Start(); // print 20
threadFive.Start(); // print 20
// 如果在线程启动后再更改 i 的值,则不会影响到已经启动的线程。
使用 C# 中的 lock 关键字
点击查看代码
csharp
/// <summary>
/// 使用C#的lock关键字
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
Console.WriteLine("Incorrect counter");
var c = new Counter();
var t1 = new Thread(() => TestCounter(c));
var t2 = new Thread(() => TestCounter(c));
var t3 = new Thread(() => TestCounter(c));
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
// 多个线程中存在竞争条件(race condition)
// 由于++并不是一个线程安全的操作,所以输出结果一般都不为 0
Console.WriteLine($"Total count: {c.Count}");
Console.WriteLine("----------------------------------------");
Console.WriteLine("Correct counter");
var c1 = new CounterWithLock();
t1 = new Thread(() => TestCounter(c1));
t2 = new Thread(() => TestCounter(c1));
t3 = new Thread(() => TestCounter(c1));
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
Console.WriteLine($"Total count: {c1.Count}");
Console.ReadLine();
}
static void TestCounter(CounterBase c)
{
for (int i = 0; i < 100000; i++)
{
c.Increment();
c.Decrement();
}
}
class Counter : CounterBase
{
public int Count { get; private set; }
public override void Decrement()
{
Count++;
}
public override void Increment()
{
Count--;
}
}
class CounterWithLock : CounterBase
{
// 如果锁定了一个对象,需要访问该对象的所有其他线程则会处于阻塞状态,并等待直到该对象解除锁定。
// 这可能会导致严重的性能问题。
private readonly object _ayncRoot = new object();
public int Count { get; private set; }
public override void Decrement()
{
lock(_ayncRoot)
{
Count++;
}
}
public override void Increment()
{
lock (_ayncRoot)
{
Count--;
}
}
}
abstract class CounterBase
{
public abstract void Increment();
public abstract void Decrement();
}
点击查看输出结果
Incorrect counter
Total count: -2516
----------------------------------------
Correct counter
Total count: 0
使用 Monitor 类锁定资源
点击查看代码
csharp
/// <summary>
/// 使用 Monitor 类锁定资源
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
object lock1 = new object();
object lock2 = new object();
new Thread(() => LockTooMuch(lock1, lock2)).Start();
lock (lock2)
{
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("Monitor.TyrEnter allows not to get stuck, returning false after a specified timeout is elapased");
// Monitor.TyrEnter 方法接收一个超时参数。
// 如果在能够获取被 lock 保护的资源之前,超时时间过期,则该方法会返回 false。
if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(5)))
{
Console.WriteLine("Acquired a protected resource succesfully");
} else
{
Console.WriteLine("Timeout acquiring a resource!");
}
}
new Thread(() => LockTooMuch(lock1, lock2)).Start();
lock (lock2)
{
Console.WriteLine("This will be a deadlock");
Thread.Sleep(TimeSpan.FromSeconds(1));
lock (lock1)
{
Console.WriteLine("Acquired a protected resource succesfully");
}
}
}
static void LockTooMuch(object lock1, object lock2)
{
lock(lock1)
{
Thread.Sleep(TimeSpan.FromSeconds(1));
lock (lock2)
{
}
}
}
点击查看输出结果
Monitor.TyrEnter allows not to get stuck, returning false after a specified timeout is elapased
Timeout acquiring a resource!
This will be a deadlock
处理异常
一般来说不要在线程中抛出异常,而是在线程代码中使用 try/catch 代码块。
点击查看代码
csharp
static void Main(string[] args)
{
var t = new Thread(FaultyThread);
t.Start();
t.Join();
try
{
t = new Thread(BadFaultyThread);
t.Start();
t.Join();
}
catch (Exception ex)
{
Console.WriteLine("We won't get here!");
}
}
static void BadFaultyThread()
{
Console.WriteLine("Starting a faulty thread...");
Thread.Sleep(TimeSpan.FromSeconds(2));
throw new Exception("Boom!");
}
static void FaultyThread()
{
try
{
Console.WriteLine("Starting a faulty thread...");
Thread.Sleep(TimeSpan.FromSeconds(1));
throw new Exception("Boom!");
}
catch (Exception ex)
{
Console.WriteLine($"Exception handled: {ex.Message}");
}
}
点击查看输出结果
线程外的 try/catch 并没有捕获到新线程中的异常,而且直接导致进程崩溃了
Starting a faulty thread...
Exception handled: Boom!
Starting a faulty thread...
未经处理的异常: System.Exception: Boom!
在 Recipe11.Program.BadFaultyThread() 位置 C:\OneDrive\Project\MultiThreading\Recipe11\Program.cs:行号 34
在 System.Threading.ThreadHelper.ThreadStart_Context(Object state)
在 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
在 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
在 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
在 System.Threading.ThreadHelper.ThreadStart()