C# 多线程安全(2)多线程访问集合的问题2

勇哥注:

《多线程安全》这个系列会持续写下去,它是我的一个弱点,有兴趣的朋友可以选择性看看。


上节说到lock锁关键字,它实际上是Monitor的语法糖。

lock锁定的是一个内存地址的引用。

lock必须锁定一个引用类型的变量。


锁定的变量msdn推荐是下面这样的:

private static readonly object lockobj = new object();


这里为什么必须这样是有玄机的。我们来依次看几个例子。


(一)锁定null

null在定义的时候当引用类型,执行的时候会报错误。

image.png

这里需要注意的是,有时候这样用vs是不会报错的,因为线程会把异常吃掉,这个时候你用try catch把它括起来就可以看到异常。

这可能跟vs版本或者异常设置有关系。


(二)锁定的变量改为public


见代码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private static readonly object lockobj = new object();

        private void button1_Click(object sender, EventArgs e)
        {
            var list1 = new List<int>();
            for (int i = 0; i < 10000; i++)
            {
                Task.Run(() =>
                {
                    lock (lockobj)
                    {
                        list1.Add(i);
                    }
                });
            }
            Thread.Sleep(6000);
            Console.WriteLine(list1.Count);
        }

        private void button2_Click(object sender, EventArgs e)
        {
            test1.show();

            for (int i = 0; i < 5; i++)
            {
                int k = i;
                Task.Run(() =>
                {
                    lock (test1.lockobj)
                    {
                        Console.WriteLine($"{i},{k},start...[{Thread.CurrentThread.ManagedThreadId}]");
                        Thread.Sleep(1000);
                        Console.WriteLine($"{i},{k},end...[{Thread.CurrentThread.ManagedThreadId}]");
                    }
                });
            }
        }
    }


    public class test1
    {
        //注意这里是public
        public static readonly object lockobj = new object();
        public static void show()
        {
            for(int i=0;i<5;i++)
            {
                int k = i;
                Task.Run(() =>
                {
                    lock(lockobj)
                    {
                        Console.WriteLine($"{i},{k},test1start...[{Thread.CurrentThread.ManagedThreadId}]");
                        Thread.Sleep(1000);
                        Console.WriteLine($"{i},{k},test1end...[{Thread.CurrentThread.ManagedThreadId}]");
                    }
                });
            }
        }
    }
}


这里lock后,执行次序就变成先start,再end

这里由于lock后程序的执行变为单线程模式。先是10号线程工作,它完成之间其它线程需要排除。


如果没有lock,是乱序的。

image.png



如果lock的变量置为public, 则两个线程之间是相互阻塞的,而不是并发的。

这个就是public的潜在问题。

image.png


因此如果两个线程想并发,请各自锁自己的变量,不要锁相同的变量。


(三)锁定的变量去掉static


把上面的代码改一下:

public class test2
    {
        //注意这里没有static
        public  readonly object lockobj = new object();
        public  void show(int pos)
        {
            for (int i = 0; i < 5; i++)
            {
                int k = i;
                Task.Run(() =>
                {
                    lock (lockobj)
                    {
                        Console.WriteLine($"{i},{k},[{pos}]start...[{Thread.CurrentThread.ManagedThreadId}]");
                        Thread.Sleep(1000);
                        Console.WriteLine($"{i},{k},[{pos}]end...[{Thread.CurrentThread.ManagedThreadId}]");
                    }
                });
            }
        }
    }
    
   private void button3_Click(object sender, EventArgs e)
        {
            var t1= new test2();
            t1.show(1);
            var t2 = new test2();
            t2.show(2);
            
        }


程序结果单次调用内部肯定是顺序执行的,即先有start, 才有end,因为有lock

问题是两次调用之间可以并发吗?

答案是可以。

因为没有static后是成员变量,实例化后两个lockobj 变量是两个对象。

如果你希望多个类的实例共用一个变量,就要加static


执行结果中箭头所示线程10和15同时start,证明两次调用间是并发关系。


image.png


(四)锁定的变量为string


由于字符串也是引用类型,能不能锁字符串呢?

答案是最好不要!!


我们把代码改下,试完你就知道了。

        private void button4_Click(object sender, EventArgs e)
        {
            var t1 = new test3();
            t1.show(1);
            var t2 = new test3();
            t2.show(2);
        }
        
 public class test3
    {
        //注意这里字符串,它也是引用类型
        public readonly string lockobj = "勇哥";
        public  void show(int pos)
        {
            for (int i = 0; i < 5; i++)
            {
                int k = i;
                Task.Run(() =>
                {
                    lock (lockobj)
                    {
                        Console.WriteLine($"{i},{k},[{pos}]start...[{Thread.CurrentThread.ManagedThreadId}]");
                        Thread.Sleep(1000);
                        Console.WriteLine($"{i},{k},[{pos}]end...[{Thread.CurrentThread.ManagedThreadId}]");
                    }
                });
            }
        }
    }


从执行结果上来看,两次调用间是不能并发的。

image.png

这个原因是C#的字符串是类似享元模式的。

