清除(刷新)只有一行,但在两个不同的任务中
Clear(refresh) just one line but in two different tasks
当我 运行 有 2 个任务时,有时会在 3 行中写入数据(必须始终最多写入 2 行)但我不知道为什么。它为什么要这样做?我该如何解决?否则,如果我 运行 有 1 个任务,它运行良好。 我试着评论代码。
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
- 你的第一个背景 task/thread 运行s 并且它设置当前光标位置
b
- 它还不是 运行 下一个命令,所以我们可以认为它“坐在”第 b
. 行
d
- 第二个任务设置光标位置
c
- 第一个任务写入控制台。
嗯,第二个任务换位置了!! Console
class 上的所有属性都是 static
所以第一个任务不再写入 它 移动光标的位置,但是而不是第二个任务完成的地方。
时机可能更糟;例如,光标位置可以同时移动(a
和d
同时执行)或第二个任务可以在第一个任务写入时移动光标位置(d
在 c
开始后执行 但在 完成之前执行 ).如果同时调用 c
和 f
行,您可能会看到乱码输出。
当然,这是一个简化的解释,在涉及并发时通常更适用。 Console
class 本身是同步的以防止其中一些情况,但是当单个“操作”涉及多个命令(设置位置 + 写入)时它会 not/cannot。这种协调由您决定。
我们可以使用 C# lock
statement (a shorthand for 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
.
当我 运行 有 2 个任务时,有时会在 3 行中写入数据(必须始终最多写入 2 行)但我不知道为什么。它为什么要这样做?我该如何解决?否则,如果我 运行 有 1 个任务,它运行良好。 我试着评论代码。
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
- 你的第一个背景 task/thread 运行s 并且它设置当前光标位置b
- 它还不是 运行 下一个命令,所以我们可以认为它“坐在”第b
. 行
d
- 第二个任务设置光标位置c
- 第一个任务写入控制台。
嗯,第二个任务换位置了!! Console
class 上的所有属性都是 static
所以第一个任务不再写入 它 移动光标的位置,但是而不是第二个任务完成的地方。
时机可能更糟;例如,光标位置可以同时移动(a
和d
同时执行)或第二个任务可以在第一个任务写入时移动光标位置(d
在 c
开始后执行 但在 完成之前执行 ).如果同时调用 c
和 f
行,您可能会看到乱码输出。
当然,这是一个简化的解释,在涉及并发时通常更适用。 Console
class 本身是同步的以防止其中一些情况,但是当单个“操作”涉及多个命令(设置位置 + 写入)时它会 not/cannot。这种协调由您决定。
我们可以使用 C# lock
statement (a shorthand for 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
.