一.简述
使用多线程时,不可避免的会遇到线程安全的问题,从而需要为使用多线程代码的安全执行考虑
二.注意点
- lock只对多线程有效,对单线程无效,单线程lock不会导致死锁
- 不推荐使用lock(this),因为在它外部也可以访问它
- 不应该使用lock(string(类型)),因为string在内存分配上是重用的,可能会导致冲突
- lock中包含的代码最好不要太多,因为在这里是单线程运行的
- .net提供了一些线程安全的集合类,使用这些集合不需要用到lock
- 在可以使用数据分拆的方法来使用多线程时,最好使用数据分拆而不使用lock
- lock的对象应该是 private static readonly object Object_Lock = new object();
三.简单案例
-
lock只对多线程有效,对单线程无效 ,单线程lock不会导致死锁
class Program
{
static void Main(string[] args)
{
try
{
Console.WriteLine("RecursiveCall call start");
RecursiveCall();
Console.WriteLine("RecursiveCall call end");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
private static object Object_Lock = new object();
private static int Num = 0;
/// <summary>
/// 单线程lock测试
/// </summary>
private static void RecursiveCall()
{
lock (Object_Lock)
{
Num++;
if (Num <= 5)
{
//输出Num-当前线程id-当前时间
Console.WriteLine($"Num:{Num} , ThreadId:{Thread.CurrentThread.ManagedThreadId} , {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}"); //$在C# 6 方可使用
Thread.Sleep(1000);
//递归调用,所以会多次访问lock (Object_Lock)
RecursiveCall();
}
};
}
}
调用RecursiveCall方法之后控制台输出如下:
结论:从控制台输出结果可知,单线程可以多次进入lock (Object_Lock)区域,所以不用担心lock锁主单线程导致死锁的情况
-
不推荐使用lock(this),因为在它外部也可以访问它
class Program
{
static void Main(string[] args)
{
try
{
ThisLockTest lockTest = new ThisLockTest();
//开启一个任务在2秒钟之后执行
Task.Delay(2000).ContinueWith(t =>
{
lock (lockTest)
{
Console.WriteLine($"ThreadId:{Thread.CurrentThread.ManagedThreadId} , {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
}
});
//调用测试类方法
lockTest.Method();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
}
/// <summary>
/// This lock测试类
/// </summary>
public class ThisLockTest
{
private int num = 0;
public void Method()
{
lock (this)
{
//循环5次,输出相应的信息,每次停止1秒钟,耗时大约5秒
while (num < 5)
{
Console.WriteLine($"{num++} , ThreadId:{Thread.CurrentThread.ManagedThreadId} , {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
Thread.Sleep(1000);
}
}
}
}
控制台输出如下:
结论:正常理解应该是在第2或者3行就输出ThreadId:4的信息,但却不是,这里就是因为lock(lockTest)和lock(this)中的 lockTest和this是同一对象,也就是说两个lock锁的是同一对象,只是名称不同而已,所以才导致同步输出了
-
不应该使用lock(string(类型)),因为string在内存分配上是重用的,可能会导致冲突
/// <summary>
/// 定义两个相同字符串的 lock
/// </summary>
public static string StringLock = "String";
public static string StringLock_New = "String";
class Program
{
static void Main(string[] args)
{
try
{
///启动两个线程执行任务
///两个任务分别是在进入lock之前输出即将进入lock区域,在进入lock区域时输出已经进入,在退出lock时输出即将退出lock区域
Task.Run(() =>
{
Console.WriteLine($"ThreadId:{Thread.CurrentThread.ManagedThreadId} 即将进入lock区域 , {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
lock(StringLock)
{
Console.WriteLine($"ThreadId:{Thread.CurrentThread.ManagedThreadId} 已经进入lock区域 , {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
//停止5秒钟
Thread.Sleep(5000);
Console.WriteLine($"ThreadId:{Thread.CurrentThread.ManagedThreadId} 即将出lock区域 , {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
}
});
Task.Run(() =>
{
Console.WriteLine($"ThreadId:{Thread.CurrentThread.ManagedThreadId} 即将进入lock区域 , {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
lock (StringLock_New)
{
Console.WriteLine($"ThreadId:{Thread.CurrentThread.ManagedThreadId} 已经进入lock区域 , {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
//停止5秒钟
Thread.Sleep(5000);
Console.WriteLine($"ThreadId:{Thread.CurrentThread.ManagedThreadId} 即将出lock区域 , {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
}
});
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
}
控制台输出如下:
从控制台输出红色方格中可以看出之后登记第一个线程退出了lock之后,第二个线程才能进入lock,这说明了lock()的是同一个对象,所以推荐不应该使用string类型的lock,因为两个string对象可能相同(享元模式)
-
lock中包含的代码最好不要太多,因为在这里是单线程运行的
这里就不用举例子了,想一想就能够知道,一个线程执行了越多的代码,执行使用可能就越长,那么在lock外面等待的线程就要一直等,这样用多线程就没太大的作用了
-
.net提供了一些线程安全的集合类,使用这些集合不需要用到lock
在System.Collections.Concurrent命名空间下的集合类都是线程安全的,就不需要使用lock了
-
在可以使用数据分拆的方法来使用多线程时,最好使用数据分拆而不使用lock
对于多任务的程序,如果这些任务可以分开进行,那么最好是使用数据分拆的方法执行,而不是使用lock(待更新)