虽然lockobj 是两个类的实例的成员变量,理论是不同的两个变量。lock不是锁定变量而是锁定变量的引用,但是由于享元模式,这些变量的引用是同一个对象。

就算是有1000个test3类的实例,在堆里面只装着一个“勇哥”。


有关C#字符串的享元模式的知识点,参见下面的贴子:

http://www.skcircle.com/?id=1894


(四)范型类的问题


上代码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private static readonly object lockobj = new object();

        private void button1_Click(object sender, EventArgs e)
        {
            //泛型类在类型参数相同的情况下,它是相同的类
            //泛型类在类型参数不同的情况下,它是不同的类
            test1<int>.show(1);
            test1<int>.show(2);
            test1<test2>.show(3);
        }

    }


    public class test1<T>
    {
        
        private static readonly object lockobj = new object();
        public static void show(int index)
        {
            for(int i=0;i<5;i++)
            {
                int k = i;
                Task.Run(() =>
                {
                    lock(lockobj)
                    {
                        Console.WriteLine($"{i},{k},test1start[{index}]...[{Thread.CurrentThread.ManagedThreadId}]");
                        Thread.Sleep(1000);
                        Console.WriteLine($"{i},{k},test1end[{index}]...[{Thread.CurrentThread.ManagedThreadId}]");
                    }
                });
            }
        }
    }

    public class test2
    {
        
        public  readonly object lockobj = new object();
        public  void show(int pos)
        {
            for (int i = 0; i < 5; i++)
            {
                int k = i;
                Task.Run(() =>
                {
                    lock (lockobj)
                    {
                        Console.WriteLine($"{i},{k},[{pos}]start...[{Thread.CurrentThread.ManagedThreadId}]");
                        Thread.Sleep(1000);
                        Console.WriteLine($"{i},{k},[{pos}]end...[{Thread.CurrentThread.ManagedThreadId}]");
                    }
                });
            }
        }
    }





}


对于下面的调用,我们想知道的问题是: 1和3能不能并发?

 test1<int>.show(1);

 test1<int>.show(2);

 test1<test2>.show(3);


答案是可以。

因为泛型类在类型参数类型相同的情况下,它是相同的类。

泛型类在类型参数不同的情况下,它是不同的类。


这意味着1和3的锁变量lockobj  虽然是static的,但是却有两个不同的副本。


(五)this的问题


this当前类的实例,如果锁this

则t1与t2之间可以并发吗?

答案当然是可以。

 private void button2_Click(object sender, EventArgs e)
        {
            var t1 = new test3();
            t1.show(1);
            var t2 = new test3();
            t2.show(2);
        }
        
  public class test3
    {
        public void show(int pos)
        {
            for (int i = 0; i < 5; i++)
            {
                int k = i;
                Task.Run(() =>
                {
                    lock (this)
                    {
                        Console.WriteLine($"{i},{k},[{pos}]start...[{Thread.CurrentThread.ManagedThreadId}]");
                        Thread.Sleep(1000);
                        Console.WriteLine($"{i},{k},[{pos}]end...[{Thread.CurrentThread.ManagedThreadId}]");
                    }
                });
            }
        }
    }


再来看个递归调用的问题

当t1.show(1)后,代码会产生锁死的问题吗?

答案是不会。

private void button2_Click(object sender, EventArgs e)
        {
            var t1 = new test3();
            t1.show(1);
       
        }

public class test3
    {
        int js = 0;
        public void show(int pos)
        {
            for (int i = 0; i < 5; i++)
            {
                js++;
                int k = i;
                Task.Run(() =>
                {
                    lock (this)
                    {
                        Console.WriteLine($"{i},{k},[{pos}]start...[{Thread.CurrentThread.ManagedThreadId}]");
                        Thread.Sleep(1000);
                        Console.WriteLine($"{i},{k},[{pos}]end...[{Thread.CurrentThread.ManagedThreadId}]");
                       
                    }
                });
                if (this.js < 5)
                {
                    this.show(pos);
                }
                else
                {
                    break;
                }
            }
        }
    }




--------------------- 

作者:hackpig

来源:www.skcircle.com

版权声明:本文为博主原创文章,转载请附上博文链接!



本文出自勇哥的网站《少有人走的路》wwww.skcircle.com,转载请注明出处!讨论可扫码加群:

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

会员中心
搜索
«    2024年4月    »
1234567
891011121314
15161718192021
22232425262728
2930
网站分类
标签列表
最新留言
    热门文章 | 热评文章 | 随机文章
文章归档
友情链接
  • 订阅本站的 RSS 2.0 新闻聚合
  • 扫描加本站机器视觉QQ群,验证答案为:halcon勇哥的机器视觉
  • 点击查阅微信群二维码
  • 扫描加勇哥的非标自动化群,验证答案:C#/C++/VB勇哥的非标自动化群
  • 扫描加站长微信:站长微信:abc496103864
  • 扫描加站长QQ:
  • 扫描赞赏本站:
  • 留言板:

Powered By Z-BlogPHP 1.7.2

Copyright Your skcircle.com Rights Reserved.

鄂ICP备18008319号


站长QQ:496103864 微信:abc496103864