多线程、Socket
多线程
线程、进程和应用程序域
进程:进程是一个操作系统上的概念,用来实现多任务并发执行,是资源分配的最小单元,各个进程是相互独立的,可以理解为执行当中的程序,在操作系统中一般用一个称为PCB的结构体表示,里面存放了一些线程共用的、进程独立的数据;
应用程序域:是一个程序运行的逻辑区域,一个进程可以有多个应用程序域,一个应用程序域可以有多个线程,任一时刻一个线程只能运行在一个应用程序域中;
线程:进程因为包含了太多的数据,在做任务切换的时候非常消耗系统资源,所以就产生了线程,线程是操作系统进行任务调度的最少单元,是进程的子内容,一个进程可以有多个线程,各个线程之间共享进程里面的数据,线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的;
线程基础知识
- 线程的状态
线程在不同的时刻有不同的线程状态,ThreadState枚举用来标识一个线程的状态
- 线程的优先级
前面说线程是操作系统用来进行任务调度的最小单元,所以线程就必须有一些信息提供给操作系统,这些信息是操作系统进行任务调度的依据,一般会为线程设置优先级别,用来保证程序当中比较紧急和重要的任务能优先得到执行,ThreadPriority枚举用来设置线程的优先级别;
- 前后台线程
Thread有一个属性IsBackground,通过把此属性设置为true,就可以把线程设置为后台线程!这时应用程序域将在主线程完成时就被卸载,而不会等待异步线程的运行。一般将那些当主程序关闭后随之关闭的线程设置为后台线程,否则设置为前台线程;
.NET中实现多线程的几种方案
Thread类实现多线程
- 提供一个方法,该方法将作为线程的执行体
Public void ThreadTestMethod(object obj)
{
//方法体
}
- 创建Thread类实例
Thread th=new Thread(ThreadTestMethod); //该构造方法有四个形式的重载
- 两个委托
public delegate void ParameterizedThreadStart(object obj);
public delegate void ThreadStart();
ParameterizedThreadStart委托接受一个object类型的参数,这样我们就可以为线程执行的方法传递参数了,而ThreadStart委托不接受参数;
- 启动线程
Th.Start(null); 如果在实例化Thread类的实例的时候传递的是一个ParameterizedThreadStart类型的委托,则可以在启动一个线程的时候为线程的执行传递相关的参数,而如果是ThreadStart类型的委托则不能传递参数;
线程池ThreadPool实现多线程
向线程池中注册一个线程:
public static bool QueueUserWorkItem(WaitCallback callBack, object state);
WaitCallback委托:
public delegate void WaitCallback(object state);
state参数可以为线程传递参数;
简单的例子:
static void TestThreadMethod(object state) { while (true) { Thread.Sleep(1000); Console.WriteLine(Thread.CurrentThread.ManagedThreadId); Console.WriteLine(state.ToString()); } } bool isSuccess= ThreadPool.QueueUserWorkItem(TestThreadMethod ,"A test paramter");
|
异步委托
方式一:等待直至完成
声明委托类型:
delegate void TestDelegate(string para);
创建线程执行代码(函数):
static void TestDelegateMethod(string para)
{
Thread.Sleep(10000);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(para);
}
定义委托变量:
TestDelegate del = new TestDelegate(TestDelegateMethod);
调用:
IAsyncResult asynResult = del.BeginInvoke("Test Parameter", null, null);
//do something...
del.EndInvoke(asynResult);
说明:这是一种使用委托创建多线程应用程序的简单方式,但往往并不是最合适的方式,因为EndInvoke方法会使得主线程等待异步线程的执行完毕,也可以在BeginInvoke和EndInvoke之间写一些代码,但这样显得非常不灵活,因为我们不知道开启的异步线程什么时候执行完毕,所以,我们希望有这样的一种方式去执行异步委托:当异步线程执行完毕后,能主动调用我们提供的方法,这就是回调方式;
方式二:回调方式
与等待直至完成方式相比,前面定义委托类型、定义委托变量和指定线程执行代码的过程都是一致的,唯一不同的是在掉用的时候需要给BeginInvoke传递后两个参数(第三个参数也可以不传)。
BeginInvoke的参数说明:
第一部分参数由委托类型的参数决定,这里的委托TestDelegate只包含一个string类型的参数,所以BeginInvoke函数的第一个参数就是一个string类型的;
第二个参数是一个AsyncCallback类型的委托,原型为:
public delegate void AsyncCallback(IAsyncResult ar);通过传递一个符合该签名的函数名,则可以在异步线程执行完毕之后主动调用该函数;
在回调函数中获得委托变量的两种方式:
方式一:
del.BeginInvoke("Test Parameter", new AsyncCallback(TestCallBackFunction), null); static void TestCallBackFunction(IAsyncResult result) { Console.WriteLine("回到函数被执行了!"); TestDelegate del = (TestDelegate)result.AsyncState; del.EndInvoke(result); } |
方式二:
del.BeginInvoke("Test Parameter", new AsyncCallback(TestCallBackFunction), del); //将del传递给了第三个参数 static void TestCallBackFunction(IAsyncResult result) { Console.WriteLine("回到函数被执行了!"); TestDelegate del = (TestDelegate)(result as AsyncResult). AsyncDelegate; del.EndInvoke(result); } |
这种方式将委托变量del作为附加参数传递到BeginInvoke函数中,最终它会被封装在一个AsyncResult类型的变量中,并且该类型实现了IAsyncResult接口,随后当异步线程执行完毕调用回调函数的时候会将该变量传递给回调函数中(即result),这样就可以通过AsyncDelegate拿到委托了,当然,也可以将委托变量的作用域设置为更广的范围,这样在回调函数中就可以直接拿到委托对象了;
为什么要拿到委托变量?
因为拿到了委托变量(del)才能调用EndInvoke方法,那么为什么要调用EndInvoke方法?因为EndInvoke方法可以拿到异步线程执行函数的返回值,所以这就是为什么EndInvoke的返回类型与委托的返回类型一致了。
System.Threading.Timer类实现多线程
Timer类提供了一种简单的执行异步操作的方式,一般的用法如下:
static void TestTimerMethod(object para)
{
Console.Write(para.ToString());
}
//传递的方法名参数实际会被动态创建一个委托变量
Timer t = new Timer(TestTimerMethod, "*", 1000, 1000);
委托原型:public delegate void TimerCallback(object state);
使用的时候只需要调用Timer类的一个适当的构造函数重载,然后线程池就会通过委托变量指向的方法创建一个线程到等待队列,而该线程什么时候开始执行由第三个参数决定,每次执行的间隔由第四个参数决定,上面的示例中TestTimerMethod方法会在1秒后开始执行,然后每隔1秒再执行一次,每次执行都会将一个“*”字符串传递进去打印出来,所以,第二个参数是用来为异步线程的执行传递参数用的;
多线程的安全问题
跨线程访问控件的两种处理方式
当开启一个线程执行异步操作的时候如果在该线程中访问了不是该线程创建的控件时,会出现一个错误:线程间操作无效: 从不是创建控件“textBox1”的线程访问它。 解决这个问题有两种处理方式。
1.在窗体的构造函数中:Control.CheckForIllegalCrossThreadCalls = false;
这种简单的处理方式使得跨线程访问检查被忽略,这就不能保证程序安全执行;
2.让创建控件的线程去访问自己的控件
if (textBox1.InvokeRequired) { this.Invoke(new Action<TextBox, string>((t, s) => t.Text = s), textBox1, text.ToString()); } else { textBox1.Text = text.ToString(); } |
InvokeRequired属性获取控件被访问的时候是否需要Invoke调用,并且这里是调用的Form的Invoke方法,同样也可以使用TextBox控件自身的Invoke方法实现,原理是一样的,Invoke方法有多个形式的重载,这里是常见的一种,第一个参数传递的是一个泛型的Action,在构造这个Action的时候传递进去了一个Lambda表达式,Lambda表达式的代码修改了textBox1控件的Text属性;Invoke方法后面的参数是一个可变参数,这里为前面的委托(Action)提供所需要的参数;
锁lock
多线程带来了编程中的许多好处,使得程序可以在一个时间段内处理多个事情(宏观上),但是也提高了编程的复杂程度,多线程会带来许多问题,而造成问题的根本原因在于线程是“同时”执行的,所以解决问题的方法就是让它们在执行一些特定操作的时候不要同时执行了(这些操作会带来错误才这样处理),.NET中提供了LOCK的机制。
通过一个简单的示例演示一下LOCK的概念:
- 新建一个窗体应用程序,在默认打开的窗体上面拖出两个按钮;
- 添加一个私有的成员变量count:
private int count = 0;
- 第一个按钮的点击事件
private void button1_Click(object sender, EventArgs e) { for (int i = 0; i < 100; i++) { Thread th = new Thread(MultiThreadTest); th.Start(); } } |
在这个按钮的点击事件处理程序当中,启动了100个线程去执行MultiThreadTest指向的代码。
- MultiThreadTest函数
private void MultiThreadTest() { for (int i = 0; i < 100; i++) { Thread.Sleep(10); count++; } } |
这个函数中使成员变量count自增100,所以最终的count值应该是100*100=10000(其实并不是)
- 使用另一个按钮的事件处理程序显示当前count的值
private void button2_Click(object sender, EventArgs e)
{
MessageBox.Show(count.ToString());
}
上面说过100个线程都对count变量进行自增,所以理想的结果应该是count==10000,但是并不是这样,下面是对话框显示的最终线程执行完毕之后的结果:
"9996"
- 使用锁
.NET中实现线程锁很简单,只需要改变一行代码就可以得到正确的结果:
lock (this)
{
count++;
}
在访问count的时候这里其实做了两部操作:先读取出count的值,然后加1再赋值给count,问题就出现在这样的一个过程中,如果在读得count值的时候其它的线程已经修改了count的值,那么当前线程获得的就不是一个实际的、最新的值,也就是说本线程的赋值会覆盖掉这个过程中其它线程的++操作,于是我们加上了一个Lock语句;
显示的结果如下:
" 1000;"
- 锁的原理(自己的理解)
每一个引用类型的对象都有一个同步索引块,指示当前使用该对象的线程数,每个线程执行到Lock语句块的时候就会判断当前锁定项(这里是this,当前窗体对象)的同步索引块是否等于0(即没有线程在访问该变量),如果等于0则进入执行块,首先将同步索引块的索引加1,表示当前多了一个线程使用this,等lock块执行完成再将同步索引块中的索引值减1,使得其它线程能够继续访问,这样就相当于实现了一个排队机制,使得在适当的时候该串行执行的代码串行执行;
SOCKET网络编程
协议
1、 TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。
2、UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。
Socket概念
socket称作“套接字”,用于描述IP地址和端口,是一个通信链的句柄。
三类端口
- 公认端口(Well Known Ports):从0到1023,它们紧密绑定(binding)于一些服务。通常这些端口的通讯 明确表明了某种服务的协议。例如:80端口实际上总是HTTP通讯。
- 注册端口(Registered Ports):从1024到49151。它们松散地绑定于一些服务。也就是说有许多服务绑定于这些端口,这些端口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。
- 动态和/或私有端口(Dynamic and/or Private Ports):从49152到65535。理论上,不应为服务分配这些端 口。实际上,机器通常从1024起分配动态端口。但也有例外:SUN的RPC端口从32768开始。
Socket种类
1. 流式Socket(STREAM):是一种面向连接的Socket,针对于面向连接的TCP服务应用,安全,但是效率低;
2. 数据报式Socket(DATAGRAM):是一种无连接的Socket,对应于无连接的UDP服务应用.不安全(丢失,顺序混乱,在接收端要分析重排及要求重发),但效率高.
Socket一般应用模式(服务器端和客户端)
l 服务器端的Socket(至少需要两个),一个负责接收客户端连接请求(但不负责与客户端通信)每成功接收到一个客户端的连接便在服务端产生一个对应的负责通信的Socket,在接收到客户端连接时创建,为每个连接成功的客户端请求在服务端都创建一个对应的Socket(负责和客户端通信).
l 客户端的Socket,必须指定要连接的服务端地址和端口,通过创建一个Socket对象来初始化一个到服务器端的TCP连接。
Socket的通讯过程
服务器端:
1、 申请一个socket
2、 绑定到一个IP地址和一个端口上
3、 开启侦听,等待接授连接
客户端:
1、 申请一个socket
2、 连接服务器(指明IP地址和端口号)
3、 服务器端接到连接请求后,产生一个新的socket(端口大于1024)与客户端建立连接并进行通讯,原监听socket继续监听。
Socket的构造函数
连接通过构造函数完成。
public Socket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType)
如:mySocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);
Socket方法
- IPAddress类:包含了一个IP地址
- IPEndPoint类:包含了一对IP地址和端口号
- Socket (): 创建一个Socket
- Bind(): 绑定一个本地的IP和端口号(IPEndPoint)
- Listen(): 让Socket侦听传入的连接尝试,并指定侦听队列容量
- Connect(): 初始化与另一个Socket的连接
- Accept(): 接收连接并返回一个新的socket
- Send(): 输出数据到Socket
- Receive(): 从Socket中读取数据
- Close(): 关闭Socket (销毁连接)