23C#线程基础知识2
线程安全
本地状态(Local)和共享状态(Shared)
Local本地独立:
CLR为每个线程分配自己的内存栈,以便使本地变量保持独立。
static void Main(string[] args)
{
new Thread(Go).Start();//在分线程上 调用Go()
Go();//在main线程上 调用Go()
Console.ReadKey();
}
public static void Go()
{
//cycles是本地变量,在每个线程的内存栈上,都会创建cycles独立的副本。(线程之间不共享数据)
for (int cycles = 0; cycles < 5; cycles++)
{
Console.WriteLine("?");
}
}
Shared共享:
如果多个线程都引用到同一个对象实例,那么它们就共享了数据。
被lambda表达式或匿名委托所捕获的本地变量,会被编译器转化为字段,所以也会被共享。
静态字段也会在线程间共享。
下面的_done字段就是共享变量
class Program
{
private static bool _done;
static void Main(string[] args)
{
new Thread(Go).Start();
Go();
}
static void Go()
{
if (!_done)
{
_done = true;
Console.WriteLine("Done");
}
}
}
字段共享就会引出线程安全问题,因为上述例子的输出极有可能不是固定的,如果在_done = true;前加上Thread.Sleep()结果就会不一样。这样两个线程都同时进入了里面的判断了,因为一个线程还没有来得及修改_done的值,而另一个线程也拿到_done的值进入了该判断了。主线程和分线程是同时运行的。
class Program
{
private static bool _done;
static void Main(string[] args)
{
new Thread(Go).Start();
Go();
}
static void Go()
{
if (!_done)
{
Thread.Sleep(100);
_done = true;
Console.WriteLine("Done");
}
}
}
上例子就引出了线程安全这个关键概念(或者说缺乏线程安全)
上述例子的输出实际上是无法确定的:有可能(理论上)“Done”会被打印两次。如果交换Go方法里语句的顺序,那么“Done”被打印两次的几率会大大增加。因为一个线程可能正在评估 if,而另外一个线程在执行Thread.Sleep(100);
语句,它还没来得及把done设置为true。所以,尽可能避免使用共享状态。
锁定与线程安全简介
Locking & Thread Safety
在读取和写入共享数据的时候,通过使用一个互斥锁( exclusive lock ),就可以修复前面例子的问题。
C#里使用lock语句来加锁
当两个线程同时竞争一个锁的时候(锁可以基于任何引用类型对象),一个线程会等待或阻塞,直到锁变成可用状态。
如:
class Program
{
private static bool _done;
static readonly object _locker = new object();
static void Main(string[] args)
{
Thread t = new Thread(Go);
t.Start();//分线程调用
Go();//main线程调用
Console.ReadKey();
}
static void Go()
{
lock (_locker)
{
if (!_done)
{
Thread.Sleep(100);
_done = true;
Console.WriteLine("Done");
}
}
}
}
在这里使用lock将共享代码块包括,这个lock代码块就称为临界区,每次只允许一个线程进入,在多线程上下文中,以这种方式避免不确定性的代码就叫做线程安全。
互斥锁Lock不是线程安全的最好方法,很容易忘记对字段加锁,Lock也会引起一些问题(死锁)。
线程传参
①以直接使用lambda表达式做为Thread的参数
②可以使用Thread的Start方法来传递任务的参数,因为传参委托的类型是ThreadStart,所以参数必须是object类型。
class Program
{
static void Main(string[] args)
{
new Thread(G).Start("Hello World");
}
static void G(object str)
{
Console.WriteLine((string)str);
}
}
③使用lambda表达式可以很简单地给Thread传递参数。但是线程开始后,可能会不小心修改了被捕获地变量,比如下面的例子,每一次运行都有不同的输出,而且输出有可能会有相同的值。也就是lambda默认使用引用捕获,很有可能是线程运行时(非构造时,new Thread是构造但不一定立即运行)for循环已经将i递增到10了。i被lambda表达式捕获并被编译成字段,被所有线程共享。所以会有重复值。
for (int i = 0; i < 10; i++)
{
new Thread(() => Console.Write(i)).Start();
}
可以使用局部变量来解决这个问题,这个变量每次循环都会创建一个新的临时变量。也就是会创建10个内存地址。
for (int i = 0; i < 10; i++)
{
int temp = i;
new Thread(() => Console.Write(temp)).Start();
}
线程的执行是无序的,因为由操作系统的调度决定cpu运行哪条线程,所以打印也是无序的,但是绝对没有重复的值。
异常处理
创建线程时在作用范围内的 trylcatch/finally 块,在线程开始执行后就与线程无关了。
创建线程代码处的trycatch和线程里面的方法的异常捕获没关系。
static void Main(string[] args)
{
try
{
new Thread(Go).Start(); ;
}
catch (Exception ex)
{
Console.WriteLine("Exception!");
}
Console.ReadKey();
}
static void Go()
{
throw null;
}
解决方法,在线程里面捕获
static void Main(string[] args)
{
new Thread(Go).Start();
Console.ReadKey();
}
static void Go()
{
try
{
throw null;
}
catch (Exception ex)
{
Console.WriteLine("Exception!");
}
}
在WPF、WinForm里,可以订阅全局异常处理事件:
- Application.DispatcherUnhandledException
- Application.ThreadException
在通过消息循环调用的程序的任何部分发生未处理的异常(这相当于应用程序处于活动状态时在主线程上运行的所有代码)后,将触发这些异常。但是非U线程上的未处理异常,并不会触发它。
而任何线程有任何未处理的异常都会触发
AppDomain.CurrentDomain.UnhandledException
线程的优先级
线程的优先级(Priority属性)决定了相对于操作系统中其他活跃线程所占的执行时间。
如果想让某线程的优先级比其他进程中的线程高,那就必须提升进程的优先级,这里可以使用Process类
using (Process p = Process.GetCurrentProcess())
{
p.PriorityClass = ProcessPriorityClass.High;
}
但是,提高线程或进程的优先级可能会导致其他线程或线程处于饥饿状态,不能随便设置。
信号
有时,你需要让某个线程一直处于等待状态,直到接收到其他线程发来的通知,才解除等待状态,这就叫做signaling。最简单的信号结构是MaunalResetEvent类对象。
调用MaunalResetEvent类对象的WaitOne方法就会阻塞当前的线程,直到另一个线程通过调用Set方法来打开信号(发送信号)。
internal class Program
{
private static void Main(string[] args)
{
var signal = new ManualResetEvent(false);
var thread = new Thread(() =>
{
Console.WriteLine("Waiting for signal...");
//等待主线程发送的信号
signal.WaitOne();
signal.Dispose();
Console.WriteLine("Got signal!");
});
thread.Start();
Thread.Sleep(3000);
//向thread打开信号
signal.Set();
}
}
信号打开后可以通过Reset方法来重新关闭信号。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署