C#里很多支持异步方法调用的函数;比如TCPClient的BeginReceive,BeginWrite等;
但不明白的是BeginReceive调用后,系统是如何调配线程来执行这个函数的;
一般接受数据的写法是:
clientSocket.BeginReceive(receiveDataBuffer, bytesReceived, MESSAGE_LENGTH_SIZE - bytesReceived,SocketFlags.None,newAsyncCallback(RecieveComplete), clientSocket);
而这个回调里会继续调用BeginReceive,以持续不断地接收从服务器发来的数据。
我想问的是这样每次都异步投递,线程的切换不是很频繁,会不会开销大?
对C#线程机制不熟悉,请指教。谢谢。
BeginReceive 会一直等,只到接收命令;线程很多可以用线程池管理,BeginReceive 等待的时占用资源很低的,我也刚刚学习,压力测试也是几千个
BeginXXX 会将 SOCKET 绑定到线程池,即调用 BindIoCompletionCallback,在新的 Threadpool API 之前,回调方法会由 IO 线程池中的线程来执行,这就是常说的 IOCP 特性。正因为如此,我们有一个针对 BeginXXX 之类的方法调用中的回调方法的优化建议,尽可能短小,不要包含阻塞式 IO 调用,尽量不要使用内核态同步数据结构。
线程切换的问题是针对整个进程而言的,准确的讲是上下文切换,对单个操作没有意义,因为虽然有可能异步投递的线程和回调线程不是同一个线程,但是只要回调线程没有被挂起(比如此线程一直在执行一个 100 的阶乘的运算,刚执行完毕就被分配来执行此回调函数),那么就不会产生上下文切换。
BeginXXX里传入的callback会在IOCP回调里被调用,即便里面有阻塞也是在IO线程吧,这会有影响?请指教。
@hailong: 我先问你个问题,有使用完成端口编写过线程池吗?
@Launcher: 没有,都是用封装好的库,如C++下boost::asio,libevent等
@hailong: 那我只能简单解释下,剩下的你需要补下完成端口和线程池的知识。当 IO 线程被阻塞后,它就会被挂起,如果此时完成端口上仍然有就绪的请求,并且没有达到完成端口的并发上线,那么线程池会创建一个新的 IO 线程来满足请求。设想下,当此类情况经常发生时,IO 线程池达到上限,此时所有 IO 线程都被挂起,就绪的请求就得不到满足,但是此时 CPU 却是无事可干,因为所有 IO 线程都在等待 IO 或者内核的唤醒。在新的 Threadpool API 中,系统不区分 IO 线程和工作线程,那么遇到这种情况就更麻烦,因为你甚至都没有可用的工作线程去处理计算任务。
@Launcher: 嗯,线程池我是理解,IO线程通常也不会阻塞,如果是beginReceived收到数据会push的队列,worker线程会不断检测处理。
@hailong: 通常会不会阻塞,不是系统提供的能力,而是你写的代码产生的影响,举个例子:
clientSocket.BeginReceive(xxxx,o=> {
clientSocket.Receive(); // 这句代码就会阻塞 IO 线程,就会产生上下文切换
// 或者像下面这样:
WriteFile(); // no overlapped.
// 或者这样:
SqlConnecton.Open();
SqlCommand.Execute();
} );
@Launcher: 我已经检测代码了,里面没有任何阻塞调用。像你举的例子都是阻塞IO调用,肯定会有问题。
@hailong:
clientSocket.BeginReceive(xxxx,o=> {
lock(obj){//dosomething;} // 视 dosomething 执行时间长短而定;
// 或者
WaitForXXXXObject();
});
@Launcher: 嗯,对内核对象的操作也有可能。
@hailong: 你的代码咋写的,我管不着,我只是给你解释这个问题“BeginXXX里传入的callback会在IOCP回调里被调用,即便里面有阻塞也是在IO线程吧,这会有影响?”的答案为什么是“有影响”。
@Launcher: 谢谢了!真是受益匪浅。另外问下,您主要在做C#开发吗?
@hailong: C++.
@Launcher: 好吧,我在做C++服务器开发,不过最近在看客户端代码,希望有所优化。谢谢您的帮助。