在C#上使用多线程的问题

时间:2011-09-16 15:53:13

标签: c# multithreading recursion console

我想实现一个程序,显示一些在控制台上随机移动的字符,每个字符都有不同的速度。

我创建了一个递归方法,在控制台上随机移动一个字母。当我想移动两个字母时,我使用两个线程调用相同的方法。

该程序在第一分钟完美运行,但过了一段时间后,这些字母开始出现在控制台的任何地方!

我确信我的递归方法没问题(我甚至尝试创建另一种方法,这次只使用一段时间(i <100000)而不是递归。但是出现了同样的错误)。有人可以帮我这个吗?

非常感谢。

编辑:对不起,这是一个示例代码(不考虑如果字母占据相同位置会发生什么)。字母在'体育场'上移动,它们在x轴上移动20 - 51,在y轴上移动5 - 26。

public void WriteAt(string s, int x, int y)
    {
        try
        {
            Console.SetCursorPosition(x, y);
            Console.Write(s);
        }
        catch (ArgumentOutOfRangeException e)
        {
            Console.Clear();
            Console.WriteLine(e.Message);
        }

    }

    public void impresion()
    {
        int x = random.Next(20, 51);
        int y = random.Next(5, 26);
        WriteAt("A", x, y);
        imprimir("A", x, y, 80);
    }

    public void impresion2()
    {
        int x = random.Next(20, 51);
        int y = random.Next(5, 26);
        WriteAt("E", x, y);
        imprimir2("E", x, y, 20);
    }

    public void go()
    {
        Thread th1 = new Thread(impresion);
        Thread th2 = new Thread(impresion2);
        th1.Start(); //creates an 'A' that will move randomly on console
        th2.Start(); //creates an 'E' that will move randomly on console
    }

    public void imprimir(string s, int x, int y, int sleep)
    {
        Thread.Sleep(sleep);
        WriteAt(" ", x, y);
        int n = random.Next(1, 5);

        if (n == 1)
        {
            if ((x + 1) > 50)
            {
                WriteAt(s, x, y);
                imprimir(s, x, y, sleep);
            }
            else
            {
                WriteAt(s, x + 1, y);
                imprimir(s, x + 1, y, sleep);
            }
        }

        else if (n == 2)
        {
            if ((y - 1) < 5)
            {
                WriteAt(s, x, y);
                imprimir(s, x, y, sleep);
            }
            else
            {
                WriteAt(s, x, y - 1);
                imprimir(s, x, y - 1, sleep);
            }
        }

        else if (n == 3)
        {
            if ((x - 1) < 20)
            {
                WriteAt(s, x, y);
                imprimir(s, x, y, sleep);
            }
            else
            {
                WriteAt(s, x - 1, y);
                imprimir(s, x - 1, y, sleep);
            }
        }

        else
        {
            if ((y + 1) > 25)
            {
                WriteAt(s, x, y);
                imprimir(s, x, y, sleep);
            }
            else
            {
                WriteAt(s, x, y + 1);
                imprimir(s, x, y + 1, sleep);
            }
        }
    }

2 个答案:

答案 0 :(得分:5)

线程可能存在一百万个微妙问题 - 访问共享资源的任何必须被视为可疑。

考虑移动位置跟随放置字符不是原子,并且一个线程可以中断另一个线程导致移动 - 移动 - 放置场景。实际上情况实际上更糟而不是因为control sequences本身被发送到终端的多个字节所损害:因此控制序列本身可能变得腐败!

在终端访问周围使用关键区域防护(lock)。 lock应包含与彼此相关的所有必须原子(未中断)的操作:

lock (foo) {
   move(...)
   draw(...)
}

根据需要调整WriteAt功能。

但是,请记住,即使有了这种变化,仍然一种微妙的竞争条件,请考虑:

  1. A被清除。
  2. 绘制A(到E所在的位置)。
  3. E被清除(是刚刚绘制A的地方)。
  4. E被绘制。
  5. 有了上述内容,有可能(在特定时间)E将出现在屏幕上而A不会出现。也就是说,lock本身在保护对控制台的访问权限的同时,无法充分保护线程和控制台之间的交互。

    快乐的编码。


    有关一些一般提示和链接,另请参阅What are common concurrency pitfalls?

答案 1 :(得分:1)

关于锁定访问控制台的上一个答案将解决您当前的问题。

你真的不需要显式线程。您可以使用几个计时器和一些状态信息来完成。例如:

class CharState
{
    private static Random rnd = new Random();
    private object RandomLock = new object();
    public int x { get; private set; }
    public int y { get; private set; }
    public readonly char ch;
    public CharState(char c)
    {
       ch = c;
       SetRandomPos();
    }

    public void SetRandomPos()
    {
        lock (RandomLock)
        {
            // set x and y
        }
    }
}

随机数生成器在所有CharState个对象实例之间共享。它被SetRandomPos中的锁保护,因为如果多个线程同时调用Random.Next将失败。不要担心&#34;效率&#34;锁。这将花费你100纳秒。

现在,创建两个CharState实例和计时器来控制它们:

CharState char1 = new CharState('A');
CharState char2 = new CharState('X');

System.Threading.Timer timer1 = new System.Threading.Timer(
    MoveChar, char1, 1000, 1000);

System.Threading.Timer timer2 = new System.Threading.Timer(
    MoveChar, char2, 1200, 1200);

在这里,&#34; A&#34;将每秒移动一次,并且&#34; X&#34;将每1.2秒移动一次。

您的MoveChar功能变为:

void MoveChar(object state)
{
    CharState ch = (CharState)state;

    // erase the previous position
    WriteAt(" ", ch.x, ch.y);

    ch.SetRandomPos();
    WriteAt(ch.ch, ch.x, ch.y);
}

这种方法有很多好处。对于要移动的每个角色,您不需要单独的方法,并且可以以不同的速率移动每个角色。如果需要,可以扩展CharState类,为每个角色提供一个特定的移动区域。

您可以使用显式线程执行相同类型的操作,但计时器更易于使用,并且将消耗更少的系统资源。如果要移动10个不同的字符,则需要10个单独的线程,每个线程占用系统上的大量资源。这不好,特别是因为线程大部分时间都在睡觉 - 什么都不做。

另一方面,对于定时器,系统只需要处理旋转,因为只需要很多线程来处理并发请求。使用计时器,你可以移动100个不同的角色,系统只使用少数几个线程。