You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现C#统计服务的线程安全版本?

Thread-Safe Refactoring for Your StatisticsService

Great question! Making this service thread-safe without sacrificing performance is totally achievable—let’s break down the key changes and optimizations you need:

1. Replace Dictionary with ConcurrentDictionary for Company-Level Storage

First, swap out your regular Dictionary<string, CompanyStats> with ConcurrentDictionary<string, CompanyStats>. This gives you thread-safe add/retrieve operations out of the box, but we still need to handle the "thundering herd" problem where multiple threads might try to load the same company's data at once.

To avoid redundant database calls when loading a company for the first time, add a secondary lock dictionary to gate access per company:

private readonly ConcurrentDictionary<string, CompanyStats> _statsByCompany = new();
private readonly ConcurrentDictionary<string, object> _companyLocks = new();

2. Refactor CompanyStats and UserStats for Thread Safety

Your List<UserStats> isn't thread-safe, so replace it with another ConcurrentDictionary (keyed by a composite of user ID and hour for fast lookups). Also, wrap non-atomic operations (like incrementing requests and updating the timestamp) in locks to ensure consistency:

private class CompanyStats
{
    public ConcurrentDictionary<string, UserStats> UserStats { get; }

    public CompanyStats(IEnumerable<UserStats> userStats)
    {
        UserStats = new ConcurrentDictionary<string, UserStats>(
            userStats.Select(us => new KeyValuePair<string, UserStats>(GetUserHourKey(us.UserId, us.Hour), us))
        );
    }

    public static string GetUserHourKey(string userId, DateTime hour) 
        => $"{userId}_{hour:yyyy-MM-dd HH:00}";
}

private class UserStats
{
    public string UserId { get; }
    public DateTime Hour { get; }
    private int _requests;
    public int Requests => Volatile.Read(ref _requests);
    public DateTime Updated { get; private set; }
    private readonly object _syncRoot = new();

    public UserStats(string userId, int requests, DateTime hour)
    {
        UserId = userId;
        _requests = requests;
        Hour = hour;
        Updated = DateTime.MinValue;
    }

    public void IncrementAndUpdateTimestamp()
    {
        lock (_syncRoot)
        {
            _requests++;
            Updated = DateTime.UtcNow;
        }
    }

    public void ResetUpdatedTimestamp()
    {
        lock (_syncRoot)
        {
            Updated = DateTime.MinValue;
        }
    }
}

3. Rewrite Increment to Be Thread-Safe

Update the Increment method to use the concurrent collections and per-company locks to prevent duplicate database loads. We'll use GetOrAdd to safely retrieve or create company/user stats:

public void Increment(string companyId, string userId)
{
    var utcNow = DateTime.UtcNow;
    var currentHour = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, utcNow.Hour, 0, 0, DateTimeKind.Utc);
    
    // Get or create a lock object for the company to prevent duplicate DB loads
    var companyLock = _companyLocks.GetOrAdd(companyId, _ => new object());
    lock (companyLock)
    {
        // Safely get or load the company's stats
        var companyStats = _statsByCompany.GetOrAdd(companyId, id =>
        {
            var dbStats = database.GetAllFromThisMonth(id);
            return new CompanyStats(dbStats);
        });

        // Get or create the user's stats for the current hour
        var userHourKey = CompanyStats.GetUserHourKey(userId, currentHour);
        var userStats = companyStats.UserStats.GetOrAdd(userHourKey, _ => 
            new UserStats(userId, 1, currentHour));

        userStats.IncrementAndUpdateTimestamp();
    }
}

4. Make Tick Thread-Safe

The Tick method needs to safely iterate over stats and update the database. Use snapshots of collections to avoid "collection modified" exceptions, and double-check the Updated timestamp inside the user stats lock to prevent race conditions:

public void Tick(object state)
{
    var utcNow = DateTime.UtcNow;
    var currentHour = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, utcNow.Hour, 0, 0, DateTimeKind.Utc);

    // Iterate over a snapshot of company keys to avoid collection modification issues
    foreach (var companyId in _statsByCompany.Keys.ToList())
    {
        if (!_statsByCompany.TryGetValue(companyId, out var companyStats))
            continue;

        // Get a snapshot of user stats that need updating
        var usersToUpdate = companyStats.UserStats.Values
            .Where(us => us.Updated > lastTick)
            .ToList();

        foreach (var userStats in usersToUpdate)
        {
            lock (userStats._syncRoot)
            {
                // Double-check in case another thread reset the timestamp after we queried
                if (userStats.Updated <= lastTick)
                    continue;

                database.Save(GenerateSomeEntity(userStats.Requests));
                userStats.ResetUpdatedTimestamp();
            }
        }
    }

    // Clear cache if we've crossed a month boundary
    if (lastTick != DateTime.MinValue && lastTick.Month != utcNow.Month)
    {
        _statsByCompany.Clear();
        _companyLocks.Clear(); // Clean up unused locks too
    }

    lastTick = utcNow;
}

Key Performance & Safety Notes

  • No Global Locks: We use per-company and per-user-hour locks, so contention is minimized—only threads accessing the same company/user/hour will block each other.
  • Idempotent DB Loads: Even if multiple threads hit the company lock at the same time, the GetOrAdd ensures only one instance of CompanyStats is stored. The per-company lock eliminates redundant database calls entirely.
  • Atomic Operations: Locking around IncrementAndUpdateTimestamp ensures we don't split the request count increment and timestamp update across threads.
  • Snapshot Iteration: Using ToList() on collection keys/values prevents exceptions when the collection is modified during iteration.

内容的提问来源于stack exchange,提问作者ThomasArdal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 12:52:42