前一篇文章记录了简单的多线程编程的几种方式,但是在实际的项目中,也需要等待多线程执行完成之后再执行的方法,这个就叫做多线程的同步,或者,由于多个线程对同一对象的同时操作造成数据错乱,需要线程安全。这篇文章主要记录多线程的同步异步如何实现线程安全的几种方式的笔记,如有错误,请大神不吝赐教。
因为代码里面有很详细的注释,所以下面直接附上代码,不做过多的解释,如有疑问可以百度相关主题的文章详细了解。
1、 Mutex
////1.Mutex测试
////Mutex互斥锁,用于多线程间的线程同步通过WaitOne等待当前锁定的线程执行完成,例如,线程B执行需要等待线程A执行结束的情况下,可以使用Mutex
////同时Mutex还有一个比较有趣的功能就是可以设置实现客户端在同一太电脑上只能打开一个进程
//bool createNew = false;
//Mutex mutex = new Mutex(true, "MutexTest", out createNew);
//AutoResetEvent ae = new AutoResetEvent(false);//定义一个信号量,表示执行结束,可以释放互斥锁
////参数1表示初始化的时候当前互斥锁是否已被获取,false代表未被获取,
////参数2标识当前互斥锁的名称,指定一个名称,配合参数3即可实现只能开启一个进程的效果
////参数3表示是否创建了一个新的互斥锁
//Thread t1 = new Thread(new ThreadStart(() =>
//{
// Console.WriteLine("我是线程1");
// Console.WriteLine("线程1开始执行!");
// Thread.Sleep(1000);//线程休眠1秒钟,用于模拟需要较长时间执行的功能
// Console.WriteLine("线程1执行结束!");
// ae.Set();
//}));
//Thread t2 = new Thread(() =>
// {
// Console.WriteLine("我是线程2");
// Console.WriteLine("线程2开始执行!");
// mutex.WaitOne();//等待互斥锁被释放,模拟实际项目中需要其他线程执行完毕方可执行的功能
// Console.WriteLine("线程2执行结束!");
// });
////因为是多线程执行,所以线程1与线程2的谁先开始执行,以上代码中未进行控制,
////但线程2一定是在线程1执行完成之后才能结束
//t1.Start();
//t2.Start();
//ae.WaitOne();//等待释放信息
//mutex.ReleaseMutex();//释放互斥锁
////AutoResetEvent的功能类似于一个红绿灯信号,当达到可以释放的条件的时候,调用Set方法来通知后续代码可以执行了,
////此处为何需要一个信号,是因为Mutex定义在主线程中,如果在异步线程中释放,会报一个错,提示在不安全的代码块中执行
////互斥锁,所以此处使用信号来通知主线程可以释放互斥锁了
2、AutoResetEvent
/// <summary>
/// 通过AutoRestEvent实现线程同步
/// </summary>
public void TestAutoResetEvent()
{
AutoResetEvent[] autoResetEvents = new AutoResetEvent[3];
autoResetEvents[0] = new AutoResetEvent(false);//定义初始信号为关
autoResetEvents[1] = new AutoResetEvent(false);
autoResetEvents[2] = new AutoResetEvent(false);
//以下代码实现线程1结束之后线程2才能结束,线程2结束之后线程3才能开始,所有线程都结束之后主线程才能继续
Thread t1 = new Thread(new ThreadStart(() =>
{
Console.WriteLine("线程1开始!");
Thread.Sleep(1000);
Console.WriteLine("线程1结束!");
autoResetEvents[0].Set();
}));
Thread t2 = new Thread(new ThreadStart(() =>
{
Console.WriteLine("线程2开始!");
Thread.Sleep(1000);
autoResetEvents[0].WaitOne();
Console.WriteLine("线程2结束!");
autoResetEvents[1].Set();
}));
Thread t3 = new Thread(new ThreadStart(() =>
{
autoResetEvents[1].WaitOne();
Console.WriteLine("线程3开始!");
Thread.Sleep(1000);
Console.WriteLine("线程3结束!");
autoResetEvents[2].Set();
}));
t1.Start();
t2.Start();
t3.Start();
Console.WriteLine("主线程开始等待......");
autoResetEvents[2].WaitOne();//等待所有线程结束
//AutoResetEvent从字面即可知道是自动信号,意思为当信号被捕捉之后会自动重置为关闭状态
//对应的ManualResetEvent为手动信号,使用方法相同但是在被捕捉之后不会被重置为关闭状态
//需要手动调用Reset方法关闭信号,如果是简单的同步,使用自动信号即可,如果需要很复杂的流程控制
//可以使用自动信号,同时可以配合WaitHandle来实现线程的同步,WaitHandle拥有WaitAny方法等待任意一个信号
//WaitAll方法等待所有信号,使用方法与信号的WaiOne相似,此处不再进行举例,可以查看相关文章具体了解
Console.WriteLine("主线程执行结束!");
}
3、 lock与Monitor
/// <summary>
/// 测试lock和Monitor实现线程安全的多线程
/// </summary>
public void TestLockAndMonitor()
{
//lock与monitor实现相同的功能,多线程的线程安全
//lock实际上就是Monitor.Enter与Monitor.Exit的语法糖
object obj = new object();//创建一个应用类型用于lock
int count = 1;
int sum = 0;
for (int i = 0; i < 20; i++)
{
Thread t = new Thread(new ThreadStart(() =>
{
for (int j = 0; j < 1000; j++)
{
//此处保证线程安全的原理是,当前多个线程同时访问count的时候,如果不lock
//可能多个线程访问到的count是相同的值,这样虽然多个线程都执行了count++但是
//结果却没有加上去,造成最终的结果错误,当lock之后,lock内部的代码每次只能
//有一个线程访问,所以每个线程获取的count都不可能相同,这样就能保证最后的结果一定是正确的
//lock (obj)//取消此句代码测试多线程的不安全性,取消之后可能每次执行的结果都不一样
//{
// sum += count;
// count++;
//}
//使用下面的方法与使用lock的功能相同
//Monitor.Enter(obj);
//sum += count;
//count++;
//Monitor.Exit(obj);
}
}));
t.Start();
}
Thread.Sleep(3000);//延时3秒保证异步线程全部执行完成
Console.WriteLine(sum);
}
4、信号量
/// <summary>
/// 测试信号量实现线程安全
/// </summary>
public void TestSemaphore()
{
//Semaphore 类似于线程池,用于设置同时可以有多少个线程执行
//当线程超过信号量运行的最大值之后,后续的线程就需要等待
Semaphore semaphore = new Semaphore(2, 2);//用于设置最大可以有两个线程同时执行,初始时有两个位置空闲
for (int i = 0; i <= 20; i++)
{
Thread t = new Thread(new ThreadStart(() =>
{
semaphore.WaitOne();//等待信号释放,若未超过信号的最大数值,则不需等待
Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString() + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Random random = new Random();
Thread.Sleep(random.Next(1,5) * 1000);//随机休眠1-5秒
semaphore.Release();//释放当前信号
}));
t.Start();
}
}
5、 自旋锁
/// <summary>
/// 测试自旋锁
/// </summary>
public void TestSpinLocked()
{
//自旋锁与lock实现的功能相同,但是lock锁住对象开销比较大
//相反自旋锁开销比较小,效率相对也比lock高,当锁住的次数比较多,同时锁的时间比较短的时候,可是使用自旋锁
int count = 1;
int sum = 0;
SpinLock spinLock = new SpinLock();
for (int i = 0; i < 20; i++)
{
Thread t = new Thread(new ThreadStart(() =>
{
bool lockTaken = false;
//使用下面的方法与使用lock的功能相同
//申请获取锁
spinLock.Enter(ref lockTaken);
for (int j = 0; j < 1000; j++)
{
sum += count;
count++;
}
if (lockTaken) //判断当前线程是否锁住,如果锁住则释放它,防止出现死锁的情况
{
spinLock.Exit();
}
}));
t.Start();
}
Thread.Sleep(3000);//延时3秒保证异步线程全部执行完成
Console.WriteLine(sum);
}
6、 原子操作
/// <summary>
/// 测试InterLocked
/// </summary>
public void TestInterLocked()
{
//InterLocked拥有几个方法来保证线程安全,每个操作都是原子级的,所以效率高,线程安全
//此方法使用InterLocked实现类似于自旋锁的功能
//关于InterLocked的更多用法请参考MSDN
double current = 0;
for (int i = 0; i < 10; i++)
{
Thread t = new Thread(new ThreadStart(() =>
{
while (Interlocked.Exchange(ref current, 1) == 1)
{
//此循环用于等待当前捕获current的线程执行结束
}
Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString() + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Random random = new Random();
Thread.Sleep(random.Next(1, 3) * 1000);//随机休眠1-3秒
Interlocked.Exchange(ref current, 0);//将current重置为0
}));
t.Start();
}
}
以上的代码仅仅是笔记用途,没有深入讲解各个方式的优缺点及用途,只是大概的解释知道的这些方法,有兴趣的话,大家可以结合每一个主题的文章详细了解其用法及优缺点,谢谢!