初步谈谈 C# 多线程、异步编程与并发服务器

时间:2022-11-11 08:14:26

多线程与异步编程可以达到避免调用线程异步阻塞作用,但是两者还是有点不同。

多线程与异步编程的异同

1.线程是cpu 调度资源和分配的基本单位,本质上是进程中的一段并发执行的代码。

2.线程编程的思维符合正常人的思维习惯,线程中的处理程序依然是顺序执行,所以编程起来比较方便,但是缺点也是明显的,多线程的使用会造成多线程之间的上下文切换带来系统花销,并且共享变量之间也是会造成死锁的问题。

3.因为异步操作无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码可以减少 共享变量的数量),减少了死锁的可能。当然异步操作也并非完美无暇。编写异步操作的复杂程度较高,程序主要使用回调方式进行处理,与普通人的思维方式有些出入,而且难以调试。

适用范围

  在了解了线程与异步操作各自的优缺点之后,我们可以来探讨一下线程和异步的合理用途。我认为:当需要执行I/O操作时,使用异步操作比使用线程+同步 I/O操作更合适。I/O操作不仅包括了直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及.net Remoting等跨进程的调用。

  而线程的适用范围则是那种需要长时间CPU运算的场合,例如耗时较长的图形处理和算法执行。但是往往由于使用线程编程的简单和符合习惯,所以很多朋友往往会使用线程来执行耗时较长的I/O操作。这样在只有少数几个并发操作的时候还无伤大雅,如果需要处理大量的并发操作时就不合适了。

二、多线程编程

刚好这段时间在看网络编程,在这里就结合多线程和网络编程,实现能够应对多客户端请求的服务端。Socket 类的 Accept() 方法一直等待,直到有客户端连接请求。Socket 网络编程可以参考C# 网络编程之 Socket 编程 。

C# 中有专门的异步网络编程方法,具体可以参考 几种Socket服务器模型比较。

多线程实现一个并发服务器的例子:

static Socket client;
static void Main(string[] args)
{
IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 9060); //创建 IPEndPoint 对象,表示接受任何端口 9050 的客户机地址
Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // TCP
server.Bind(ipep); //绑定
server.Listen(20); //监听
Console.WriteLine("正在监听...");

//下面这段代码阻塞,可以用新线程执行,但可能会出问题
while (true)
{
client = server.Accept(); //收到客户端请求

//开新线程发送数据
Thread recvthread = new Thread(sendData);
recvthread.IsBackground = true; //后台线程
// 启动消息服务线程
recvthread.Start();
///也可以开其他线程,如接收数据线程
  }
}

static private void sendData()
{
if (client != null)
{
Console.WriteLine("客户机" + client.RemoteEndPoint + "连接到服务器");
string data = "hello client";
byte[] msg = System.Text.Encoding.ASCII.GetBytes(data); //将 string 转化为 byte 数组
client.Send(msg); //向客户端发生数据
Console.WriteLine("发生数据:" + data);
client.Close();
}
}

三、异步编程

基于异步的编程方法有三种:

  • 异步编程模式(APM),其中异步操作要求 Begin 和 End 方法(例如,异步写操作的 BeginWrite 和 EndWrite)。对于新的开发工作不再建议采用此模式。
  • 基于事件的异步模式 (EAP) 需要一个具有 Async 后缀的方法,还需要一个或多个事件、事件处理程序、委托类型和 EventArg 派生的类型,在 .NET Framework 2.0 版中引入的。对于新的开发工作不再建议采用此模式。
  • 基于任务的异步模式 (TAP),该模式使用一个方法表示异步操作的启动和完成。.NET Framework 4 中引入了 TAP,并且是 .NET Framework 中异步编程的建议方法。

Task 异步,有以下三种方法创建 Task:

  • Task.Factory.StartNew,比较常用。

  • Task.Run,是.net 4.5中增加的。

  • Task.FromResult,如果结果是已计算,就可以使用这种方法来创建任务。

下面就以 Task.Factory.StartNew 进行异步编程实现一个并发服务器的例子:

static Socket client;
static void Main(string[] args)
{
IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 9060); //创建 IPEndPoint 对象,表示接受任何端口 9050 的客户机地址
Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // TCP
server.Bind(ipep); //绑定
server.Listen(20); //监听
Console.WriteLine("正在监听...");

//下面这段代码阻塞,可以放到子线程执行,但是可能会出现问题,可以看最后面分析
while (true)
{
client = server.Accept();
//创建并启动 task
Task.Factory.StartNew(() =>
{
sendData(); //一个没有返回值的方法
});
}
}

static private void sendData()
{
if (client != null)
{
Console.WriteLine("客户机" + client.RemoteEndPoint + "连接到服务器");
string data = "hello client";
byte[] msg = System.Text.Encoding.ASCII.GetBytes(data); //将 string 转化为 byte 数组
client.Send(msg); //向客户端发生数据
Console.WriteLine("发生数据:" + data);
client.Close();
}
}

使用 Async 与 Await 进行异步编程

static Socket client;
static Socket server;
static void Main(string[] args)
{
IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 9060); //创建 IPEndPoint 对象,表示接受任何端口 9050 的客户机地址
server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // TCP
server.Bind(ipep); //绑定
server.Listen(20); //监听
Console.WriteLine("正在监听...");

accept();
}
static async void accept()
{
await acceptAsync();
}
static async Task acceptAsync() //异步接受请求
{
while (true)
{
client = server.Accept(); //收到客户端请求
await sendData();
}
}
static async Task sendData() //异步发生数据
{
if (client != null)
{
Console.WriteLine("客户机" + client.RemoteEndPoint + "连接到服务器");
string data = "hello client";
byte[] msg = System.Text.Encoding.ASCII.GetBytes(data); //将 string 转化为 byte 数组
client.Send(msg); //向客户端发生数据
//添加一个异步方法
await Task.Delay(1000);
Console.WriteLine("发生数据:" + data);
client.Close();
}
}

由于 Main() 函数不能设置为 async 模式,所以增加了一个accept 函数,使用 await 来执行异步操作 acceptAsync() ,等待接受客户端的请求。同时在异步操作 acceptAsync() 中执行异步 sendData() ,异步发送数据。

一个问题:在上面的三个程序中,采用多线程和Task.Factory.StartNew 实现服务端的两个程序,如果把下面两段代码作为子线程或者异步函数执行,本来是阻塞的函数 client = server.Accept(),却没有等待客户端连接,直接执行过去了??是因为 accept()在同时在静态函数和多线程中的关系????(因为在非静态函数中这样并没有问题),但是使用Async 与 Await 执行的异步函数却能正常执行。

while (true)
{
client = server.Accept(); //收到客户端请求

//开新线程发送数据
Thread recvthread = new Thread(sendData);
recvthread.IsBackground = true; //后台线程
// 启动消息服务线程
recvthread.Start();
///也可以开其他线程,如接收数据线程
}

while (true)
{
client = server.Accept();
//创建并启动 task
Task.Factory.StartNew(() =>
{
sendData(); //一个没有返回值的方法
});
}