C#多線程之線程同步
一、前言
我們先來看下面一個例子:
using System;
using System.Threading;
namespace ThreadSynchDemo
{
class Program
{
private static int Counter = 0;
static void Main(string[] args)
{
Thread t1 = new Thread(() => {
for (int i = 0; i < 1000; i++)
{
Counter++;
Thread.Sleep(1);
}
});
t1.Start();
Thread t2 = new Thread(() => {
for (int i = 0; i < 1000; i++)
{
Counter++;
Thread.Sleep(1);
}
});
t2.Start();
Thread.Sleep(3000);
Console.WriteLine(Counter);
Console.ReadKey();
}
}
}我們猜想一下程序的輸出結(jié)果是多少?2000?我們運(yùn)行程序看一下輸出結(jié)果:

我們看到,程序最后輸出的結(jié)果跟我們預(yù)測的完全不一樣,這是什么原因呢?這就是由線程同步引起的問題。
線程同步問題:是解決多個線程同時操作一個資源的問題。
在上面的例子中,t1和t2兩個線程里面都是讓變量Counter的值自增1,假設(shè)這時t1線程讀取到Counter的值為200,可能t2線程執(zhí)行非???,t1線程讀取Counter值的時候,t2線程已經(jīng)把Counter的值改為了205,等t1線程執(zhí)行完畢以后,Counter的值又被變?yōu)榱?01,這樣就會出現(xiàn)線程同步的問題了。那么該如何解決這個問題呢?
二、解決線程同步問題
1、lock
解決線程同步問題最簡單的是使用lock。lock可以解決多個線程同時操作一個資源引起的問題。lock是C#中的關(guān)鍵字,它要鎖定一個資源,lock的特點(diǎn)是:同一時刻只能有一個線程進(jìn)入lock的對象的范圍,其它lock的線程都要等待。我們看下面優(yōu)化后的代碼:
using System;
using System.Threading;
namespace ThreadSynchDemo
{
class Program
{
private static int Counter = 0;
// 定義一個locker對象
private static Object locker = new Object();
static void Main(string[] args)
{
#region 存在線程同步問題
//Thread t1 = new Thread(() => {
// for (int i = 0; i < 1000; i++)
// {
// Counter++;
// Thread.Sleep(1);
// }
//});
//t1.Start();
//Thread t2 = new Thread(() => {
// for (int i = 0; i < 1000; i++)
// {
// Counter++;
// Thread.Sleep(1);
// }
//});
//t2.Start();
#endregion
#region 使用Lock解決線程同步問題
Thread t1 = new Thread(() => {
for (int i = 0; i < 1000; i++)
{
lock(locker)
{
Counter++;
}
Thread.Sleep(1);
}
});
t1.Start();
Thread t2 = new Thread(() => {
for (int i = 0; i < 1000; i++)
{
lock (locker)
{
Counter++;
}
Thread.Sleep(1);
}
});
t2.Start();
#endregion
Thread.Sleep(3000);
Console.WriteLine(Counter);
Console.ReadKey();
}
}
}這時我們在運(yùn)行程序,查看輸出結(jié)果:

這時輸出結(jié)果是正確的。
注意:lock只能鎖住同一個對象,如果是不同的對象,還是會有線程同步的問題。lock鎖定的對象必須是引用類型的對象。
我們在定義一個Object類型的對象,lock分別鎖住兩個對象,看看是什么結(jié)果:
using System;
using System.Threading;
namespace ThreadSynchDemo
{
class Program
{
private static int Counter = 0;
// 定義一個locker對象
private static Object locker = new Object();
// 定義locker2
private static Object locker2 = new Object();
static void Main(string[] args)
{
#region 存在線程同步問題
//Thread t1 = new Thread(() => {
// for (int i = 0; i < 1000; i++)
// {
// Counter++;
// Thread.Sleep(1);
// }
//});
//t1.Start();
//Thread t2 = new Thread(() => {
// for (int i = 0; i < 1000; i++)
// {
// Counter++;
// Thread.Sleep(1);
// }
//});
//t2.Start();
#endregion
#region 使用Lock解決線程同步問題
//Thread t1 = new Thread(() => {
// for (int i = 0; i < 1000; i++)
// {
// lock(locker)
// {
// Counter++;
// }
// Thread.Sleep(1);
// }
//});
//t1.Start();
//Thread t2 = new Thread(() => {
// for (int i = 0; i < 1000; i++)
// {
// lock (locker)
// {
// Counter++;
// }
// Thread.Sleep(1);
// }
//});
//t2.Start();
#endregion
#region 使用lock鎖住不同的對象也會有線程同步問題
Thread t1 = new Thread(() => {
for (int i = 0; i < 1000; i++)
{
lock (locker)
{
Counter++;
}
Thread.Sleep(1);
}
});
t1.Start();
Thread t2 = new Thread(() => {
for (int i = 0; i < 1000; i++)
{
lock (locker2)
{
Counter++;
}
Thread.Sleep(1);
}
});
t2.Start();
#endregion
Thread.Sleep(3000);
Console.WriteLine(Counter);
Console.ReadKey();
}
}
}程序運(yùn)行結(jié)果:

