C#:阻塞功能调用,直到满足条件

12
我正在开发一个C# Winforms应用程序,其中一部分是使用AsyncUpload将文件上传到Web服务器(需要使用进度回调),在C#程序中,我有一个简单的for循环调用上传函数。
 for(int i=0;i < 10 ; i++)
{
  Uploadfun();
}

这个函数有一些神奇的效果:

Uploadfun()
  { 
  // Logic comes here

   // webClient.UploadFileAsync runs a 2nd thread to perform upload .. 
   webClient.UploadFileAsync(uri, "PUT", fileNameOnHD);  

 }

当异步上传完成时调用的回调函数

Upload_Completed_callback()
{
  //Callback event
}

编辑

逻辑序列:

  1. 从循环中调用了Fun函数。
  2. 执行并完成Fun函数的逻辑。
  3. 返回到for循环。
  4. 当UploadFileAsync(在另一个线程中运行某些逻辑)结束时,最终将调用回调函数。

问题出现在第三步,当执行返回到for循环时,我需要阻止循环继续执行,直到回调被调用。


3
你能访问“fun”实现吗?你应该考虑提供一个同步接口,以实现异步 API。 - Mehrdad Afshari
从你的描述来看,你的代码似乎是顺序执行且单线程的,这意味着在callback函数被调用之前,循环不会继续执行。如果我理解有误,请给出更详细的解释。 - Darin Dimitrov
@Madi:那你的做法是反过来的。你应该在逻辑的核心使用UploadFile的同步版本,并在其上使用异步API(如果需要)。 - Mehrdad Afshari
1
@Mehrdad:我实际上被迫使用Async版本,因为它提供了“进度”回调,这是一个要求。 - Madi D.
4个回答

20

如果我理解正确,你想调用UploadFileAsync,然后阻塞直到异步调用命中你的回调函数。如果是这样,我会使用AutoResetEvent

private readonly AutoResetEvent _signal = new AutoResetEvent(false); 

fun()
  { 
  // Logic comes here

   // runs a 2nd thread to perform upload .. calling "callback()" when done
   webClient.UploadFileAsync(uri, "PUT", fileNameOnHD);  

   _signal.WaitOne();   // wait for the async call to complete and hit the callback     
 }



callback()
 {
   //Callback event
   _signal.Set(); // signal that the async upload completed
 }

使用AutoResetEvent意味着在调用Set并且等待的线程通过WaitOne接收到信号后,状态会自动重置。


在 uploadAsync 后使用 Waitone() 时,程序会停止,并且回调函数不会被调用。 - Madi D.
好的,我解决了停机问题,在单独的线程中调用了fun()函数,“显然”在主线程中调用它会导致问题!@Juliet帮助指出了问题..谢谢你们两个=) - Madi D.

4

C#方法默认是阻塞的,所以你不需要做任何事情。我假设由于某种原因,你正在调用一个非阻塞方法,该方法启动了一个后台任务/线程/其他,并在完成时给出回调。你想以同步方式调用此异步方法。

你可以从回调函数中调用fun。以下是伪代码示例:

int n;

callFunTenTimes()
{
    n = 0;
    fun(n);
}

callback()
{
    ++n;
    if (n < 10)
       fun(n);
    else
       print("done");
}

这类似于传递续延风格
使用此方法的优点是,您可以使方法异步,而无需添加任何额外的线程、锁或逻辑 - 您只需提供一个回调函数,客户端就可以订阅它。在事件驱动环境中表现良好。

很有趣。我一直在思考自己的问题,沿着这些线路:让回调函数执行循环以前所做的“主要工作”。这个解决方案似乎将等待时间最小化。 - Joe
我需要异步版本,因为它提供了一个“进度”回调函数,这是一个核心要求。 - Madi D.
@Madi D:如果上传必须异步执行,为什么不将您的函数也设为带有回调的异步函数呢?您能详细解释一下这个函数的使用场景吗?这是一个WinForms应用程序、ASP.NET还是其他什么? - Mark Byers
我编辑了问题并回答了你的问题,还添加了一些额外的信息。 - Madi D.
+1 鼓励我学习新概念,并提供了另一种解决方案! - Madi D.

3

Zebrabox使用WaitHandle的方法是正确的。 虽然Juliet的解决方案可行,但执行自旋等待的线程会消耗大量处理器资源,与此同时,WaitHandle实际上处于空闲状态。


它不会消耗太多处理器资源,但可能会阻止系统将CPU置于更深的空闲状态,从而在笔记本电脑上更快地耗尽电池。 - Ben Voigt

1

问题在这里:

for(int i=0;i < 10 ; i++)
{
  fun(); <-- if we block until this function finishes here, we stop the UI thread
}

你正在进行的是顺序操作。如果你不能阻塞UI线程,就将循环移出UI线程:

volatile downloadComplete;

void DownloadUpdates()
{
    ThreadPool.QueueUserWorkItem(state =>
        for(int i = 0; i < 10; i++)
        {
            downloadComplete = false;
            webClient.UploadFileAsync(uri, "PUT", fileNameOnHD);
            while(!downloadComplete) { Thread.Sleep(1); }
        });
}

Upload_Completed_callback()
{
    downloadComplete = true;
}

现在,您可以阻止循环的执行而不会停止 UI 线程,并且您还可以从 webclient 类中获得进度指示器的好处。

不要使用布尔变量来实现这个,编译器会将其优化掉(这会导致 OP 提到的无法继续执行的行为)。zebrabox 展示了正确的方法,使用可等待事件对象。 - Ben Voigt
1
@Ben:感谢您的评论,但我认为它们是误导性的。首先,只要布尔值被使用或分配到某个地方(并且可能是),编译器就不会将其优化掉。其次,旋转等待布尔值和重置事件都是阻塞线程的有效方法,两者都是“正确的方法”,没有“错误”的选择之分,选择哪种方法取决于程序员的喜好。 - Juliet
非常感谢您提供如此有用的答案,最终我将您的线程解决方案与@zebrabox的resetEvent解决方案结合起来,现在它已经可以正常工作了.. =) - Madi D.
@朱丽叶,根据.NET内存模型的规则,以下两个代码是完全等价的:while(!downloadComplete) { Thread.Sleep(1); }和if (!downloadComplete) { while (true) { Thread.Sleep(1); } }由于循环不会改变变量,编译器可以将测试移到循环外。我看到你现在尝试将downloadComplete声明为volatile时添加了语法错误。如果你修复声明,它将防止编译器执行我提到的优化,但它仍然是轮询,比事件更糟糕,因为有许多原因。 - Ben Voigt
@Ben:感谢您的评论,但是while(!downloadComplete) { Thread.Sleep(1); }if (!downloadComplete) { while (true) { Thread.Sleep(1); } }在任何内存模型中都*不等价。您能否提供一些MSDN文档或代码示例,演示轮询如何编译成您提供的代码(无论是否使用volatile)? - Juliet
@朱丽叶:这是一个非常有信息量的讨论,涉及Java语言,但是在C#中也适用(因为该优化在顺序一致性下是合法的,而且Java和.NET CLR内存模型都比它更弱): http://java.sun.com/docs/books/jls/third_edition/html/memory.html特别要注意该页面的第17.9节。 - Ben Voigt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接