在javascript中,我们可以调用SetTimeout进行异步操作,而不必担心线程安全问题,因为javascript是单线程的,SetTimeout不会在新线程中执行代码块。
在 C# 中,我们可以使用 Task 类来使操作异步,如下所示。
Task.Factory.StartNew(()=> DoOperation());
但据我所知,DoOperation 可以发生在主线程或新线程中,并且 Task 不允许我们决定成为新线程并在同一线程中。
新的异步功能不等同于 SetTimeout。
如何在像 Javascript 这样的 C# 应用程序中实现完全相同的事情?有没有办法将控制台应用程序配置为单线程(我记得这样的事情)
编辑:考虑我正在处理的以下用例场景。它是简化得多的版本。
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 个任务访问无线程安全列表对象。我们当然可以像锁定一样以不同的方式处理线程安全问题,但在我的用例中,这是不必要的,效率低下,太多的锁定很难遵循并且很容易陷入死锁问题。
@Asad和其他人指出了正确的方向,所以谢谢大家。
如果您只想确保不超过一个线程正在运行(并且不一定所有线程都在同一线程上运行),则可以使用独占调度程序来执行此操作:
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
,并按时调用超时回调。
最后,以下是使用单线程异步模拟 JSsetTimeout
行为的方法:
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();
}
你想要的东西看起来很有趣,你可能错过了一些关于异步/等待的东西。话虽如此,您也许可以使用
TaskFactory.StartNew Method (Func, CancelToken, TaskCreationOptions, TaskScheduler)
并传递您自己的任务计划程序,例如
https://codereview.stackexchange.com/questions/43000/a-taskscheduler-that-always-run-tasks-in-a-specific-thread
不过,您希望使用此任务计划程序的所有可能导致并发问题的代码(因此不要在主线程上运行任何代码)。
如果您正在寻找以单线程方式发生的Tasks
,请查看TaskScheduler
。
替代方法包括设置SynchronizationContext
- 如果您使用的是 WPF 应用程序,这是因为访问起来要容易得多。获得后,只需调用Post
即可将调用发送到 UI/Main 线程的"下一个可用时间"。
听起来您的实际问题是如何以线程安全的方式实现生产者/消费者结构。看起来您尝试使用非线程安全的集合来实现这一点,并尝试使用来自 Javascript 的技术添加线程安全。
无需setTimeout
即可确保线程安全。在任何情况下,调用都等效于触发一次窗口计时器,它始终在应用程序的 UI 线程上执行。await Task.Delay()
内部还使用一次触发计时器,但确保在其原始上下文(在桌面应用程序(即 UI 线程)上恢复执行。
不过,如果可以让使用者独立工作,则不需要轮询。即使您确实想要轮询,也可以使用其他计时器类之一和 .NET对生产者/消费者的支持。
.NET 已经提供了几个生产者/消费者实现,甚至提供了 IProducerConsumerCollection 抽象来组合它们。
- BlockingCollection 是一个高级抽象,可以使用任何集合实现
IProducerConsumerCollection
。默认情况下,它使用 ConcurrentQueue 集合来确保线程安全,而无需锁定。生产者只是添加元素,而消费者则以阻塞方式读取项目。不需要轮询,因为使用者线程只是在等待时阻塞,并在发布新数据时立即响应。 - 如果你不喜欢阻塞使用者的想法,你可以直接在消费者端使用 ConcurrentQueue 和轮询。队列是无锁的,支持多个并发读取器和写入器。
- TPL 数据流库的活动块(如 ActionBlock)允许生产者将数据发布到块,并且该块确保使用一个或多个并发任务以安全的方式处理它们。默认情况下,操作块仅使用一个任务,尽管可以修改该任务。不需要轮询,因为 ActionBlock 将在数据可用时立即调用使用方法。代码可以像这样简单:
var myBlock=new ActionBlock(data=>DoSomething(data)); .... myBlock.Post(一些其他数据);
- 如果您不想使用 ActionBlock 进行处理,则可以将 BufferBlock 用作"仅"生产者/消费者缓冲区,以允许使用者独立于生产者读取数据。在这种情况下,您确实需要实现轮询,例如使用计时器
- 最坏的情况(不再需要)是使用锁定不安全的集合(如
List<T>
)来防止多个生产者和使用者同时修改集合。