可以看到,這時還是會有線程同步的問題。雖然使用了lock,但是我們鎖住的是不同的對象,這樣也會有線程同步問題。lock必須鎖住同一個對象才可以。
我們下面在來看一個多線程同步問題的例子:
using System;
using System.Threading;
namespace ThreadSynchDemo2
{
class Program
{
static int Money = 100;
/// <summary>
/// 定義一個取錢的方法
/// </summary>
/// <param name="name"></param>
static void QuQian(string name)
{
Console.WriteLine(name + "查看一下余額" + Money);
int yue = Money - 1;
Console.WriteLine(name + "取錢");
Money = yue;
Console.WriteLine(name + "取完了,剩" + Money);
}
static void Main(string[] args)
{
Thread t1 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
QuQian("t2");
}
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
QuQian("t2");
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("余額" + Money);
Console.ReadKey();
}
}
}我們看一下輸出結(jié)果:

可以看到,最終的余額并不是80,這也是線程同步帶來的問題,如何解決。解決思路就是使用同步的技術(shù)避免兩個線程同時修改一個余額。
2、最大粒度——同步方法
在方法上面使用[MethodImpl(MethodImplOptions.Synchronized)],標(biāo)記該方法是同步方法,這樣一個方法只能同時被一個線程訪問。我們在QuQian的方法上面標(biāo)記,修改后的代碼如下:
using System;
using System.Runtime.CompilerServices;
using System.Threading;
namespace ThreadSynchDemo2
{
class Program
{
static int Money = 100;
/// <summary>
/// 定義一個取錢的方法,在上面標(biāo)記為同步方法
/// </summary>
/// <param name="name"></param>
[MethodImpl(MethodImplOptions.Synchronized)]
static void QuQian(string name)
{
Console.WriteLine(name + "查看一下余額" + Money);
int yue = Money - 1;
Console.WriteLine(name + "取錢");
Money = yue;
Console.WriteLine(name + "取完了,剩" + Money);
}
static void Main(string[] args)
{
Thread t1 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
QuQian("t2");
}
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
QuQian("t2");
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("余額" + Money);
Console.ReadKey();
}
}
}程序輸出結(jié)果:

現(xiàn)在的方法就是“線程安全”的了。什么是“線程安全”呢?“線程安全”是指方法可以被多個線程隨意調(diào)用,而不會出現(xiàn)混亂。如果出現(xiàn)了混亂,那么就是“線程不安全”的。“線程安全”的方法可以在多線程里面隨意的使用。
3、對象互斥鎖
對象互斥鎖就是我們上面講的lock。我們在用lock來修改上面QuQian的例子:
using System;
using System.Runtime.CompilerServices;
using System.Threading;
namespace ThreadSynchDemo2
{
class Program
{
static int Money = 100;
/// <summary>
/// 定義一個取錢的方法,在上面標(biāo)記為同步方法
/// </summary>
/// <param name="name"></param>
//[MethodImpl(MethodImplOptions.Synchronized)]
//static void QuQian(string name)
//{
// Console.WriteLine(name + "查看一下余額" + Money);
// int yue = Money - 1;
// Console.WriteLine(name + "取錢");
// Money = yue;
// Console.WriteLine(name + "取完了,剩" + Money);
//}
private static object locker = new object();
static void QuQian(string name)
{
Console.WriteLine(name + "查看一下余額" + Money);
int yue = Money - 1;
Console.WriteLine(name + "取錢");
Money = yue;
Console.WriteLine(name + "取完了,剩" + Money);
}
static void Main(string[] args)
{
Thread t1 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
// 使用對象互斥鎖
lock(locker)
{
QuQian("t1");
}
}
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
lock (locker)
{
QuQian("t2");
}
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("余額" + Money);
Console.ReadKey();
}
}
}程序輸出結(jié)果:

