C#多线程编程中的锁系统基本用法
平常在多线程开发中,总避免不了线程同步。本篇就对net多线程中的锁系统做个简单描述。
目录
一:lock、monitor
1:基础。
2: 作用域。
3:字符串锁。
4:monitor使用
二:mutex
三:semaphore
四:总结
一:lock、monitor
1:基础
lock是monitor语法糖简化写法。lock在il会生成monitor。
//======example 1=====
string obj = "helloworld";
lock (obj)
{
console.writeline(obj);
}
//lock il会编译成如下写法
bool isgetlock = false;
monitor.enter(obj, ref isgetlock);
try
{
console.writeline(obj);
}
finally
{
if (isgetlock)
{
monitor.exit(obj);
}
}
isgetlock参数是framework 4.0后新加的。 为了使程序在所有情况下都能够确定,是否有必要释放锁。例: monitor.enter拿不到锁
monitor.enter 是可以锁值类型的。锁时会装箱成新对象,所以无法做到线程同步。
2:作用域
一:lock是只能在进程内锁,不能跨进程。走的是混合构造,先自旋再转成内核构造。
二:关于对type类型的锁。如下:
//======example 2=====
new thread(new threadstart(() => {
lock (typeof(int))
{
thread.sleep(10000);
console.writeline("thread1释放");
}
})).start();
thread.sleep(1000);
lock(typeof(int))
{
console.writeline("thread2释放");
}
运行结果如下:
我们在来看个例子。
//======example 3=====
console.writeline(datetime.now);
appdomain appdomain1 = appdomain.createdomain("appdomain1");
locktest worker1 = (locktest)appdomain1.createinstanceandunwrap(
assembly.getexecutingassembly().fullname,
"consoleapplication1.locktest");
worker1.run();
appdomain appdomain2 = appdomain.createdomain("appdomain2");
locktest worker2 = (locktest)appdomain2.createinstanceandunwrap(
assembly.getexecutingassembly().fullname,
"consoleapplication1.locktest");
worker2.run();
/// <summary>
/// 跨应用程序域边界或远程访问时需要继承marshalbyrefobject
/// </summary>
public class locktest : marshalbyrefobject
{
public void run()
{
lock (typeof(int))
{
thread.sleep(10000);
console.writeline(appdomain.currentdomain.friendlyname + ": thread 释放," + datetime.now);
}
}
}
运行结果如下:
第一个例子说明,在同进程同域,不同线程下,锁type int,其实锁的是同一个int对象。所以要慎用。
第二个例子,这里就简单说下。
a: clr启动时,会创建 系统域(system domain)和共享域(shared domain), 默认程序域(default appdomain)。 系统域和共享域是单例的。程序域可以有多个,例子中我们使用appdomain.createdomain方法创建的。
b: 按正常来说,每个程序域的代码都是隔离,互不影响的。但对于一些基础类型来说,每个程序域都重新加载一份,就显得有点浪费,带来额外的损耗压力。聪明的clr会把一些基本类型object, valuetype, array, enum, string, and delegate等所在的程序集mscorlib.dll,在clr启动过程中都会加载到共享域。 每个程序域都会使用共享域的基础类型实例。
c: 而每个程序域都有属于自己的托管堆。托管堆中最重要的是gc heap和loader heap。gc heap用于引用类型实例的存储,生命周期管理和垃圾回收。loader heap保存类型系统,如methodtable,数据结构等,loader heap生命周期不受gc管理,跟程序域卸载有关。
所以共享域中loader heap mscorlib.dll中的int实例会一直保留着,直到进程结束。单个程序域卸载也不受影响。作用域很大有没有!!!
这时第二个例子也很容易理解了。 锁int实例是跨程序域的,mscorlib中的基础类型都是这样。 极容易造成死锁,慎用。 而自定义类型则会加载到自己的程序域,不会影响别人。
3:字符串的锁
我们都知道锁的目的,是为了多线程下值被破坏。也知道string在c#是个特殊对象,值是不变的,每次变动都是一个新对象值,这也是推荐stringbuilder原因。如例:
//======example 4=====
string str1 = "mushroom";
string str2 = "mushroom";
var result1 = object.referenceequals(str1, str2);
var result2 = object.referenceequals(str1, "mushroom");
console.writeline(result1 + "-" + result2);
/* output
* true-true
*/
正式由于c#中字符串的这种特性,所以字符串是在多线程下是不会被修改的,只读的。它存在于systemdomain域中managed heap中的一个hash table中。key为string本身,value为string对象的地址。
当程序域需要一个string的时候,clr首先在这个hashtable根据这个string的hash code试着找对应的item。如果成功找到,则直接把对应的引用返回,否则就在systemdomain对应的managed heap中创建该 string,并加入到hash table中,并把引用返回。所以说字符串的生命周期是基于整个进程的,也是跨appdomain。
4:monitor用法
介绍下wait,pulse,pulseall的用法。有注释,大家直接看代码吧。
static string str = "mushroom";
static void main(string[] args)
{
new thread(() =>
{
bool isgetlock = false;
monitor.enter(str, ref isgetlock);
try
{
console.writeline("thread1第一次获取锁");
thread.sleep(5000);
console.writeline("thread1暂时释放锁,并等待其他线程释放通知信号。");
monitor.wait(str);
console.writeline("thread1接到通知,第二次获取锁。");
thread.sleep(1000);
}
finally
{
if (isgetlock)
{
monitor.exit(str);
console.writeline("thread1释放锁");
}
}
}).start();
thread.sleep(1000);
new thread(() =>
{
bool isgetlock = false;
monitor.enter(str, ref isgetlock); //一直等待中,直到其他释放。
try
{
console.writeline("thread2获得锁");
thread.sleep(5000);
monitor.pulse(str); //通知队列里一个线程,改变锁状态。 pulseall 通知所有的
console.writeline("thread2通知其他线程,改变状态。");
thread.sleep(1000);
}
finally
{
if (isgetlock)
{
monitor.exit(str);
console.writeline("thread2释放锁");
}
}
}).start();
console.readline();
二:mutex
lock是不能跨进程锁的。 mutex作用和lock类似,但是它能跨进程锁资源(走的是windows内核构造)。 我们来看个例子
static bool createnew = false;
//第一个参数 是否应拥有互斥体的初始所属权。即createnew true时,mutex默认获得处理信号
//第二个是名字,第三个是否成功。
public static mutex mutex = new mutex(true, "mushroom.mutex", out createnew);
static void main(string[] args)
{
//======example 5=====
if (createnew) //第一个创建成功,这时候已经拿到锁了。 无需再waitone了。一定要注意。
{
try
{
run();
}
finally
{
mutex.releasemutex(); //释放当前锁。
}
}
//waitone 函数作用是阻止当前线程,直到拿到收到其他实例释放的处理信号。
//第一个参数是等待超时时间,第二个是否退出上下文同步域。
else if (mutex.waitone(10000,false))//
{
try
{
run();
}
finally
{
mutex.releasemutex();
}
}
else//如果没有发现处理信号
{
console.writeline("已经有实例了。");
console.readline();
}
}
static void run()
{
console.writeline("实例1");
console.readline();
}
我们顺序起a b实例测试下。 a首先拿到锁,输出 实例1 。 b在等待, 如果10秒内a释放,b拿到执行run()。 超时后输出 已经有实例了。
这里注意的是第一个拿到处理信号 的实例,已经拿到锁了。不需要再waitone。 否则报异常。
三:semaphore
即信号量,我们可以把它理解为升级版的mutex。mutex对一个资源进行锁,semaphore则是对多个资源进行加锁。
semaphore是由windows内核维持一个int32变量的线程计数器,线程每调用一次、计数器减一、释放后对应加一, 超出的线程则排队等候。
走的是内核构造,所以semaphore也是可以跨进程的。
static void main(string[] args)
{
console.writeline("准备处理队列");
bool createnew = false;
semaphoresecurity ss = new semaphoresecurity(); //信号量权限控制
semaphore semaphore = new semaphore(2, 2, "mushroom.semaphore", out createnew,null);
for (int i = 1; i <= 5; i++)
{
new thread((arg) =>
{
semaphore.waitone();
console.writeline(arg + "处理中");
thread.sleep(10000);
semaphore.release(); //即semaphore.release(1)
//semaphore.release(5);可以释放多个,但不能超过最大值。如果最后释放的总量超过本身总量,也会报错。 不建议使用
}).start(i);
}
console.readline();
}
四:总结
mutex、semaphore 需要由托管代码转成本地用户模式代码、再转换为本地内核代码。
反之同样,饶了一大圈,性能肯定不会很好。所以仅在需要跨进程的场景才使用。