1

I'm trying to learn about threads for an assignment for school, and I'm trying to get two threads to empty a collection. The code I came up with so far throws an exception, saying that the collection got modified.

First I had a while loop in the locked code part, but then (of course ;-)) only one thread empties the collection.

My question is, how can I have a loop in which the threads both take turns in emptying the collection?

class Program
{
    private static List<int> containers = new List<int>();

    static void Main(string[] args)
    {
        for (int i = 0; i < 100; i++)
        {
            containers.Add(i);
        }

        Thread t1 = new Thread(() => { foreach (int container in containers) { GeefContainer(); } });
        t1.Name = "Kraan 1";
        t1.Start();

        Thread t2 = new Thread(() => { foreach (int container in containers) { GeefContainer(); } });
        t2.Name = "Kraan 2";
        t2.Start();

        Console.Write("Press any key to continue...");
        Console.Read();
    }

    static void GeefContainer()
    {
        lock (containers)
        {
            int containerNummer = containers.Count - 1;

            //Container container = containers[containerNummer];

            //Console.Write("Container {0} opgehaald... Overladen", containerNummer);
            Console.WriteLine("Schip: Container {0} gegeven aan {1}", containerNummer, Thread.CurrentThread.Name);

            //Gevaarlijk, want methode aanroepen kan klappen
            containers.RemoveAt(containerNummer);
        }
    }
}
4

4 回答 4

2

我假设您不允许使用 System.Collections.Concurrent 命名空间中的任何 ThreadSafe 集合。

在检查是否还有条目时,您需要获得对容器集合的独占访问权限。但是,您不希望 1 个线程在释放其锁定之前采取独占控制删除所有条目。Monitor.Pulse 可用于允许等待锁定容器的其他线程“先行”。尝试 GeefContainers 的以下实现:

static void GeefContainer()
{
    lock (containers)
    {
        while (containers.Any()) // using linq, similar to: while(container.Count > 0)
        {
            containers.RemoveAt(0); // remove the first element

            // allow other threads to take control
            Monitor.Pulse(containers); // http://msdn.microsoft.com/en-us/library/system.threading.monitor.pulse.aspx
                            // Wait for a pulse from the other thread
                            Monitor.Wait(container);
        }
    }
}

哦,并从以下位置删除您的循环逻辑:

Thread t2 = new Thread(() => { foreach (int container in containers) { GeefContainer(); } });

只需调用 GeefContainer 就足够了。

这可以通过以下方式可视化:

  • 线程 1 获得对“集合”的锁定
  • 线程 2 被阻塞,因为它正在等待“集合”的独占锁
  • 线程 1 从“集合”中删除一个条目
  • 线程 1 释放它对“集合”的锁定并尝试获得新的独占锁定
  • 线程 2 获得对“集合”的锁定
  • 线程 2 从 'collections' 中删除一个条目
  • 线程 2 释放它对“集合”的锁定并尝试获得新的排他锁
  • 线程 1 获得对“集合”的锁定

ETC

于 2012-05-07T09:57:39.067 回答
1

您看到的异常是由枚举器抛出的。标准集合上的枚举器进行检查以确保集合没有在枚举操作中间被修改(foreach在您的情况下是通过)。

由于您希望让线程交替从集合中删除,因此您将需要某种机制来允许线程相互发送信号。我们还必须注意不要同时从多个集合中访问该集合。如果没有同步,即使该Count属性也不能安全使用。Barrier类使信号发送变得非常容易。一个简单lock的同步就足够了。这是我将如何做到这一点。

public class Program
{
    public static void Main(string[] args)
    {
        var containers = new List<int>();

        for (int i = 0; i < 100; i++)
        {
            containers.Add(i);
        }

        var barrier = new Barrier(0);

        var t1 = new Thread(() => GeefContainers(containers, barrier));
        t1.Name = "Thread 1";
        t1.Start();

        var t2 = new Thread(() => GeefContainers(containers, barrier));
        t2.Name = "Thread 2";
        t2.Start();

        Console.Write("Press any key to continue...");
        Console.Read();
    }

    private static void GeefContainers(List<int> list, Barrier barrier)
    {
        barrier.AddParticipant();
        while (true)
        {
            lock (list)
            {
                if (list.Count > 0)
                {
                    list.RemoveAt(0);
                    Console.WriteLine(Thread.CurrentThread.Name + ": Count = " + list.Count.ToString());
                }
                else
                {
                    break;
                }
            }
            barrier.SignalAndWait();
        }
        barrier.RemoveParticipant();
    }

}

Barrier课程基本上导致这种情况一遍又一遍地发生。

|----|                 |----|                 |----|
| T1 |-->|         |-->| T1 |-->|         |-->| T1 |
|----|   |         |   |----|   |         |   |----|
         |-->(B)-->|            |-->(B)-->|         
|----|   |         |   |----|   |         |   |----|
| T2 |-->|         |-->| T2 |-->|         |-->| T2 |
|----|                 |----|                 |----|

在上图中T1T2分别表示线程 1 和 2 上的删除操作。(B)表示对 的调用Barrier.SignalAndWait

于 2012-05-07T14:10:53.307 回答
0

首先,更改您的 thred 定义如下:

new Thread(() => { while(containers.Count>0) { GeefContainer(); } });

然后,重写 GeefContainer() 如下以避免异常:

static void GeefContainer()
{
    lock (containers)
    {
        int containerNummer = containers.Count - 1;

        if(containerNummer>=0) 
        {
            //Container container = containers[containerNummer];

            //Console.Write("Container {0} opgehaald... Overladen", containerNummer);
            Console.WriteLine("Schip: Container {0} gegeven aan {1}", containerNummer, Thread.CurrentThread.Name);

            //Gevaarlijk, want methode aanroepen kan klappen
            containers.RemoveAt(containerNummer);
        }
    }
}
于 2012-05-07T09:47:49.643 回答
0

如果你修改你的线程如下?这样,两个线程都应该有一些时间来对集合执行操作。

Thread t1 = new Thread(() => { 
        while (containers.Count > 0)
        {
            GeefContainer(); 
            Thread.Sleep(150);
        }});
t1.Name = "Kraan 1";
t1.Start();

Thread t2 = new Thread(() => { 
        while (containers.Count > 0)
        {
            GeefContainer(); 
            Thread.Sleep(130);
        }});
t2.Name = "Kraan 2";
t2.Start();
于 2012-05-07T09:44:22.490 回答