ASP.NET 中的自定义多线程
ASP.NET 中的自定义多线程
或者“一个人痴迷于在 ASP.NET 中找到调用同步方法异步的方法”。
这适用于任何有兴趣在 ASP.NET 中探索 System.Threading Namespace
的人。有很多错误的做法,一种正确的方法,以及一种只能在别无选择时使用的方法。
首先,错误的做法。正如你可能知道也可能不知道,在 .NET 中,你可以通过创建一个委托方法并调用委托的 BeginInvoke
和 EndInvoke
方法来简单地异步调用任何同步方法。知道这一点,你可能会想在 ASP.NET 中尝试这样做。例如,假设你正在使用一个包含以下方法的预构建库对象,该方法执行同步的 WebRequest
。
private static readonly Uri c_UrlImage1 = new Uri(
@"http://www.asyncassassin.com/asyncassassin/image.axd?picture=2008%2f12%2fSlide3.JPG");
private HttpWebRequest request;
public string Result; // Public Variable to store the result where applicable
public string SyncMethod()
{
// prepare the web page we will be asking for
request = (HttpWebRequest)WebRequest.Create(c_UrlImage1);
// execute the request
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
return String.Format("Image at {0} is {1:N0} bytes", response.ResponseUri,
response.ContentLength);
}
然后你阅读了我关于 ASP.NET 中的多线程 的文章,并决定这应该是一个异步调用。如果你无法轻松访问该预构建库的源代码,你可能会想尝试这样做:
public delegate string AsyncMthodDelegate();
public IAsyncResult BeginUpdateByDelegate(object sender, EventArgs e, AsyncCallback cb,
object state)
{
AsyncMthodDelegate o_MyDelegate = SyncMethod;
return o_MyDelegate.BeginInvoke(cb, state);
}
public void EndUpdateByDelegate(IAsyncResult ar)
{
AsyncMthodDelegate o_MyDelegate = (
AsyncMthodDelegate)((AsyncResult)ar).AsyncDelegate;
Result = o_MyDelegate.EndInvoke(ar);
lblResult.Text = Result;
}
protected void Page_Load(object sender, EventArgs e)
{
RegisterAsyncTask(new PageAsyncTask(BeginUpdateByDelegate, EndUpdateByDelegate,
AsyncUpdateTimeout, null, false));
}
public void AsyncUpdateTimeout(IAsyncResult ar)
{
Label1.Text = "Connection Timeout";
}
纸面上看,这很棒。你只是将一个同步方法转换为一个异步方法,并使用 RegisterAsyncTask
调用它。尽管这种技术看起来很有希望,但我很抱歉地说,它对可伸缩性或性能没有任何帮助。不幸的是,BeginInvoke
使用的线程实际上是从 ASP.NET 处理页面请求使用的同一个工作线程池中获取的。因此,当你调用 BeginUpdateByDelegate
时,你实际上是将主线程返回到线程池,从同一个线程池中获取第二个线程来调用 BeginInvoke
,然后阻止该线程直到调用 EndInvoke
,然后将第二个线程返回到线程池。ASP.NET 然后从同一个池中拉出第三个线程来调用 EndUpdateByDelegate
,并完成请求。净增益:0 个线程,而且你的代码更难阅读、调试和维护。
好的。使用 ThreadPool.QueueUserWorkItem()
怎么样?你再次重写代码,现在看起来像这样:
public IAsyncResult BeginThreadPoolUpDate(object sender, EventArgs e,
AsyncCallback cb, object state)
{
AsyncHelper helper = new AsyncHelper(cb, state);
ThreadPool.QueueUserWorkItem(ThreadProc, helper);
return helper;
}
public void EndThreadPoolUpDate(IAsyncResult AR)
{
AsyncHelper helper = (AsyncHelper)AR;
// If an exception occurred on the other thread, rethrow it here
if (helper.Error != null)
{
throw helper.Error;
}
// Otherwise retrieve the results
Result = (string)helper.Result;
lblResult.Text = Result;
}
protected void Page_Load(object sender, EventArgs e)
{
RegisterAsyncTask(new PageAsyncTask(BeginThreadPoolUpDate, EndThreadPoolUpDate,
AsyncUpdateTimeout, null, false));
}
public void AsyncUpdateTimeout(IAsyncResult ar)
{
Label1.Text = "Connection Timeout";
}
class AsyncHelper : IAsyncResult
{
private AsyncCallback _cb;
private object _state;
private ManualResetEvent _event;
private bool _completed = false;
private object _lock = new object();
private object _result;
private Exception _error;
public AsyncHelper(AsyncCallback cb, object state)
{
_cb = cb;
_state = state;
}
public Object AsyncState
{
get { return _state; }
}
public bool CompletedSynchronously
{
get { return false; }
}
public bool IsCompleted
{
get { return _completed; }
}
public WaitHandle AsyncWaitHandle
{
get
{
lock (_lock)
{
if (_event == null)
_event = new ManualResetEvent(IsCompleted);
return _event;
}
}
}
public void CompleteCall()
{
lock (_lock)
{
_completed = true;
if (_event != null)
_event.Set();
}
if (_cb != null)
_cb(this);
}
public object Result
{
get { return _result; }
set { _result = value; }
}
public Exception Error
{
get { return _error; }
set { _error = value; }
}
}
}
哎呀,这次需要一个辅助类来提供 IAsyncResult Interface
。现在你的代码更加难以理解了,我很抱歉地告诉你,ThreadPool.QueueUserWorkItem
使用的线程也来自 ASP.NET 处理请求所使用的同一个线程池。
好吧。我将使用 Thread.Start()
来创建自己的线程。嗯,你可以做到——通过创建自己的线程,你不会从 ASP.NET 线程池中窃取一个。但没那么快!如果你确定需要出于可伸缩性原因使此方法异步,那么该页面将承受很大的负载。仔细想想。如果你每当页面被请求时都创建一个新线程,并且你的页面每秒受到 1000 次请求的冲击,那么你几乎同时创建了 1000 个新线程,它们都在争夺 CPU 时间。显然,使用 Thread.Start()
会有不受约束的线程增长的风险,你很容易创建如此多的新线程,以至于增加的 CPU 争用实际上会降低而不是提高可伸缩性,因此我不建议在 ASP.NET 中使用 Thread.Start()
。
所以,你在网上搜寻其他方法。在 System.Threading Namespace
的深处,你找到了 ThreadPool.UnsafeQueueNativeOverlapped
方法,该方法承诺为你打开一个 I/O 完成端口线程,该线程从 I/O 线程池中抽取,专门供你运行你的方法。所以你再次修改你的代码,并勾选“允许不安全代码”复选框进行重新编译。现在它看起来是这样的:
public IAsyncResult BeginIOCPUpDate(object sender, EventArgs e, AsyncCallback cb,
object state)
{
AsyncHelper helper = new AsyncHelper(cb, state);
IOCP.delThreadProc myDel = SyncMethod;
IOCP myIOCp = new IOCP(myDel);
try
{
myIOCp.RunAsync();
}
catch (Exception ex)
{
helper.Error = ex;
}
finally
{
helper.CompleteCall();
}
return helper;
}
public void EndIOCPUpDate(IAsyncResult AR)
{
AsyncHelper helper = (AsyncHelper)AR;
// If an exception occurred on the other thread, rethrow it here
if (helper.Error != null)
{
throw helper.Error;
}
// Otherwise retrieve the results
Result = (string)helper.Result;
lblResult.Text = Result;
}
protected void Page_Load(object sender, EventArgs e)
{
RegisterAsyncTask(new PageAsyncTask(BeginIOCPUpDate, EndIOCPUpDate,
AsyncUpdateTimeout, null, false));
}
public void AsyncUpdateTimeout(IAsyncResult ar)
{
Label1.Text = "Connection Timeout";
}
class IOCP
{
public delegate string delThreadProc();
private readonly delThreadProc _delThreadProc;
public IOCP(delThreadProc ThreadProc)
{
_delThreadProc = ThreadProc;
}
public void RunAsync()
{
unsafe
{
Overlapped overlapped = new Overlapped(0, 0, IntPtr.Zero, null);
NativeOverlapped* pOverlapped = overlapped.Pack(IocpThreadProc, null);
ThreadPool.UnsafeQueueNativeOverlapped(pOverlapped);
}
}
unsafe void IocpThreadProc(uint x, uint y, NativeOverlapped* p)
{
try
{
_delThreadProc();
}
finally
{
Overlapped.Free(p);
}
}
}
class AsyncHelper : IAsyncResult
{
// Code omitted for clarity: see above for the full AsyncHelper Class
}
它似乎可行,但你怎么能确定呢?你在各个点添加以下代码进行测试:
Label1.Text += "<b>EndIOCPUpDate</b><br />";
Label1.Text += "CompletedSynchronously: " + AR.CompletedSynchronously +
"<br /><br />";
Label1.Text += "isThreadPoolThread: " +
System.Threading.Thread.CurrentThread.IsThreadPoolThread.ToString() + "<br />";
Label1.Text += "ManagedThreadId : " +
System.Threading.Thread.CurrentThread.ManagedThreadId + "<br />";
Label1.Text += "GetCurrentThreadId : " + AppDomain.GetCurrentThreadId() + "<br />";
Label1.Text += "Thread.CurrentContext : " +
System.Threading.Thread.CurrentContext.ToString() + "<br />";
int availWorker = 0;
int maxWorker = 0;
int availCPT = 0;
int maxCPT = 0;
ThreadPool.GetAvailableThreads(out availWorker, out availCPT);
ThreadPool.GetMaxThreads(out maxWorker, out maxCPT);
Label1.Text += "--Available Worker Threads: " + availWorker.ToString() + "<br />";
Label1.Text += "--Maximum Worker Threads: " + maxWorker.ToString() + "<br />";
Label1.Text += "--Available Completion Port Threads: " +
availCPT.ToString() + "<br />";
Label1.Text += "--Maximum Completion Port Threads: " + maxCPT + "<br />";
Label1.Text += "===========================<br /><br />";
你发现,虽然它确实在 I/O 线程上执行,但它也使用了或阻塞了一个工作线程,也许是为了运行 delegate
方法。无论如何,净增益是 -1
个线程——至少在前两种技术中,你只使用了工作线程。现在你同时使用了工作线程和 I/O 线程,并且使用了不安全代码,你的代码看起来更糟糕了!
所以,你思考了一会儿,决定使用自定义线程池。也许你还记得 Wintellect.com 的人曾经在他们的 PowerThreading Library 中提供过免费的自定义线程池。你在存档中找到了一个副本并设置好了。(代码看起来就像上面的 Threadpool.QueueUserWorkItem
代码,但它使用的是自定义线程池。你添加了你的代码进行验证,是的,它能完成你需要的所有工作。可用工作线程 = 最大工作线程,可用 I/O 线程 = 最大 I/O 线程。没有线程从 ASP.NET 中被窃取,而且你也不冒不受约束的线程增长的风险。除了找到一种方法将异步方法添加到预构建的 DLL 中(获取并修改源代码 - 联系作者/供应商要求在其库中添加异步调用)或切换到你自己的代码,或切换到另一个已支持异步方法的库之外,这是你唯一的选择。
在 ASP.NET 中进行异步编程的唯一正确方法是找到一种方法为你的库添加真正的异步功能。如果这是不可能的,那么自定义线程池就是你唯一的选择。现在,在你急忙前往 wintellect.com 下载这个神奇的解决方案之前,我必须警告你,自定义线程池不再是 Power Threading Library 的一部分。好奇心驱使我联系了作者( Jeffrey Richter 先生),他告诉我,他删除了自定义线程池,因为他认为它会促进糟糕的编程实践。他解释说,随着 3.x 框架的引入,线程的默认数量已大大增加(当然,你可以在 machine.config 中增加它),但最终,如果你的库不支持异步 I/O 调用,你的应用程序将无法扩展。
“如果它们不支持异步操作,那么它们就不能用于可伸缩的应用程序——就这样……老实说,我不会使用一个[库],除非我使用它的情况是我知道我总是只有几个客户端。”
我鼓励你在控制台应用程序和 Windows Forms 程序中自由使用异步委托和 ThreadPool.QueueUserWorkItem
,只是不要在 ASP.NET 中使用它们,因为那是在浪费时间。
好了。就是这样。我用来测试上述每种技术所使用的线程池线程的示例代码可以在这里下载。该示例包括一个 Wintellect's Power Threading Library 的版本,其中仍然包含 Custom Threadpool - 在 bin 文件夹中查找它。
注意:我建议你在 IIS 6.0 或 IIS 7.0 下进行测试:在 Casini Web 服务器上运行或在 Windows XP 下的 IIS 5.1 上运行会显示不一致的结果。