线程的同步
概念
线程同步是指控制多个线程的相对执行顺序,避免在使用共享资源时可能出现的问题。
线程同步可用的方法
- 轮询(不推荐):通过反复检查Thread类
IsAlive
属性判断调用状态。 Thread.Join()
:将一个线程加入到本线程中,本线程的执行会等待另一线程执行完毕。适合管理少量线程,不适用于复杂情况。lock
语句(Monitor类)
Monitor
概述
Monitor类主要用于防止多个线程同时操作一个对象产生冲突。 (无法实例化)
主要功能:
- 可以从任何上下文调用
- 加锁:当第一个线程访问加锁资源时,此资源会被锁住,其他想要访问此资源的线程进入锁等待状态,直到第一个线程结束访问。
- 解锁
涉及操作有:lock
语句/Monitor.Enter()
/Monitor.Exit()
/Monitor.Wait()
/Monitor.Pulse()
/Monitor.PulseAll()
对于每个同步对象来维护以下信息:
- 对当前拥有锁的线程的引用。
- 对就绪的队列,其中包含就可以获得的锁的线程的引用。
- 对等待队列,其中包含有关的锁定的对象的状态更改通知等待的线程的引用。
Monitor
将锁定对象(即引用类型)而非值类型。 在您将值类型传递给Enter
和Exit
时,它会针对每个调用分别装箱。 由于每个调用都创建一个单独的对象,所以Enter
从不拦截,并且其旨在保护的代码并未真正同步。 此外,传递给Exit
的对象不同于传递给Enter
的对象,所以Monitor
将引发SynchronizationLockException
,并显示消息“从不同步的代码块中调用了对象同步方法”。
lock
语句 /Monitor.Enter()
/Monitor.Exit()
语法:
lock(对象或表达式)//引用类型,可用this,静态类使用typeof()
{
...
}
当程序进入lock
区域时,首先获取指定对象的互斥锁(类似Threading
命名空间下的Mutex
的作用),此语句执行期间给指定对象上锁,语句执行完成后进行解锁。
等同于:
System.Threading.Monitor.Enter(对象或表达式);
try{
...
}
finally{
System.Threading.Monitor.Exit(对象或表达式);
}
以下是一个实例,每个进程分别让x,y自增并打印自增后的值。如果不加锁的话x,y的值可能不同。
class Program
{
static int x = 0, y = 0;
public static void Main(string[] args)
{
Thread thread1 = new Thread(TestEqual);
Thread thread2 = new Thread(TestEqual);
thread1.Start();
thread2.Start();
}
static void TestEqual()
{
for (int i = 0; i < 5; i++)
{
//lock (typeof(Program))
{
x++;
Thread.Sleep(new Random().Next(10));
y++;
Console.WriteLine($"x={x},y={y}");
}
}
}
}
//运行结果:
//x=2,y=1
//x=2,y=2
//x=4,y=4
//x=4,y=4
//x=6,y=6
//x=6,y=6
//x=8,y=8
//x=8,y=8
//x=10,y=10
//x=10,y=9
此外:在锁内也可被Thread.Interrupt()
中断,将会引发ThreadInterruptedException
异常,在另一进程中使用中断进程的Join()
方法恢复运行。
Monitor.Wait()
/Monitor.Pulse()
/Monitor.PulseAll()
Monitor.Wait()
:释放对象上的锁并阻止当前线程,直到它重新获取该锁。Monitor.Pulse()
/Monitor.PulseAll()
:当前拥有指定对象的锁的线程调用此方法,以向第一个线程发出锁。 接收到脉冲后,等待线程会移动到就绪队列。 如果调用的线程Pulse
释放该锁,则就绪队列(不一定是发出脉冲的线程)中的下一个线程将获取该锁。如果调用的线程PulseAll
释放该锁,则就绪队列中的所有线程将依次获取该锁。
注意:
- 必须从同步的代码块内调用
Pulse
、PulseAll
和Wait
方法。 - 若要向多个线程发出信号,请使用
PulseAll
方法。
实例:
class Program
{
public static void Main(string[] args)
{
Thread thread1 = new Thread(Test);
thread1.Name = "thread1";
Thread thread2 = new Thread(Test);
thread2.Name = "thread2";
Thread pulseThread = new Thread(Pulse);
thread1.Start();
thread2.Start();
pulseThread.Start();
}
private static void Pulse()
{
lock (typeof(Program))
{
Thread.Sleep(1500);
x++;
Monitor.Pulse(typeof(Program));//唤醒先进入就绪队列的进程
//Monitor.PulseAll(typeof(Program)); //唤醒全部就绪队列的进程
}
}
static void Test()
{
lock (typeof(Program))
{
Monitor.Wait(typeof(Program));
Thread.Sleep(400);
Console.WriteLine($"Thread name:{Thread.CurrentThread.Name}");
}
}
}
Monitor.TryEnter()
/Monitor.IsEntered()
- 使用
Monitor.IsEntered()
确定当前线程是否保留指定对象上的锁。 Monitor.TryEnter()
类似于Enter
但它永远不会阻止当前线程。如果该线程不能输入而不会阻塞,则该方法将返回false
,和线程不进入关键节。