一、简介
线程不是计算机硬件的功能,而是操作系统提供的逻辑功能。 线程本质上是进程中并发运行的一段代码,因此线程需要操作系统投入CPU资源来运行和调度。
线程,有时称为轻量级进程 (LWP),是程序执行流的最小单元。 一个标准线程由线程ID、当前指令指针(PC)、寄存器组和堆栈组成。
在讲线程锁之前,我们先来了解一下什么是线程同步?
线程同步----在多线程程序中,多个线程可能会抢占一个资源。 这可能会造成冲突,即一个线程可能来不及保存改变的资源,而另一个线程可能还没来得及保存就改变了资源。 开始了。 可能会造成数据不一致。 因此,引入了多线程同步,这意味着多个线程中只有一个线程可以更改共享资源,其他线程无法修改数据。
为了解决多个线程占用一个资源的问题,我们会使用线程锁来解决。
2. 螺纹锁的类型
常用的线程锁分为以下七种:关键字、Lock锁、..原子级操作、Metux、同步事件。 下面我们详细介绍这六种类型:
1. 关键词
没有实现真正的线程同步,操作级别停留在变量级别,而不是原子级别。 对于单系统处理器来说,变量存储在主存中,没有机会被别人修改。但是如果是多处理器的话,可能会出现问题,因为每个处理器都有单独的数据缓存,并且数据更新可能不会立即写回主存,这可能会导致失同步。
2.锁锁
Lock锁将互斥对象添加到操作的代码块中。 如果A线程正在访问,对象没有到达临界区,B线程就不会访问它。 不建议使用Lock(this)来加锁,因为你不确定包含Lock的对象是否已经被重新实例化。
对于Lock锁具有以下建议
如果是类的实例,最好不要lock(this)。 因为使用你的类的人可能不知道你使用了锁。 如果他们创建一个新的实例并锁定这个实例,很容易造成死锁。 如果是这样,请不要锁定(())。 不要锁定字符串。 3....
类是互锁操作,是对某个内存位置的原子操作。 在大多数计算机上,添加变量不是原子操作,需要以下三个步骤:
将实例变量中的值加载到寄存器中 添加或减去寄存器中的值 将添加或减去的值保存到实体变量中
线程执行完前两步后可能会被剥夺CPU时间,而另一个线程继续对同一个变量进行操作,导致第一个线程继续进行加减运算时运算结果不准确。 这是更微观的表现,
该类提供了 4 个原子级变量操作方法。 , , 和。 使用 sum 可确保添加或减去整数是原子操作。 方法自动交换指定变量的值。
该方法结合了两种操作:比较两个值并根据比较结果将第三个值存储在其中一个变量中。 比较和交换操作也是原子执行的。
.(参考a、b、c); 原子操作,比较参数a和参数c,相等则用b替换a,不相等则不替换。
4.
它与Lock方法类似,但可以更好地保护功能块。 您可以使用.Enter 来占用正确的使用权限,并使用.Exit 来释放该权限。 该类还提供了(o,[int])的重载方法,该方法尝试获取o对象的独占权,当获取独占权失败时,会返回false。
Lock可以作为封装的方法,使用起来更简单、更方便。 如果使用后占用资源后报错,如果不使用调用.Exit释放资源,会导致其他线程锁住。 因此,使用Lock更加简单、方便。 它还提供了三个静态方法.Pulse(o)、.(o)和.Wait(o)用于实现唤醒机制的同步。
5. 互斥体
Mutex与其很接近,但它不具备Pulse和Wait的唤醒功能。 它的优点是可以跨进程,并且可以在同一台机器甚至远程机器人上的不同进程之间共享互斥锁。 当然也可以用于线程同步。 但由于是win32封装的,需要互操作转换,消耗资源较多。
6.
如果我们只获取某个资源,并且不经常修改该资源,那么获取权限就会浪费时间。 那么网络提供了一个方法。 当未获取写权限时,可以获取多个读权限。 当已经执行写权限时,无法获取读权限。 写入完成后,继续阅读。
7. 线程同步事件和
相同的:
两个布尔变量都传递给构造函数来控制初始状态。 True 表示非阻塞,False 表示阻塞。
不同的:
虽然它们都可以使用Set()来解除阻塞,但每次执行时都会自动Reset(),并且.()一次只允许一个线程。
需要手动Reset(),这样才能唤醒多个线程。 如果.Set信号被触发,该事件将继续传递,直到手动Reset()。
3. 示例代码测试
下面,我们对以上几种方法中比较常用的方法进行测试和比较。
1.锁锁
Lock主要测试对象的几种定义,分为以下几种情况:
多线程调用Lock的单实例化
Locker LockTest1 = new Locker("LockTest1");
Thread GainMsg1 = new Thread(new ThreadStart(LockTest1.Work));
GainMsg1.Name = "GainMsg1";
Thread GainMsg2 = new Thread(new ThreadStart(LockTest1.Work));
GainMsg2.Name = "GainMsg2";
GainMsg1.Start();
GainMsg2.Start();
全球私人
静止的
= 新的()
= 新的()
非静态
对象=新()
= 新的()
检测结果:
全球私人
静止的
好的
好的
非静态
好的
好的
Lock 的多个实例
Locker LockTest1 = new Locker("LockTest1");
Locker LockTest2 = new Locker("LockTest2");
Thread GainMsg1 = new Thread(new ThreadStart(LockTest1.Work));
GainMsg1.Name = "GainMsg1";
Thread GainMsg2 = new Thread(new ThreadStart(LockTest2.Work));
GainMsg2.Name = "GainMsg2";
GainMsg1.Start();
GainMsg2.Start();
全球私人
静止的
= 新的()
= 新的()
非静态
对象=新()
= 新的()
检测结果:
全球私人
静止的
好的
好的
非静态
不合格
不合格
锁测试总结:在单实例的情况下,因为只有一个对象,所以无论是静态的还是全局的,都可以用单一的方式来访问。 在多实例线程锁的情况下,由于只有一个静态对象,因此不受实例化的影响。 ,能够完成单次访问并实现线程锁
但对于非静态的,由于实例化了两个对象,如果对同一个对象进行单次访问,就可以实现线程锁。 但如果分别访问两个对象,就无法实现线程锁功能。 2. 使用方法
System.Threading.Monitor.Enter(obj); //加锁
System.Threading.Monitor.Exit(obj); //解锁,释放资源
System.Threading.Monitor.Enter(obj);
num = 0;
for (int i = 0; i < 10; i++)
{
num += 1;
msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
Console.WriteLine(msg);
Thread.Sleep(1000);
}
Console.WriteLine("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name);
System.Threading.Monitor.Exit(obj);
3....
:增加+1
:增加-1
:自动交换指定变量的值
:比较两个值,并根据比较结果将第三个值存储到其中一个变量中
3. 互斥锁的使用
线程使用 Mutex.() 方法等待 C# Mutex 对象被释放。 如果它正在等待的 C# Mutex 对象被释放,它会自动拥有该对象,直到它调用 Mutex.() 方法来释放该对象。 在此期间,其他想要获取这个C# Mutex对象的只能等待。
public void Work()
{
mutex.WaitOne();
num = 0;
for (int i = 0; i < 10; i++)
{
num += 1;
msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
Console.WriteLine(msg);
Thread.Sleep(1000);
}
Console.WriteLine("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name);
mutex.ReleaseMutex();
}
4. 使用方法
m_readerWriterLock.AcquireReaderLock(-1);//获取读取的权限,-1代表无限时等待,每次获取完毕后,记得释放.此权限可以被多线程同时获取,也就是“多读”。******如果不是释放,写入的权限将无法被获取**********
m_readerWriterLock.ReleaseReaderLock();//释放读取权限资源
m_readerWriterLock.AcquireWriterLock(-1);//获取写入的权限,-1代表无限时等待,每次写入完毕后,记得释放******如果不是放,其它线程将无法读取**********
public void Read()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("ThreadName " + Thread.CurrentThread.Name + " AcquireReaderLock");
m_readerWriterLock.AcquireReaderLock(-1);
num += 1;
msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
Console.WriteLine(msg);
Thread.Sleep(1000);
m_readerWriterLock.ReleaseReaderLock();
}
Console.WriteLine("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name);
}
public void Writer()
{
Console.WriteLine("ThreadName " + Thread.CurrentThread.Name + " AcquireWriterLock");
m_readerWriterLock.AcquireWriterLock(-1);
num = 1000;
msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
Console.WriteLine(msg);
Thread.Sleep(3000);
Console.WriteLine("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name);
m_readerWriterLock.ReleaseWriterLock();
}
测试效果
从打印的信息就可以看出。 当执行获取写权限时,无法获取读权限,会阻塞读功能。 另一方面,如果不释放读权限,就无法获得写权限。因此,我们一定要养成用完就扔的习惯,避免死锁。
5. 7. 同步事件及使用
声明Auto和时的起始布尔值设置为True,表示信号开启。设置为False则阻止消息
public static EventWaitHandle WaitAutoTest = new AutoResetEvent(false);//线程
public static EventWaitHandle WaitManualTest = new ManualResetEvent(false);// 运动指令的堵塞
测试用方法()和()建立了两个独立的线程。 线程启动后,会被Form1..()阻塞; 操作说明。
点击按钮后,会执行.Set(); 信号将被设置为通过
private void Btn_Auto_Click(object sender, EventArgs e)
{
WaitAutoTest.Set();
}
此时只有一个线程将获得访问权限,并将再次将信号设置为 False。 当该pass的线程运行完毕后,.Set()会再次执行,另一个线程会pass。
public void AutoTest1()
{
Form1.WaitAutoTest.WaitOne();
int num = 0;
for (int i = 0; i < 10; i++)
{
num += 1;
msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
if (dataReceive != null) dataReceive(msg);
//Console.WriteLine(msg);
Thread.Sleep(1000);
}
dataReceive("");
dataReceive(string.Format("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name));
dataReceive("");
Form1.WaitAutoTest.Set();
}
public void AutoTest2()
{
Form1.WaitAutoTest.WaitOne();
int num = 0;
for (int i = 0; i < 10; i++)
{
num += 1;
msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
if (dataReceive != null) dataReceive(msg);
Thread.Sleep(1000);
}
dataReceive("");
dataReceive(string.Format("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name));
dataReceive("");
Form1.WaitAutoTest.Set();
}
按钮测试结果如图
可以看到执行.Set();后只有一个线程获得通过的机会,而另一个线程仍然处于阻塞状态。
测试中还建立了两个独立的线程,方法为()和()。 线程启动后,会被Form1..()阻塞; 操作说明。
private void Btn_Manual_Click(object sender, EventArgs e)
{
WaitManualTest.Set();
}
点击按钮后,会触发.Set(); 通过线程唤醒。唤醒后,除非手动执行Reset(),否则会一直保持在访问状态。
public void ManualTest1()
{
Form1.WaitManualTest.WaitOne();
int num = 0;
for (int i = 0; i < 10; i++)
{
num += 1;
msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
if (dataReceive != null) dataReceive(msg);
//Console.WriteLine(msg);
Thread.Sleep(1000);
}
dataReceive("");
dataReceive(string.Format("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name));
dataReceive("");
Form1.WaitManualTest.Set();
}
public void ManualTest2()
{
Form1.WaitManualTest.WaitOne();
int num = 0;
for (int i = 0; i < 10; i++)
{
num += 1;
msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
if (dataReceive != null) dataReceive(msg);
Thread.Sleep(1000);
}
dataReceive("");
dataReceive(string.Format("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name));
dataReceive("");
Form1.WaitManualTest.Set();
}
按钮测试结果如图
可以看到执行.Set();后一旦,两个线程被解除阻塞并同时开始运行,因此该事件可以同时唤醒多个线程。
测试中使用的代码请参见链接:
链接:该资源包含如下所示的内容。