静态类的线程安全性

4

我希望在C#的Windows应用程序中创建自己的事件系统。为此,我编写了以下类:

internal class EventManager
{
    private static List<EventRecord> s_listEvents = new List<EventRecord>();

    public static void AddEvent(EventRecord record)
    {
        record.EventDate = DateTime.Now;
        s_listEvents.Add(record);
    }

    public static List<EventRecord> GetRecordsByDate(DateTime date)
    {
        var r = (from l in s_listEvents
                 where l.EventDate >= date
                 select l).ToList<EventRecord>();
        return r;
    }
}

我希望确认EventManager类是线程安全的,因为我的应用程序将同时创建数百个线程。所有线程很可能会使用此类来生成事件。并且当不同线程调用AddEvent函数时,GetRecordsByDate函数可能会从类外部被调用。

简单地说,您能否告诉我这个设计是否适用于多线程窗口应用程序?如果这不是线程安全的,那么我该如何使我的类或其成员线程安全?我应该使用同步对象锁定整个EventManager类,还是应该使用读写锁来锁定我的s_listEvents静态成员?

5
成百上千的线程并不理想。即使被阻塞时,线程的成本也相对较高。 - Roman Starkov
我的应用程序需要通过TCP从远程机器收集数据。而且这个收集操作必须在给定的时间内完成。所以我必须创建许多线程。我可以通过硬件负载均衡(例如使用多台服务器)来减少线程的数量,但无论如何,我的应用程序都将是多线程应用程序。 - Fer
为什么需要创建自己的事件系统? - Alexandre Vinçon
听起来你正在采用“每个连接1个线程”的模型(从你关于tcp的评论中)。众所周知,这种模型不具备良好的可扩展性。 - Damien_The_Unbeliever
@Damien_The_Unbeliever 是的,我每个 TCP 连接使用一个线程。实际上,我使用 TCP 库的异步方法,并且不为每个连接创建一个线程,而是使用异步方法使用线程池来使用线程。我认为它们有相同的意思。你喜欢哪种方法? - Fer
显示剩余2条评论
5个回答

3

不要使用 List<T>,而应该使用 ConcurrentBag<T>

ConcurrentBag 是一个线程安全的集合实现,针对同一线程既生产又消费存储在集合中的数据进行了优化。

更多信息:

http://msdn.microsoft.com/en-us/library/dd381779.aspx

此外,请注意创建访问线程的数量,超过100个线程会降低性能,因为切换上下文需要时间。

编辑:对于.NET 3.5,您可以使用简单的 lock 来实现线程安全。

internal class EventManager
{
    private static List<EventRecord> s_listEvents = new List<EventRecord>();
    private static object _syncObject = new object();


    public static void AddEvent(EventRecord record)
    {
        record.EventDate = DateTime.Now;
        lock(_syncObject)
        {
           s_listEvents.Add(record); 
        }

    }

    public static List<EventRecord> GetRecordsByDate(DateTime date)
    {
        lock (_syncObject)
        {
             var r = (from l in s_listEvents
                 where l.EventDate >= date
                 select l).ToList<EventRecord>();

             return r;
        }

    }
}

编辑:

根据您的情况,如果您非常频繁地读取数据,则在整个应用程序中使用 ReaderWriterLockSlim 结合 ReaderWriterLock 更好,因为它允许多个线程读取数据。

如果不是,使用 lock 通常具有更好的性能。

参见链接:

http://blogs.msdn.com/b/pedram/archive/2007/10/07/a-performance-comparison-of-readerwriterlockslim-with-readerwriterlock.aspx


谢谢,但是 ConcurrentBag 在 .NET Framework 3.5 上似乎无法工作。由于我正在使用 Framework 3.5,所以不能使用 ConcurrentBag 的实现。我应该自己实现这个逻辑。 - Fer
@Dmitry,哪一个更理想,ReaderWriterLock 还是锁定 _syncObject 对象? - Fer
1
@Fer:ReaderWriterLock 在 lock 上并没有太多的性能优势,使用 lock 会更简单和易读。 - cuongle

2
您可以使用ReaderWriterLock类来实现此功能,具体请参考这里
internal class EventManager
{
    static ReaderWriterLock rwl = new ReaderWriterLock();

    private static List<EventRecord> s_listEvents = new List<EventRecord>();

    public static void AddEvent(EventRecord record)
    {
        record.EventDate = DateTime.Now;
        rwl.AcquireWriterLock(0);
        try
        {
            s_listEvents.Add(record);
        }
        finally
        {
            rwl.ReleaseWriterLock();
        }
    }

    public static List<EventRecord> GetRecordsByDate(DateTime date)
    {
        rwl.AcquireReaderLock(0);
        try
        {
            var r = (from l in s_listEvents
                     where l.EventDate >= date
                     select l).ToList<EventRecord>();
            return r;
        }
        finally
        {
            rwl.ReleaseReaderLock();
        }
    }
}

2
由于这个类是静态的,因此您应该锁定s_listEvents成员。调用者可能没有访问共享锁对象的机会,除非您将锁定作为EventManager本身(或任何其他静态类)的静态成员可用。如果是这种情况,您可以直接在EventManager中实现对s_listEvents的访问锁定。这样,您就可以避免调用者忘记获取锁的问题。
读/写锁似乎是一个不错的选择。

我不确定 ReaderWriter 锁在这里是最好的选择。写入者非常快,读取者要慢得多。它需要进行测试。 - H H

1
以下链接可能会有所帮助:

如何使一个类线程安全

private object _lock;

public static void AddEvent(EventRecord record)
{
    lock (_lock)
    {
        record.EventDate = DateTime.Now;
        s_listEvents.Add(record);
    }
}

1
你问题的最基本答案如下:为了使你的解决方案线程安全,你必须保护数据存储不受同时访问。这可以通过在任何访问列表的点上锁定列表来完成。这意味着当你遍历列表、添加或删除其中的元素时,必须锁定该区域。
尽管你要访问那么多服务器,但你可能不想生成100多个线程,而是想使用线程池,详情请参见http://msdn.microsoft.com/en-us/library/0ka9477y(v=vs.90).aspx。这将为你提供一个线程池,用于简单的“签入-下载数据-签出”任务,就像你描述的那样。
在编写多线程应用程序时,考虑底层存储的使用模式非常重要。如果您的应用程序每秒执行数百个添加操作,您可能需要考虑拥有一个只读副本的底层数据结构,这样每次按日期获取记录时就不会阻塞整个系统。有关详细介绍,请参见Intel's Optimization Guide

考虑拥有一个只读的数据结构副本在性能方面看起来不错。我会阅读并思考你的建议。谢谢。 - Fer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接