清除(刷新)只有一行,但在两个不同的任务中

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 所以第一个任务不再写入 移动光标的位置,但是而不是第二个任务完成的地方。

时机可能更糟;例如,光标位置可以同时移动ad同时执行)或第二个任务可以在第一个任务写入时移动光标位置(dc 开始后执行 但在 完成之前执行 ).如果同时调用 cf 行,您可能会看到乱码输出。

当然,这是一个简化的解释,在涉及并发时通常更适用。 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.