• C#中什么样的代码存在线程安全问题
  • 发布于 2个月前
  • 283 热度
    0 评论
最近线上又出现了几次线程安全问题 导致的服务异常,线程安全问题都是隐藏的炸弹,有可能几个月都不出问题,也有可能连续几天爆炸好几次,问题出现的结果完全是无法确定的,包括但不限于如下结果:
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;
    }
}

用户评论