NashTech Insights

Common Misconceptions in C# and .NET – Not Understanding Thread Safety.

Hieu Nguyen
Hieu Nguyen
Table of Contents
binary, binary system, binary code-8177888.jpg

We will explore the critical topic of thread safety, a concept that demands careful consideration. In the .NET realm, it’s important to note that all instance methods are generally assumed to lack thread safety, while conversely, static methods are presumed to be thread-safe unless explicitly documented otherwise.

Let’s delve into an illustrative case where the absence of thread safety disrupts our program. In the following sample application, we maintain a static list named ‘L’ and concurrently initiate two tasks:

    class Program
    {
        static object locker = new object();
        static List<int> l = new List<int>();
        static void Main(string[] args)
        {

            Task.Run(Loop);
            Task.Run(Loop);
            Console.ReadKey();
        }
        static void Loop()
        {
            try
            {
                while (true)
                {
                    //lock (locker)
                    {
                        l.Add(1);
                        l.RemoveAt(0);
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }

These two tasks continually and remove items from our list. Ideally, this should not pose a problem since we consistently add and subsequently remove items. Moreover, in the event of an exception, we handle it gracefully by printing the error message.

However, upon running this application, an “IndexOutOfRangeException” promptly emerges, indicating that the index must be negative. This seemingly paradoxical error occurs because the thread safety of our operations is in question. Both “Add()” and “RemoveAt()” are instance methods and are inherently not thread-safe, making them unsuitable for concurrent use.

To rectify this issue, we need to encapsulate our logic within a “lock” statement on a global variable. If we uncomment this “lock” statement and run the application once more, you will observe that no errors occur.

This example underscore the important of exercising caution regarding to thread safety. When working with collections, it is advisable to either employ “lock” or, preferable, opt for concurrent collections:
https://learn.microsoft.com/en-us/dotnet/standard/collections/thread-safe/

Entity Framework thread safe.

You should also be careful when working with Entity Framework because the DbContext is not thread safe. In the following sample, I will implement a Generic Repository pattern .ASP.net core, and on IRepository, I use IQueryable instead of IEnumerable for the collection returned.

public  interface IRepository<T> where T: BaseEntity
{
    IQueryable<T> Table { get; }
    IEnumerable<T> TableNoTracking { get; }
    T Get(long id);
    void Insert(T entity);
    void Update(T entity);
    void Delete(T entity);
}

The implementation class is:

  public class EFRepository<T> : IRepository<T> where T : BaseEntity
    {
        private readonly ApplicationDbContext _ctx;
        private DbSet<T> entities;
        string errorMessage = string.Empty;

        public EFRepository(ApplicationDbContext context)
        {
            this._ctx = context;
            entities = context.Set<T>();
        }

        public virtual IQueryable<T> Table => this.entities;
    }

and the Service class is:

public class MovieService : IMovieService
{
        private readonly IRepository<MovieItem> _repoMovie;

        public MovieService(IRepository<MovieItem> repoMovie)
        {
            _repoMovie = repoMovie;
        }

        public async Task<PaginatedList<MovieItem>> GetAllMovies(int pageIndex = 0, int pageSize = int.MaxValue,
               IEnumerable<int> categoryIds = null)
        {
            var query = _repoMovie.Table;

            if (categoryIds != null)
            {
                query = from m in query
                        where categoryIds.Contains(m.CategoryId)
                        select m;
            }

            return await PaginatedList<MovieItem>.CreateAsync(query, pageIndex, pageSize);
        }
}

On Startup.cs

public void ConfigureServices(IServiceCollection services)
  {
        // Add framework services.
        services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

        services.AddMvc();
        services.AddScoped(typeof(IRepository<>), typeof(EFRepository<>));           
        services.AddTransient<IMovieService, MovieService>();
        services.AddTransient<ICategoryService, CategoryService>();
    }

These code throws an error:

InvalidOperationException: A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.

If I replace IQueryable by IEnumerable on IRepository, then it runs fine.

Entity Framework DbContext is not thread safe. You can execute only one query at a time otherwise you will get an exception like you did above.

I suppose that you use our repository multiple times during same request in parallel that’s why you get the exception. If you don’t create a transaction per request you can simply make repository transient. In this case new repository will be created each instance of your service and you will avoid concurrency issue.

But if you need to use transaction for manipulate multiple entities through 2 or many services classes, you will need to use AddScoped lifetime. In this case, you should consider to use “lock” statement for methods which got potential risk while running in multiple parallel tasks.

var locker = new Object(); //should be a global variable
lock(locker)
{
        var query = _repoMovie.Table;

            if (categoryIds != null)
            {
                query = from m in query
                        where categoryIds.Contains(m.CategoryId)
                        select m;
            }

            return await PaginatedList<MovieItem>.CreateAsync(query, pageIndex, pageSize);
}

Hieu Nguyen

Hieu Nguyen

Hieu is an Engineering Manager at NashTech with 20+ years of experience in software development, he is specializing in the Microsoft stack have in-depth knowledge and experience with Microsoft technologies such as .NET framework, C#, ASP.NET, SQL Server, Azure, and Visual Studio. He is in a leadership role that involves overseeing and guiding the technical teams involved in software engineering projects.

Suggested Article

%d