• .Net常见线程安全问题整理


    最近线上又出现了几次线程安全问题 导致的服务异常,
    线程安全问题都是隐藏的炸弹,有可能几个月都不出问题,也有可能连续几天爆炸好几次,
    问题出现的结果完全是无法确定的,包括但不限于如下结果:

    1. 应用异常,且无法自恢复,必须重启站点或服务;
    2. 陷入死循环,导致CPU占用100%,从而整台服务器崩溃;
    3. 错误数据入库,导致一系列的排查、数据修复的困难,甚至可能无法修复数据;

    因此,很有必要做几次全局的筛查,做一些特征值搜索和处理,
    简单梳理了一下,凡是符合如下5种特征的代码,都有线程不安全的可能性:

    1、类的静态变量:

    public class Iamclass
    {
        static Dictionary<string, string> _cache = new Dictionary<string, string>();
        public static void Operation()
        {
            _cache.Add(new Guid().ToString(), "1");// 线程不安全代码
        }
    }
    

    2、类的静态属性:

    public class Iamclass
    {
        static Dictionary<string, string> Cache {get; set;} = new Dictionary<string, string>();
        public static void Operation()
        {
            Cache.Add(new Guid().ToString(), "1");// 线程不安全代码
        }
    }
    

    3、单例对象的静态变量:

    public class XxxService
    {
        IIamclass instance = IocHelper.GetSingleInstance<IIamclass>(); // 获取单例
    }
    public class Iamclass : IIamclass
    {
        Dictionary<string, string> _cache = new Dictionary<string, string>();
        public void Operation()
        {
            _cache.Add(new Guid().ToString(), "1");// 线程不安全代码
        }
    }
    

    4、单例对象的静态属性:

    public class XxxService
    {
        IIamclass instance = IocHelper.GetSingleInstance<IIamclass>(); // 获取单例
    }
    public class Iamclass : IIamclass
    {
        Dictionary<string, string> Cache {get; set;} = new Dictionary<string, string>();
        public void Operation()
        {
            Cache.Add(new Guid().ToString(), "1");// 线程不安全代码
        }
    }
    

    5、多线程共享的局部变量

    public class XxxService
    {
        public void Operation()
        {
            var cache = new Dictionary<string, string>();
            System.Threading.Tasks.Parallel.For(1, 10, idx =>
            {
                cache.Add(new Guid().ToString(), "1"); //线程不安全代码
            });
        }
    }
    

    列举下处理过的几次线程安全问题,都是如下2类问题:
    1、应用错误且无法恢复的,通常异常为:索引超出了数组界限:

    public class MessageService : BaseService
    {
        private static Dictionary<string, Timer> _timerDict = new Dictionary<string, Timer>();
        public async void SendMessageAsync(string msgId, MessageInputDto2 input)
        {
    	    var timer = new Timer(60 * 1000) { AutoReset = true };
            _timerDict[msgId] = timer;	   // 问题代码 
    	    timer.Elapsed += (sender, eventArgs) =>
    	    {
    	    	try
    	    	{
    	    		/* 具体业务代码 */
    	    		timer.Stop();
                    timer.Close();
                    _timerDict.Remove(msgId);
    	    	}
    	    	catch(Exception exp)
    	    	{
    	    		// 异常处理代码
    	    	}
    	    }
    	}
    }
    

    解决方法,一般是加锁
    注:如果加lock 可能出现瓶颈,要进行流程梳理,是否要更换实现方案:

    lock(_timerDict)
    {
    	_timerDict[msgId] = timer;	   // 问题代码 
    }
    timer.Elapsed += (sender, eventArgs) =>
    {
    	try
    	{
    		/* 具体业务代码 */
    		timer.Stop();
            timer.Close();
            lock(_timerDict)
            {
            	_timerDict.Remove(msgId);
            }
    	}
    	catch(Exception exp)
    	{
    		// 异常处理代码
    	}
    }
    

    2、陷入死循环,导致服务器CPU 100%卡顿问题:
    有个常见业务,获取一串没有使用过的随机数或随机字符串,比如用户身份Token,比如抽奖等等
    下面是常见的获取不重复的随机数代码,
    在_rnd.Next 没有加锁,其内部方法InternalSample会导致返回结果都是0,从而导致while陷入死循环:

    public class CodeService
    {
        private static Random _rnd = new Random(Guid.NewGuid().GetHashCode());
        public static GetCode()
        {
        	var ret = "";
    	    var redis = IocHelper.GetSingleInstance<IRedis>();
    	    // 获取一个未使用过的序号
        	do
        	{
        	    ret = _rnd.Next(10000).ToString();  // 问题代码
        	}while(!redis.Add(ret, "", TimeSpan.FromSeconds(3600)));
        	return ret;
        }
    }
    

    解决方法,双重校验:加锁,并判断循环次数:

    public class CodeService
    {
        private static Random _rnd = new Random(Guid.NewGuid().GetHashCode());
        public static GetCode()
        {
        	var ret = "";
    	    var redis = IocHelper.GetSingleInstance<IRedis>();
    	    var maxLoop = 10;
    	    // 获取一个未使用过的序号
        	do
        	{
        		lock(_rnd)
        		{
        	        ret = _rnd.Next(10000).ToString();
        	    }
        	}while(!redis.Add(ret, "", TimeSpan.FromSeconds(3600)) && (maxLoop--) > 0);
        	if(maxLoop <= 0)
        	{
        		throw new Exception("10次循环,未找到可用数据:" + ret);
        	}
        	return ret;
        }
    }
    

    https://blog.csdn.net/youbl/article/details/101693752 转载

  • 相关阅读:
    网络配置
    数据管理
    仓库
    dockerfile
    docker 概念
    查看日志小技巧
    springboot缓存
    360笔试算法题 AT变换
    删除链表里全部重复元素以及删除链表重复元素只保留一个
    报错类型
  • 原文地址:https://www.cnblogs.com/anyihen/p/14097282.html
Copyright © 2020-2023  润新知