只清除(刷新)一行,但要执行两个不同的任务



当我运行两个任务时,有时会在3行中写入数据(最多必须在2行中写入(,但我想知道为什么。它为什么这么做?我该怎么修?否则,如果我运行一个任务,它运行得很好我试图对代码进行注释。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace LifeBar
{
class Program
{
public static readonly int FullHealth = 200;
static void Main(string[] args)
{
List<Task> tasks = new List<Task>();
Console.Write("What is the player ID: "); int playerId = int.Parse(Console.ReadLine());
//playerID is equals with the line where is writed
var t1 = Task.Run(() =>
{
for (int i = FullHealth; i >= 0; i--)
{
WhatDraw(i, playerId);
System.Threading.Thread.Sleep(150);
}
});
//tasks.Add(t1);
//var t2 = Task.Run(() =>
//{
//    for (int i = 200; i >= 0; i--)
//    {
//        WhatDraw(i, playerId + 1); (+1 cus I would like to write it to the next line)
//        System.Threading.Thread.Sleep(200);
//    }
//});
//tasks.Add(t2);
Task.WaitAll(tasks.ToArray());
Console.ReadKey();
}

这是一个行删除程序

public static void ClearCurrentConsoleLine(int playerId)
{
Task.Factory.StartNew(() =>
{
Console.SetCursorPosition(playerId, Console.CursorTop);
Console.Write(new string(' ', Console.WindowWidth));
Console.SetCursorPosition(playerId, Console.CursorTop);
});
}

他们正在重新启动救生条

/// <summary>
/// Draw healt bar
/// </summary>
/// <param name = life> i </param>
/// <returns>Health bar value</returns>
static string DrawHealth(int life)
{
string health = String.Empty;
if (life == 0) return "Frank dead";
else if (life < 10) return "< 10";
for (int i = 0; i < life / 10; i++)
{
health += " |"; // 1 amout of | equal with 10 health
}
return health;
}
/// <summary>
/// Draw armour bar
/// </summary>
/// <param name="life"> (i)</param>
/// <returns>Armour bar value</returns>
static string DrawArmour(int life)
{
string armour = "| | | | | | | | | |";
for (int i = 0; i < life / 10; i++)
{
armour += " |:|"; // 1 amout of |:| equal with 10 armour
}
return armour;
}

这是一个依赖性

/// <summary>
/// Health or Armour draw
/// </summary>
/// <param name="fullLife">(i)</param>
/// <param name="playerId">playerId</param>
static void WhatDraw(int fullLife, int playerId)
{
Console.SetCursorPosition(0, playerId);
if (fullLife > 100)
{
double percent = fullLife / Convert.ToDouble(FullHealth);
Console.WriteLine($"Frank ({Math.Round(percent * 100)})% " + DrawArmour(fullLife - 100));
if (fullLife % 10 == 0)
{
Console.SetCursorPosition(playerId, Console.CursorTop-1);
ClearCurrentConsoleLine(playerId);
}
}
else
{
double percent = fullLife / Convert.ToDouble(FullHealth);
Console.WriteLine($"Frank ({Math.Round(percent * 100)}%) " + DrawHealth(fullLife));
if (fullLife % 10 == 0 && fullLife!=0)
{
Console.SetCursorPosition(playerId, Console.CursorTop-1);
ClearCurrentConsoleLine(playerId);
}
}
}
}
}

您正在达到比赛条件。考虑以下事件顺序:

// Thread 1
/* a */ Console.SetCursorPosition
/* b */
/* c */ Console.Write
// Thread 2
/* d */ Console.SetCursorPosition
/* e */
/* f */ Console.Write
  • a-运行第一个后台任务/线程并设置当前光标位置
  • b-它还没有运行下一个命令,所以我们可以考虑它"坐着;在线路CCD_ 3处
  • d-第二个任务设置光标位置
  • c-第一个任务写入控制台

好吧,第二个任务改变了位置!!Console类上的所有属性都是static,因此第一个任务不再写入移动光标的位置,而是写入第二个任务的位置。

时机可能更糟;例如,光标位置可以同时移动(ad同时执行(,或者第二任务可以在第一任务正在写入时移动光标位置(dc开始之后执行。如果同时调用两行cf,则可能会出现混乱的输出。

当然,这是一个简化的解释,当涉及并发时,它通常适用于Console类本身被同步以防止这些情况中的一些,但是当单个";操作";涉及多个命令(设置位置+写入(。这种协调由您来决定。

我们可以通过C#的lock语句(Monitor.Enter/Monitor.Exit的缩写(轻松实现这一点,以确保我们对控制台的操作不会重叠:

private static readonly object lockobj = new object();
public static void ClearCurrentConsoleLine(int playerId)
{
lock(lockobj) 
{
Console.SetCursorPosition(playerId, Console.CursorTop);
Console.Write(new string(' ', Console.WindowWidth));
Console.SetCursorPosition(playerId, Console.CursorTop);
} 
}
static void WhatDraw(int fullLife, int playerId)
{
lock(lockobj) 
{
Console.SetCursorPosition(0, playerId);
if (fullLife > 100)
{
double percent = fullLife / Convert.ToDouble(FullHealth);
Console.WriteLine($"Frank ({Math.Round(percent * 100)})% " + DrawArmour(fullLife - 100));
if (fullLife % 10 == 0)
{
Console.SetCursorPosition(playerId, Console.CursorTop-1);
ClearCurrentConsoleLine(playerId);
}
}
else
{
double percent = fullLife / Convert.ToDouble(FullHealth);
Console.WriteLine($"Frank ({Math.Round(percent * 100)}%) " + DrawHealth(fullLife));
if (fullLife % 10 == 0 && fullLife!=0)
{
Console.SetCursorPosition(playerId, Console.CursorTop-1);
ClearCurrentConsoleLine(playerId);
}
}
} 
}

因为我们需要针对Console同步操作集,所以我们需要在lock中包围所有这些操作。这将强制任何并发操作等待,直到资源不再使用为止。锁在同一线程上是可重入的,因此当WhatDraw具有锁时,它可以安全地调用ClearCurrentConsoleLine,而无需等待。一旦您锁定了对资源的访问,最佳实践规定您应该始终锁定它,即使是单个操作:

lock(lockobj) 
Console.WriteLine("Single write");

我还从ClearCurrentConsoleLine方法中删除了Task.Run,因为它毫无意义,并增加了不必要的复杂性,尤其是因为我们需要同步这些操作。请记住,Task是在稍后的某个时间点完成的承诺。到这种情况真正发生的时候,你可能真的不想再越界了!最好在必要时同步清除它。

还要注意,lock不能在async上下文中安全使用,您需要使用其他同步原语,如SemaphoreSlim

最新更新