异步C#app就像Javascript

时间:2015-05-17 01:15:17

标签: c# multithreading asynchronous

在javascript中,我们可以调用SetTimeout进行异步操作,而不必担心线程安全问题,因为javascript是单线程的,而SetTimeout不会在新线程中执行代码块。

在C#中,我们可以使用Task类来使操作异步,如下所示。

Task.Factory.StartNew(()=> DoOperation());

但据我所知,DoOperation可以在主线程或新线程中发生,而Task不会让我们决定成为新线程并在同一个线程中。

新的异步功能不等同于SetTimeout。

如何在C#app中实现与Javascript完全相同的东西?有没有办法将Console App配置为单线程(我记得这样的事情)

修改 考虑下面我正在处理的用例场景。它是简化版本。

class Program
{
    static BufferBlock<int> queue = new BufferBlock<int>();
    static List<int> list = new List<int>();
    static void Main(string[] args)
    {
        Task.WhenAll(Produce(), Consume()).ContinueWith(r=> Console.WriteLine("list count: "+list.Count));

        Console.ReadKey();
    }

    public static async Task Produce()
    {
        var random = new Random();
        for (int i = 0; i < 1000; i++)
        {
            var value = random.Next();
            await queue.SendAsync(value);
            await Task.Delay(random.Next(1, 4));

            list.Add(value);//manipulate none thread safe object
            Console.WriteLine("value produced " + value);
        }

        queue.Complete();
    }

    public static async Task Consume()
    {
        var random = new Random();
        while (await queue.OutputAvailableAsync())
        {
            var value = await queue.ReceiveAsync();

            //consume the value
            await Task.Delay(random.Next(1, 4));

            list.Remove(value);//manipulate none thread safe object

            Console.WriteLine("value consumed " + value);
        }
    }
}

基本上是生产者/消费者模式和生产者添加值,消费者使用这些值。我们通过这个作业操作List对象,它不是线程安全的。

在任务列表结束时,计数应为0但不是因为2个任务访问无线程安全的List对象。我们肯定可以像锁定一样处理线程安全问题,但在我的用例中它是不必要的,低效的,太多锁定很难遵循并且很容易陷入死锁问题。

@Asad和其他人指出了正确的方向,所以感谢大家。

4 个答案:

答案 0 :(得分:2)

如果您只想确保运行的线程不超过一个(并且不一定所有内容都在同一个线程上运行),您可以使用独占调度程序来执行此操作:

static void Main(string[] args)
{

    var sch = new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler;
    var tf = new TaskFactory(sch);

    var t = tf.StartNew(() => Run()).Result;
    t.Wait();
}

static async Task Run()
{
    var start = DateTime.UtcNow;
    var t = Task.Delay(1000).ContinueWith(_ =>
    {
        Console.WriteLine("I tried to log after 1 second, ended up logging after {0}", (DateTime.UtcNow - start).TotalSeconds);
    });
    Console.WriteLine("It has not yet been 1 second. I will hog the only thread available to demonstrate how I simulate JS behavior.");
    Thread.Sleep(2000);
    await t;
}

我正在使用ConcurrentExclusiveSchedulerPair.ExclusiveScheduler创建一个新的任务工厂,我将用它来运行我的主程序入口点。

如果你运行这个程序,你会注意到即使日志被安排了1秒,它也被阻止了,因为一个线程已经没有做任何事情(这正是JS的行为)。删除Sleep并按时调用超时回调。

最后,以下是使用单线程异步模拟JS setTimeout行为的方法:

static async Task JSSetTimeout(int ms, Action callback, int waitResolution = 10)
{
    var startTime = DateTime.UtcNow;
    while ((DateTime.UtcNow - startTime).TotalMilliseconds < ms)
    {
        await Task.Delay(waitResolution);
    }

    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    callback();
}

答案 1 :(得分:1)

你想要的东西看起来真的很有趣,你可能错过了关于async / await的东西。话虽这么说,你也许可以使用

TaskFactory.StartNew Method (Func, CancellationToken, TaskCreationOptions, TaskScheduler)

并传递您自己的任务计划程序,例如

https://codereview.stackexchange.com/questions/43000/a-taskscheduler-that-always-run-tasks-in-a-specific-thread

您希望使用此任务调度程序的所有代码都可能导致并发问题(因此不要在主线程上运行任何代码)。

答案 2 :(得分:0)

如果您正在寻找以单线程方式发生的Tasks,请查看TaskScheduler

替代方法包括设置SynchronizationContext - 如果您正在使用WPF应用程序,这是因为更容易访问。获得后,只需致电Post即可将您的通话发送至&#34;下一个可用时间&#34; UI /主线程。

答案 3 :(得分:0)

听起来你的实际问题是如何以线程安全的方式实现生产者/消费者结构。您似乎尝试使用不是线程安全的集合来实现此功能,并尝试使用来自Javascript的技术添加线程安全性。

不需要setTimeout来确保线程安全。在任何情况下,该调用都等同于一次windows timer,它总是在应用程序的UI线程上执行。 await Task.Delay()还在内部使用了一次启动计时器,但确保在其原始上下文中恢复执行 - 在桌面应用程序上,即UI线程。

如果您可以让消费者独立工作,那么轮询是不必要的。即使您确实需要轮询,也可以使用其他计时器类和.NET对生产者/消费者的支持来实现它。

.NET已经提供了几个生产者/消费者实现,甚至提供了IProducerConsumerCollection抽象来组合它们。

  1. BlockingCollection是一个高级抽象,可以使用任何实现IProducerConsumerCollection的集合。默认情况下,它使用ConcurrentQueue集合来确保线程安全而不锁定。生产者只需添加元素,而消费者以阻塞方式读取项目。不需要轮询,因为消费者线程在等待时只是阻塞,并在发布新数据时立即响应。
  2. 如果您不喜欢阻止消费者的想法,您可以直接使用ConcurrentQueue并在消费者方面进行投票。该队列是无锁的,支持多个并发读写器。
  3. ActionBlock这样的TPL数据流库的活动块允许生产者将数据发布到块,并且块确保使用一个或多个并发任务以安全的方式处理它们。默认情况下,ActionBlock只使用一个任务,尽管可以修改它。不需要轮询,因为当数据可用时,ActionBlock会立即调用消费方法。代码可以简单如下:
  4.     var myBlock=new ActionBlock(data=>DoSomething(data));
        ....
        myBlock.Post(someOtherData);
    
    1. 如果您不想使用ActionBlock进行处理,可以使用BufferBlock作为&#34;只需&#34;生产者/消费者缓冲区,允许消费者独立于生产者读取数据。在这种情况下,您需要实现轮询,例如使用计时器
    2. 最糟糕的情况是,不再需要的是在List<T>之类的不安全集合周围使用锁定,以防止多个生产者和消费者同时修改集合。