可以看到,最終的輸出結(jié)果還是80。
同一時刻只能有一個線程進(jìn)入同一個對象的lock代碼塊。必須是同一個對象才能起到互斥的作用。lock后必須是引用類型,不一定是object,只要是對象就行。
鎖對象選擇很重要,選不對就起不到同步的作用;選不對還有可能會造成其他地方被鎖,比如用字符串做鎖(因?yàn)樽址彌_池導(dǎo)致導(dǎo)致可能用的是其他地方正在使用的鎖),所以不建議使用字符串做鎖。下面的代碼就是不允許的:
lock("locker")兩個方法如果都用一個對象做鎖,那么訪問A的時候就不能訪問B,因此鎖選擇很重要。
4、Monitor
其實(shí)lock關(guān)鍵字就是對Monitor的簡化調(diào)用,lock最終會被編譯成Monitor,因此一般不直接使用Monitor類,看下面代碼:
using System;
using System.Threading;
namespace MonitorDemo
{
class Program
{
static int Money = 100;
private static object locker = new object();
static void QuQian(string name)
{
// 等待沒有人鎖定locker對象,就鎖定它,然后繼續(xù)執(zhí)行
Monitor.Enter(locker);
try
{
Console.WriteLine(name + "查看一下余額" + Money);
int yue = Money - 1;
Console.WriteLine(name + "取錢");
Money = yue;
Console.WriteLine(name + "取完了,剩" + Money);
}
finally
{
// 釋放locker對象的鎖
Monitor.Exit(locker);
}
}
static void Main(string[] args)
{
Thread t1 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
QuQian("t1");
}
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
QuQian("t2");
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("余額" + Money);
Console.ReadKey();
}
}
}程序輸出結(jié)果:

Monitor類里面還有TryEnter方法,如果Enter的時候有人在占用鎖,它不會等待,而是會返回false??聪旅娴氖纠a:
using System;
using System.Threading;
namespace MonitorDemo
{
class Program
{
static int Money = 100;
private static object locker = new object();
static void QuQian(string name)
{
// 等待沒有人鎖定locker對象,就鎖定它,然后繼續(xù)執(zhí)行
Monitor.Enter(locker);
try
{
Console.WriteLine(name + "查看一下余額" + Money);
int yue = Money - 1;
Console.WriteLine(name + "取錢");
Money = yue;
Console.WriteLine(name + "取完了,剩" + Money);
}
finally
{
// 釋放locker對象的鎖
Monitor.Exit(locker);
}
}
static void F1(int i)
{
if (!Monitor.TryEnter(locker))
{
Console.WriteLine("有人在鎖著呢");
return;
}
Console.WriteLine(i);
Monitor.Exit(locker);
}
static void Main(string[] args)
{
//Thread t1 = new Thread(() => {
// for (int i = 0; i < 10; i++)
// {
// QuQian("t1");
// }
//});
//Thread t2 = new Thread(() => {
// for (int i = 0; i < 10; i++)
// {
// QuQian("t2");
// }
//});
Thread t1 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
F1(i);
}
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
F1(i);
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("余額" + Money);
Console.ReadKey();
}
}
}程序輸出結(jié)果:

到此這篇關(guān)于C#多線程之線程同步的文章就介紹到這了。希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
詳解如何利用C#實(shí)現(xiàn)漢字轉(zhuǎn)拼音功能
這篇文章主要為大家詳細(xì)介紹了如何利用C#實(shí)現(xiàn)漢字轉(zhuǎn)拼音的功能,文中的示例代碼講解詳細(xì),對我們學(xué)習(xí)C#有一定的幫助,感興趣的小伙伴可以跟隨小編一起了解一下2022-12-12
C#利用Windows自帶gdi32.dll實(shí)現(xiàn)抓取屏幕功能實(shí)例
這篇文章主要介紹了C#利用Windows自帶gdi32.dll實(shí)現(xiàn)抓取屏幕功能,是C#程序設(shè)計(jì)中常見的一個重要技巧,需要的朋友可以參考下2014-08-08
C#通過正則表達(dá)式實(shí)現(xiàn)提取網(wǎng)頁中的圖片
本文給大家分享的是使用C#通過正則表達(dá)式來實(shí)現(xiàn)提取網(wǎng)頁中的圖片的代碼,十分的方便,有需要的小伙伴可以參考下。2015-12-12

