【问题标题】:Calling static async methods from ASP.net project从 ASP.net 项目调用静态异步方法
【发布时间】:2015-12-03 15:43:32
【问题描述】:

我想知道这种情况是否是线程安全的,是否存在我目前没有看到的问题:

  1. 我从 ASP.net 控制器调用非静态类的非静态方法(这个类在另一个项目中,并且类被注入到控制器中)。

  2. 这个方法(它是非静态的)做一些工作并调用一些其他的静态方法,传递给它 userId

  3. 最后静态方法做了一些工作(需要userId)

我相信这种方法是线程安全的,如果两个用户同时调用此方法(假设在同一纳秒内),一切都会正确完成。我是正确的还是完全错误的?如果我错了,在 ASP.net 项目中使用静态方法的正确方法是什么?

编辑

这里是代码:)

这是来自控制器的调用:

await _workoutService.DeleteWorkoutByIdAsync(AzureRedisFeedsConnectionMultiplexer.GetRedisDatabase(),AzureRedisLeaderBoardConnectionMultiplexer.GetRedisDatabase(), workout.Id, userId);

DeleteWorkoutByIdAsync 如下所示:

public async Task<bool> DeleteWorkoutByIdAsync(IDatabase redisDb,IDatabase redisLeaderBoardDb, Guid id, string userId)
    {

        using (var databaseContext = new DatabaseContext())
        {
            var workout = await databaseContext.Trenings.FindAsync(id);

            if (workout == null)
            {
                return false;
            }

            databaseContext.Trenings.Remove(workout);

            await databaseContext.SaveChangesAsync();

            await RedisFeedService.StaticDeleteFeedItemFromFeedsAsync(redisDb,redisLeaderBoardDb, userId, workout.TreningId.ToString());
        }

        return true;
    }

您可以注意到 DeleteWorkoutByIdAsync 调用静态方法 StaticDeleteFeedItemFromFeedsAsync,如下所示:

public static async Task StaticDeleteFeedItemFromFeedsAsync(IDatabase redisDb,IDatabase redisLeaderBoardDd, string userId, string workoutId)
 {


        var deleteTasks = new List<Task>();
        var feedAllRedisVals = await redisDb.ListRangeAsync("FeedAllWorkouts:" + userId);
        DeleteItemFromRedisAsync(redisDb, feedAllRedisVals, "FeedAllWorkouts:" + userId, workoutId, ref deleteTasks);


        await Task.WhenAll(deleteTasks);
  }

这里是静态方法 DeleteItemFromRedisAsync,在 StaticDeleteFeedItemFromFeedsAsync 中调用:

private static void DeleteItemFromRedisAsync(IDatabase redisDb, RedisValue [] feed, string redisKey, string workoutId, ref List<Task> deleteTasks)
  {
        var itemToRemove = "";

        foreach (var f in feed)
        {

            if (f.ToString().Contains(workoutId))
            {
                itemToRemove = f;
                break;
            }

        }
        if (!string.IsNullOrEmpty(itemToRemove))
        {
            deleteTasks.Add(redisDb.ListRemoveAsync(redisKey, itemToRemove));
        }
  }

【问题讨论】:

  • 我认为您需要发布代码。某些东西是否是线程安全的(或不是)取决于可以执行的每一行代码。
  • 如果静态函数只使用局部变量来完成所有工作,而不使用任何静态变量,那么你很好。否则,你一点都不好。但是你为什么不发布一些代码给我们看看呢?
  • @hyperN 你的编辑改变了一切。您本质上是在询问 async/await 是否是线程安全的,这是一个截然不同的问题,需要一个更复杂的答案。
  • 我希望我对 async/await 有足够的了解,能够回答您的问题。不幸的是,我不是。而且我什至担心,即使你进行了彻底的推理,得出了应该没问题的逻辑结论,除非你先进行压力测试,否则你可能不会知道它是否真的没问题。
  • @MikeNakis 感谢您的帮助:)

标签: c# asp.net


【解决方案1】:

“线程安全”不是一个独立的术语。 Thread Safe 面对的是什么?您期望在这里进行什么样的并发修改?

我们这里从几个方面来看:

  • 您自己的可变共享状态:您在此代码中没有任何共享状态;所以它是自动线程安全的。
  • 间接共享状态:DatabaseContext。这看起来像一个 sql 数据库,而且它们往往是线程“安全的”,但这究竟意味着什么取决于所讨论的数据库。例如,您正在删除Trenings 行,如果其他线程也删除了同一行,您可能会遇到(安全)并发冲突异常。并且根据隔离级别,您可能会遇到并发冲突异常,即使对于“Trenings”的其他某些突变也是如此。在最坏的情况下,这意味着一个失败的请求,但数据库本身不会损坏。
  • Redis 本质上是单线程的,所以所有操作都是序列化的,从这个意义上说是“线程安全的”(这可能不会给您带来太多好处)。您的删除代码获取一组键,然后最多删除其中一个。如果两个或多个线程同时尝试删除同一个键,一个线程可能会尝试删除一个不存在的键,这可能出乎您的意料(但不会导致数据库损坏)。
  • redis+sql 之间的隐式一致性:看起来你正在使用 guid,所以不相关的事情发生冲突的可能性很小。您的示例仅包含删除操作(这可能不会导致一致性问题),因此很难推测在所有 other 情况下 redis 和 sql 数据库是否会保持一致。一般来说,如果您的 ID 从未被重复使用,那么您可能是安全的 - 但保持两个数据库同步是一个难题,而且您很可能会在某个地方出错。

