欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C#多线程编程中的锁系统基本用法

程序员文章站 2022-07-22 19:35:47
平常在多线程开发中,总避免不了线程同步。本篇就对net多线程中的锁系统做个简单描述。 目录 一:lock、monitor    &nbs...

平常在多线程开发中,总避免不了线程同步。本篇就对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释放");
            }

运行结果如下:

C#多线程编程中的锁系统基本用法

我们在来看个例子。

复制代码 代码如下:

//======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);
            }
        }
    }

运行结果如下:

C#多线程编程中的锁系统基本用法

第一个例子说明,在同进程同域,不同线程下,锁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  需要由托管代码转成本地用户模式代码、再转换为本地内核代码。 

 反之同样,饶了一大圈,性能肯定不会很好。所以仅在需要跨进程的场景才使用。