我想实现一个程序,显示一些在控制台上随机移动的字符,每个字符都有不同的速度。
我创建了一个递归方法,在控制台上随机移动一个字母。当我想移动两个字母时,我使用两个线程调用相同的方法。
该程序在第一分钟完美运行,但过了一段时间后,这些字母开始出现在控制台的任何地方!
我确信我的递归方法没问题(我甚至尝试创建另一种方法,这次只使用一段时间(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);
}
}
}
答案 0 :(得分:5)
线程可能存在一百万个微妙问题 - 访问共享资源的任何必须被视为可疑。
考虑移动位置跟随放置字符不是原子,并且一个线程可以中断另一个线程导致移动 - 移动 - 放置场景。实际上情况实际上更糟而不是因为control sequences本身被发送到终端的多个字节所损害:因此控制序列本身可能变得腐败!
在终端访问周围使用关键区域防护(lock
)。 lock
应包含与彼此相关的所有必须原子(未中断)的操作:
lock (foo) {
move(...)
draw(...)
}
根据需要调整WriteAt
功能。
但是,请记住,即使有了这种变化,仍然一种微妙的竞争条件,请考虑:
有了上述内容,有可能(在特定时间)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个不同的角色,系统只使用少数几个线程。