hyperN
hyperN

Reputation: 2754

Calling static async methods from ASP.net project

I'm wondering will this scenario be thread safe and are there issues that I'm not currently seeing:

  1. From ASP.net controller I call non-static method from non-static class (this class is in another project, and class is injected into controller).

  2. This method (which is non-static) does some work and calls some other static method passing it userId

  3. Finally static method does some work (for which userId is needed)

I believe this approach is thread safe, and that everything will be done properly if two users call this method at the same time (let's say in same nanosecond). Am I correct or completely wrong ? If I am wrong what would be correct way of using static methods within ASP.net project ?

EDIT

Here is code :)

This is call from the controller:

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

Here how DeleteWorkoutByIdAsync looks like:

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;
    }

As you can notice DeleteWorkoutByIdAsync calls static method StaticDeleteFeedItemFromFeedsAsync which looks like this:

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

And here is static method DeleteItemFromRedisAsync which is called in 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));
        }
  }

Upvotes: 8

Views: 2641

Answers (3)

Eamon Nerbonne
Eamon Nerbonne

Reputation: 48146

"Thread safe" isn't a standalone term. Thread Safe in the the face of what? What kind of concurrent modifications are you expecting here?

Let's look at a few aspects here:

  • Your own mutable shared state: You have no shared state whatsoever in this code; so it's automatically thread safe.
  • Indirect shared state: DatabaseContext. This looks like an sql database, and those tend to be thread "safe", but what exactly that means depends on the database in question. For example, you're removing a Trenings row, and if some other thread also removes the same row, you're likely to get a (safe) concurrency violation exception. And depending on isolation level, you may get concurrency violation exceptions even for other certain mutations of "Trenings". At worst that means one failed request, but the database itself won't corrupt.
  • Redis is essentially single-threaded, so all operations are serialized and in that sense "thread safe" (which might not buy you much). Your delete code gets a set of keys, then deletes at most one of those. If two or more threads simultaneously attempt to delete the same key, it is possible that one thread will attempt to delete a non-existing key, and that may be unexpected to you (but it won't cause DB corruption).
  • Implicit consistency between redis+sql: It looks like you're using guids, so the chances of unrelated things clashing are small. Your example only contains a delete operation (which is likely no to cause consistency issues), so it's hard to speculate whether under all other circumstances redis and the sql database will stay consistent. In general, if your IDs are never reused, you're probably safe - but keeping two databases in sync is a hard problem, and you're quite likely to make a mistake somewhere.

However, your code seems excessively complicated for what it's doing. I'd recommend you simplify it dramatically if you want to be able to maintain this in the long run.

  • Don't use ref parameters unless you really know what you're doing (and it's not necessary here).
  • Don't mix up strings with other data types, so avoid ToString() where possible. Definitely avoid nasty tricks like Contains to check for key equality. You want your code to break when something unexpected happens, because code that "limps along" can be virtually impossible to debug (and you will write bugs).
  • Don't effectively return an array of tasks if the only thing you can really do is wait for all of them - might as well do that in the callee to simplify the API.
  • Don't use redis. It's probably just a distraction here - you already have another database, so it's very unlikely you need it here, except for performance reasons, and it's extremely premature to go adding whole extra database engines for a hypothetical performance problem. There's a reasonable chance that the extra overhead of requiring extra connections may make your code slower than if you had just one db, especially if you can't save many sql queries.

Upvotes: 3

Batavia
Batavia

Reputation: 2497

what you are doing here is synchronizing on a list (deleteTasks). If you do this i would recommend 1 of 2 things.

1) Either use thread safe collections https://msdn.microsoft.com/en-us/library/dd997305(v=vs.110).aspx

2) Let your DeleteItemFromRedisAsync return a task and await it.

Although i think in this particular case i don't see any issues as soon as you refactor it and DeleteItemFromRedisAsync can get called multiple times in parallel then you will have issues. The reason being is that if multiple threads can modify your list of deleteTasks then you are not longer guaranteed you collect them all (https://msdn.microsoft.com/en-us/library/dd997373(v=vs.110).aspx if 2 threads do an "Add"/Add-to-the-end in a non-thread safe way at the same time then 1 of them is lost) so you might have missed a task when waiting for all of them to finish.

Also i would avoid mixing paradigms. Either use async/await or keep track of a collection of tasks and let methods add to that list. don't do both. This will help the maintainability of your code in the long run. (note, threads can still return a task, you collect those and then wait for all of them. but then the collecting method is responsible for any threading issues instead of it being hidden in the method that is being called)

Upvotes: 1

Mike Nakis
Mike Nakis

Reputation: 62130

Note: this answer was posted before the OP amended their question to add their code, revealing that this is actually a question of whether async/await is thread-safe.


Static methods are not a problem in and of themselves. If a static method is self-contained and manages to do its job using local variables only, then it is perfectly thread safe.

Problems arise if the static method is not self-contained, (delegates to thread-unsafe code,) or if it manipulates static state in a non-thread safe fashion, i.e. accesses static variables for both read and write outside of a lock() clause.

For example, int.parse() and int.tryParse() are static, but perfectly thread safe. Imagine the horror if they were not thread-safe.

Upvotes: 2

Related Questions