C# 并发任务和每个任务的不同光标位置

C# 并发任务和每个任务的不同光标位置,c#,task,C#,Task,我开始练习任务,并尝试了以下代码: static void Main() { Task.Factory.StartNew(() => { Write('a', 0); }); var t = new Task(() => { Write('b', 10); }); t.Start(); Write('c', 20); Console.ReadLine(); } static

我开始练习任务,并尝试了以下代码:

static void Main()
{
    Task.Factory.StartNew(() =>
    {
        Write('a', 0);
    });

    var t = new Task(() =>
    {
        Write('b', 10);
    });

    t.Start();

    Write('c', 20);
    Console.ReadLine();
}

static void Write(char c, int x)
{
    int yCounter = 0;
    for (int i = 0; i < 1000; i++)
    {
        Console.WriteLine(c);
        Console.SetCursorPosition(x, yCounter);
        yCounter++;
        Thread.Sleep(100);
    }
}
static void Main()
{
Task.Factory.StartNew(()=>
{
写('a',0);
});
var t=新任务(()=>
{
写('b',10);
});
t、 Start();
写('c',20);
Console.ReadLine();
}
静态无效写入(字符c,整数x)
{
int yCounter=0;
对于(int i=0;i<1000;i++)
{
控制台写入线(c);
控制台。设置光标位置(x,Y计数器);
yCounter++;
睡眠(100);
}
}

我的想法是看看控制台将如何在三个不同的列之间输出不同的字符。它确实交换列,但不输出正确的字符。例如,在第一列中,它只需要输出“a”,但也输出“b”和“c”,其他两列也是如此。

这可能是使用任务的一个特别糟糕的示例,或者是如何糟糕地使用任务的示例

在任务中,您正在设置全局状态(
SetCursorPosition
),这当然会影响其他任务(
Console
毕竟是静态的)。有可能

Console.WriteLine('b')
在光标设置为
0
、设置为
10
或设置为
20
后调用,反之亦然。任务不应依赖于任何可能已更改的全局(或类级别)状态(除非该值可能已更改的任务没有问题)。关于您的示例,您必须确保在编写输出之前,没有任何其他任务调用
SetCursorPosition
。实现这一点的最简单方法是锁定任务

private static object lockObject = new object(); // you need an object of a reference type for locking

static void Write(char c, int x)
{
    int yCounter = 0;
    for (int i = 0; i < 1000; i++)
    {
        lock(lockObject)
        {
            Console.SetCursorPosition(x, yCounter);
            Console.Write(c);
        }

        yCounter++;
        Thread.Sleep(100);
    }
}
private static object lockObject=new object();//锁定时需要引用类型的对象
静态无效写入(字符c,整数x)
{
int yCounter=0;
对于(int i=0;i<1000;i++)
{
锁定(锁定对象)
{
控制台。设置光标位置(x,Y计数器);
控制台。写入(c);
}
yCounter++;
睡眠(100);
}
}

lock
确保一次不会有两个任务进入块(假定lock对象是完全相同的),因此每个任务都可以将光标设置到它要写入的位置,并写入其字符,而无需任何其他任务将光标设置到任何其他位置。(另外,我已经交换了
Write
SetCursorPosition
,因为在写入输出之前,我们必须调用
SetCursorPosition
——无论如何,如果不交换这两行,锁就没用了。)

除了Paul的回答之外

如果您正在处理任务和
异步
/
等待
,请不要以任何方式混合
任务
线程

使用
Task.Run
/
Task.Start执行
Write
方法称为“异步过同步”。这是一种不好的做法,应该避免

以下是您的代码,以异步方式重写,并使用异步同步:

class Program
{
    static void Main(string[] args)
    {
        var asyncLock = new AsyncLock();

        // we need ToList here, since IEnumerable is lazy, 
        // and must be enumerated to produce values (tasks in this case);
        // WriteAsync call inside Select produces a "hot" task - task, that is already scheduled;
        // there's no need to start hot tasks explicitly - they are already started
        new[] { ('a', 0), ('b', 10), ('c', 20) }
            .Select(_ => WriteAsync(_.Item1, _.Item2, asyncLock))
            .ToList();

        Console.ReadLine();
    }

    static async Task WriteAsync(char c, int x, AsyncLock asyncLock)
    {
        for (var i = 0; i < 1000; i++)
        {
            using (await asyncLock.LockAsync())
            {
                Console.SetCursorPosition(x, i);
                Console.Write(c);
            }

            await Task.Delay(100);
        }
    }
}
类程序
{
静态void Main(字符串[]参数)
{
var asyncLock=new asyncLock();
//我们需要在这里列出,因为IEnumerable是懒惰的,
//并且必须枚举以生成值(本例中为任务);
//Select中的WriteAsync调用生成一个“热”任务-任务,该任务已被调度;
//没有必要显式启动热任务-它们已经启动了
新[]{('a',0),('b',10),('c',20)}
.Select(=>WriteAsync(0.Item1,0.Item2,异步锁))
.ToList();
Console.ReadLine();
}
静态异步任务WriteAsync(char c、int x、AsyncLock AsyncLock)
{
对于(变量i=0;i<1000;i++)
{
使用(等待asyncLock.LockAsync())
{
控制台。设置光标位置(x,i);
控制台。写入(c);
}
等待任务。延迟(100);
}
}
}

AsyncLock
生活在软件包中。

谢谢!我理解这可能是一种不好的任务使用方式,我只是想了解更多关于它们的知识,并给出一些有趣的例子:)但我理解你的观点,我以前听说过锁,我认为这可能是相关的!非常感谢。它工作正常,但我注意到它总是以完全相同的顺序执行任务,总是先是“c”,然后是“a”,然后是“b”,因为这种情况总是发生,这肯定是有原因的,而不仅仅是一个随机过程。你知道原因吗?(我在外观中移动了thread.sleep,这就是我看到任务执行顺序的方式)