C#常用的4种锁的使用以及解释
1、lock
internal class Program
{
static object lockObject=new object();
static void Main(string[] args)
{
Student student = new Student();
Thread thread1 = new Thread(new ThreadStart(Start1));
thread1.Start();
Thread thread2 = new Thread(Start2);
thread2.Start(student);
}
private static void Start2(object? obj)
{
var student = obj as Student;
lock (lockObject)
{
for (int i = 0; i < 30; i++)
{
Console.WriteLine(student!.Name + i);
}
}
}
private static void Start1()
{
lock (lockObject)
{
for (int i = 0; i < 30; i++)
{
Console.WriteLine(i);
}
}
}
}
lock锁用于同步线程对共享资源的访问,这里的共享资源是静态的Console.WriteLine,当多个线程共同使用它的时候,lock内部会有一个监听器,监听它有没有在其他地方使用,有的话,我这里先暂停,它的底层是一个互斥锁,是一个非的概念。
2、Monitor
internal class Program
{
static object lockObject = new object();
static void Main(string[] args)
{
Student student = new Student();
Thread thread1 = new Thread(new ThreadStart(Start1));
thread1.Start();
Thread thread2 = new Thread(Start2);
thread2.Start(student);
}
private static void Start2(object? obj)
{
var student = obj as Student;
Monitor.Enter(lockObject); //在指定对象获取排他锁
try
{
for (int i = 0; i < 30; i++)
{
Console.WriteLine("张三"+i);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message.ToString());
}
finally
{
Monitor.Exit(lockObject); //释放锁
}
}
private static void Start1()
{
for (int i = 0; i < 30; i++)
{
Console.WriteLine(i);
}
}
}
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public Student()
{
Name = "张三";
}
}
Monitor的底层实现同样依赖于操作系统的同步机制。在.NET中,Monitor是基于Windows操作系统的Win32 API中的WaitForSingleObject和ReleaseMutex函数实现的,或者在其他平台上使用相应的同步机制。
当调用Monitor.Enter时,如果对象的锁没有被其他线程持有,当前线程将获得锁并继续执行。如果锁已经被其他线程持有,当前线程将等待直到锁被释放。Monitor.Exit用于释放当前线程持有的锁,允许其他等待的线程尝试获取锁。
使用Monitor时,通常需要与try...finally结构一起使用,以确保即使在发生异常的情况下锁也能被正确释放。这是Monitor与lock语句的一个主要区别,因为lock语句会自动处理异常情况并释放锁。
3、Semaphore
internal class Program
{
static Semaphore _semaphore;
static int currentCount; //当前线程信号量
static RandomNumberGenerator randomNumber;
static Random r;
static void Main(string[] args)
{
r = new Random();
randomNumber = RandomNumberGenerator.Create();
byte[] buffer = new byte[1024];
randomNumber.GetBytes(buffer);
_semaphore =new Semaphore(1,3);
for (int i = 0; i < 5; i++)
{
ThreadPool.QueueUserWorkItem(Done,i+1);
}
Console.ReadKey(true);
}
private static void Done(object? state)
{
int id = (int)state;
PrintThreadStatus(id, "等待");
_semaphore.WaitOne();
PrintThreadStatus(id, "进入");
PrintCount(1);
Thread.Sleep(r.Next(1000));
PrintThreadStatus(id, "退出");
PrintCount(-1);
_semaphore.Release();
}
//输出线程状态
static void PrintThreadStatus(int id,string thread)
{
Console.WriteLine($"线程{id}:{thread}");
}
//修改并输出线程数量
static void PrintCount(int add)
{
Interlocked.Add(ref currentCount, add);
Console.WriteLine($"=> 信号量{Interlocked.Exchange(ref currentCount,currentCount)}");
}
}
锁住的对象:
在这个代码示例中,Semaphore并不是直接“锁住”某个对象,而是作为一种同步机制,用来控制同时访问某个资源(在这种情况下是临界区代码)的线程数量。Semaphore有两个重要的参数:当前信号量计数(currentCount)和最大信号量计数(maxCount)。在这个例子中,_semaphore被初始化为new Semaphore(1, 3),这意味着初始时只有一个信号量可用,但最多可以有三个线程同时持有信号量。
当一个线程调用_semaphore.WaitOne();时,如果信号量计数大于0,它将减少计数并继续执行。如果计数为0,则线程将被阻塞,直到其他线程调用_semaphore.Release();来增加信号量计数。这确保了在任何时刻,只有指定数量的线程能够进入受保护的代码区域。
使用锁(在这个例子中是Semaphore)可以使多线程按顺序执行下去,因为Semaphore限制了同时执行临界区代码的线程数量。当一个线程进入临界区并执行完毕后,它通过调用_semaphore.Release();来释放信号量,允许其他等待的线程进入。
锁的底层实现:
Semaphore的底层实现依赖于操作系统的同步机制。在.NET中,Semaphore是基于Windows API中的同步对象,如CreateSemaphore和ReleaseSemaphore,来实现的。这些API提供了一种机制,用于跨多个线程或进程同步对共享资源的访问。
当调用WaitOne方法时,如果信号量计数大于0,它会减少计数并允许线程继续执行。如果计数为0,线程将等待,直到其他线程调用Release方法来增加信号量计数。这个过程涉及到操作系统内核的调度和同步原语,如互斥锁(mutexes)、信号量(semaphores)或事件(events)。
Semaphore的实现细节可能会因操作系统而异,但其核心概念是一致的:控制对共享资源的访问,以避免竞争条件和确保数据一致性。
Semaphore用于控制对临界区的访问,确保最多只有三个线程可以同时执行Done方法中的代码。通过这种方式,Semaphore帮助维护了线程间的同步,并防止了潜在的竞态条件。
4、Mutext既然是跨进程基元同步