但是,您的代码似乎过于复杂。如果您希望能够长期保持这一点,我建议您大大简化它。

  • 除非你真的知道你在做什么,否则不要使用ref参数(这里没有必要)。
  • 不要将字符串与其他数据类型混淆,因此尽可能避免使用ToString()绝对避免像Contains 这样的讨厌的技巧来检查密钥是否相等。您希望您的代码在发生意外情况时中断,因为“一瘸一拐”的代码几乎无法调试(并且您编写错误)。
  • 如果您唯一能做的就是等待所有任务,请不要有效地返回一组任务 - 不妨在被调用者中这样做以简化 API。
  • 不要使用redis。这可能只是分散注意力 - 您已经有另一个数据库,因此您不太可能在这里需要它,除非出于性能原因,并且为假设的性能问题添加整个额外的数据库引擎还为时过早。与只有一个 db 相比,需要额外连接的额外开销可能会使您的代码变慢,尤其是当您无法保存许多 sql 查询时。

【讨论】:

  • 非常感谢您的回答和提示,它们很棒:) 我主要将 Redis + SQL 用于提要(例如在 Twitter 或 FB 中)。我正在开发的应用程序有大约 50 万用户,每个用户都可以关注其他用户,他们会以此为基础获得提​​要。当我只使用 SQL 时,创建提要花了我 4 多秒。现在使用 Redis,提要在大约 500 毫秒内提供服务,这大大提高了速度。 Feed 是人们看到的第一件事,所以它必须很快。
  • 关于 ToString() + string.Contains: 从 Redis 我得到一个名为 ReidsValue 的对象,在我的例子中是字符串化的对象,我首先做的是反序列化,然后我检查了 Id,但是然后我注意到 string.Contains 快了几毫秒。是的,这是愚蠢的微优化,但是有些用户在他们的提要中有超过 10k 的项目,当你进行操作时,即使 1ms 超过 10.000 次也更快,最终你节省了大量时间。还是我这样做完全错了?
  • 500ms 和 4s 都非常慢 - 我怀疑你在某个地方遇到了可伸缩性问题。如果用户在他们的提要中确实有 10k 个项目,那么您真的需要获得比第一个(比如说)100 个更多的项目吗?检索 Feed 需要多少次查询?你看过索引、缓存和 Dapper 之类的东西吗?
  • 从某种角度来看,我只是对我维护的应用程序进行了快速检查:读取 10000 行的 4 列表(两个整数,一个可为空的布尔值和一个通常为空的字符串)需要大约 4 毫秒。读取 10000 行 59 列的凌乱、讨厌的表大约需要 45 毫秒。坦率地说,我认为您不需要请求 10000 行来检索提要。
  • 但是 redis 不做连接,所以在某些时候你无论如何都会对这些多个表进行非规范化(展平)——这很可能是真正的性能提升所在。 (我知道并且使用过 redis,如果你需要它——当然——但听起来你在这里并不真的需要它)。
【解决方案2】:

注意:这个答案是在 OP 修改他们的问题以添加他们的代码之前发布的,这表明这实际上是 async/await 是否是线程安全的问题。


静态方法本身不是问题。如果一个静态方法是自包含的并且只使用局部变量来完成它的工作,那么它是完全线程安全的。

如果静态方法不是自包含的(委托给线程不安全的代码)或者如果它以非线程安全的方式操作静态状态,即访问静态变量以在lock() 子句。

例如,int.parse()int.tryParse() 是静态的,但完全是线程安全的。想象一下,如果它们不是线程安全的,那会是多么可怕。

【讨论】:

    【解决方案3】:

    您在这里所做的是在列表上同步(删除任务)。如果你这样做,我会推荐 2 件事中的 1 件事。

    1) 使用线程安全集合 https://msdn.microsoft.com/en-us/library/dd997305(v=vs.110).aspx

    2) 让您的 DeleteItemFromRedisAsync 返回一个任务并等待它。

    尽管我认为在这种特殊情况下,只要重构它,我就看不到任何问题,并且 DeleteItemFromRedisAsync 可以并行调用多次,那么你就会遇到问题。原因是如果多个线程可以修改您的 deleteTasks 列表,那么您将不再保证将它们全部收集(https://msdn.microsoft.com/en-us/library/dd997373(v=vs.110).aspx 如果 2 个线程在非线程安全中执行“添加”/添加到末尾方式同时丢失其中 1 个),因此您可能在等待所有任务完成时错过了一项任务。

    我也会避免混合范式。使用 async/await 或跟踪任务集合并让方法添加到该列表中。不要两者都做。从长远来看,这将有助于您的代码的可维护性。 (注意,线程仍然可以返回一个任务,你收集这些任务然后等待所有任务。但是收集方法负责任何线程问题,而不是隐藏在被调用的方法中)

    【讨论】:

    • 感谢您的回答,所以您的意思是,如果我更改 DeleteItemFromRedisAsync 使其看起来像:private static async Task DeleteItemFromRedisAsync(IDatabase redisDb, RedisValue [] feed, string redisKey, string workoutId) 并且它有等待(而不是返回):await Task.WhenAll(deleteTasks); - deleteTasks 现在在此初始化函数,而不是作为参考传递我会很好吗? (其他代码保持不变,除了看起来像的函数调用):await DeleteItemFromRedisAsync
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-04-18
    • 1970-01-01
    • 2011-11-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多