如何实现对列表的并发,线程安全访问?

时间:2018-01-14 15:22:27

标签: c# multithreading

如果我在锁定内部以安全的方式获得了对象引用,当锁定开始使用它而没有同步时,该怎么办?锁定释放后,其他线程的映像无法使用此对象。是否可以保证我在处理器数据中没有陈旧或缓存?在我获得锁定之前,我会看到其他线程所做的所有更改吗?一般来说,它是线程安全的吗?

让我展示一些代码。

private List<string> list1 = new List<string>();
private List<string> list2 = new List<string>();
private List<string> list;
private bool flagList;

private readonly object locker = new object();

// in constructor
list = list1;
flagList = true;

public void Write(string data)
{
    lock (locker)
    {
        list.Add(data);
    }
}

// guaranteed non-reentrant code
private void TimerElapsed(object obj)
{
    List<string> l;
    lock (locker)
    {
        l = list;
        list = (flagList) ? list2 : list1;
        flagList = !flagList;
    }
    // process l Count/items here, then clear
    ...
    l.Clear();
    // restart timer
}

因此,TimerElapsed保证每次只运行一次,Write可以随时被多个不同的线程调用。问题是:我是否会始终看到自l以来所有新增内容TimerElapsed

2 个答案:

答案 0 :(得分:0)

因此,如果这是您的问题,Write将不会添加任何项目,而您位于TimerElapsed中的锁定语句块内。 您只需使用一个列表即可简化代码。

    private List<string> list = new List<string>();

    private readonly object locker = new object();

    public void Write(string data)
    {
        lock (locker)
        {
            list.Add(data);
        }
    }

    // guaranteed non-reentrant code
    private void TimerElapsed(object obj)
    {
        List<string> copy;
        lock (locker)
        {
            copy = list;
            list = new List<string>();
        }

        // process copy
    }

答案 1 :(得分:0)

在您的示例中,l.Clear()和您释放锁定后的其他处理。这意味着,在执行l.Clear()时,另一个线程可能已获得锁并开始执行代码。

由于你已经释放了锁,当你正在处理另一个线程时可以调用Write。计时器可能再次经过,另一个线程可以获取锁,获取对同一列表的引用,并开始处理它。这可能会发生多次。 (您不希望对计时器间隔的长度与处理所需的时间做出任何假设。)

您真正需要的是集合本身是线程安全的,而不是访问List线程安全。否则,正如您所说,任何其他获得“安全”引用的线程都可以随时随地为该对象做任何事情。

一种解决方案是使用ConcurrentQueue。这允许您在单独的线程上安全地插入和删除,并在内部为您处理锁定。

public class UsesConcurrentQueue
{
    private readonly ConcurrentQueue<string> _queue = new ConcurrentQueue<string>();

    public void Write(string data)
    {
        _queue.Enqueue(data);
    }

    private void TimerElapsed(object obj)
    {
        string dataToProcess = null;
        while(_queue.TryDequeue(out dataToProcess))
        {
            // process each string;
        }
    }
}

现在,当您将一个项目添加到队列中时,您不必关心另一个线程是否正在从同一队列中读取。

当计时器过去并开始处理时,您将继续处理,直到队列为空。您可以在处理过程中继续向队列中添加项目,这样就可以了。

如果您需要确保不在两个线程上处理队列(假设在第一次完成处理之前计时器再次过去),那么您可以在输入该方法时停止计时器并在您重新启动时重新启动计时器退出。

或者假设您有理由为什么要处理项目列表而不是一次处理一个项目,就像您将它们作为批处理一样处理。然后你可以这样做:

    private void TimerElapsed(object obj)
    {
        string dataToProcess = null;
        var list = new List<string>();
        while (_queue.TryDequeue(out dataToProcess))
        {
            list.Add(dataToProcess);
        }
        // Now you've got a list.
    }

现在你有一个List项,但它是一个局部变量,而不是一个类级字段。所以你可以对它进行操作,甚至将它发送到另一个方法,而不必担心其他一些线程会修改它。