Android --- 消息机制和异步任务
在Android中,只有在UIThread(主线程)中才能直接更新界面,
在Android中,长时间的工作联网都需要在workThread(分线程)中执行
在分线程中获取服务器数据后,需要立即到主线程中去更新UI来显示数据,
所以,如何实现线程间的通信(消息机制)?
消息机制原理
消息机制原理 尚硅谷
handler发送消息到 MessageQueue 消息队列中,消息队列内部是一个链表的结构,Looper会取出消息队列中待处理的消息,调用handler的dispatchMessage()分发消息,handler中处理消息的方法有三种,最常用的是第三种
Message的callback
handler的callback
handler的handleMessage方法
消息机制相关API
Message 消息
可理解为线程间通信的数据单元,可通过message携带需要的数据
创建对象:Message.obtain()、new Message()
Message.obtain()使用了Message中的消息池,比直接new一个对象更高效
常用参数:
- what: id标识,用以区分来自哪个线程
- arg1/arg2: 子线程需要向主线程传递整型参数
- obj: Object 任意对象
Handler 处理器
handler并不是只能用在处理message上,定时循环调度等工作也能使用它。
Handler 是Message 的处理器,也负责消息的发送和移除工作。
- 发送即时消息
public final boolean sendMessage(@NonNull Message msg),实际上调用了sendMessageDelayed()发送一个延迟时间为0的消息public final boolean sendMessage(@NonNull Message msg) { return sendMessageDelayed(msg, 0); } public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) { if (delayMillis < 0) { delayMillis = 0; } return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis); }
- 发送延时消息,并不是指延时发送,而是延时处理
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis)public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) { if (delayMillis < 0) { delayMillis = 0; } return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis); }
- 发送空消息
public final boolean sendEmptyMessage(int what) { return sendEmptyMessageDelayed(what, 0); } public final boolean sendEmptyMessageDelayed(int what, long delayMillis) { Message msg = Message.obtain(); msg.what = what; return sendMessageDelayed(msg, delayMillis); } public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) { if (delayMillis < 0) { delayMillis = 0; } return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis); }
不管是发送什么消息,最后都要调用 sendMessageAtTime 方法
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) { MessageQueue queue = mQueue; if (queue == null) { RuntimeException e = new RuntimeException( this + " sendMessageAtTime() called with no mQueue"); Log.w("Looper", e.getMessage(), e); return false; } return enqueueMessage(queue, msg, uptimeMillis); } private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { msg.target = this; msg.workSourceUid = ThreadLocalWorkSource.getUid(); if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis); }
- 处理消息 (回调方法) 在主线程中执行
public interface Callback { /** * @param msg A {@link android.os.Message Message} object * @return True if no further handling is desired */ boolean handleMessage(@NonNull Message msg); }
- 移除未处理消息,比如发送的延时消息:让消息队列调用 removeMessages 方法移除消息
public final void removeCallbacks(@NonNull Runnable r) { mQueue.removeMessages(this, r, null); }
MessageQueue 消息队列
它是一个按Message的when(被处理时间)排序的优先级队列,用来存放通过 Handler 发送的消息。
实例
创建Handler对象,并重写 handleMessage 方法
在主/分线程创建Message对象,利用handler对象发送消息
在 handleMessage 中处理消息
-
创建Handler对象,并重写 handleMessage 方法
只要 handle 发了消息,就一定会触发 handleMessage 方法,并传入一个 Message 对象,根据Message 对象的what属性判断属于哪个线程。
public class HandleActivity extends AppCompatActivity {
TextView textView ;
String httpDate = "";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handle);
}
// 利用 handller 处理
// 1.实例化一个 Handler
Handler handler = new Handler(
// 只要 handle 发了消息,就一定会触发 handleMessage 方法,并传入一个 Message 对象
new Handler.Callback() {
@Override
// 3.由handle对象接收消息并处理,在Handler的内部类中处理
public boolean handleMessage(@NonNull Message msg) {
Log.e("handler 处理消息", "这是handler发送的消息,此时是空");
// 根据 Message 的 what 属性,区分来源于哪个线程
if (msg.what == 1) {
textView1 = findViewById(R.id.t1);
textView1.setText(httpDate);
} else if (msg.what == 2) {
textView2 = findViewById(R.id.t2);
textView2.setText(httpDate);
} else if (msg.what == 3) {
textView3 = findViewById(R.id.t3);
textView3.setText("msg.what = "+msg.what+"\n"+msg.obj.toString());
}
return false;
}
}
);
}
- 在主/分线程创建Message对象,利用handler对象发送消息
当消息没有包含数据时,可以使用handler.sendEmptyMessage(int what) 发送一个空消息
new Thread() {
@Override
public void run() {
super.run();
getHttp();
// 当 handler 需要传送数据时,需要使用到 Message,使用 sendMessage 方法
Message message = new Message();
message.what = 3;
DataBean dataBean = new DataBean(1,"157181@qq.com","ybr","scl","asiudh");
message.obj = dataBean;
handler.sendMessage(message);
}
}.start();
- 设置一个网络操作方法
// 网络操作
private void getHttp() {
try {
// 1. 实例化一个 URL 对象
URL url = new URL("https://reqres.in/api/users");
// 2. 获取 HttpURLConnection 实例,使用URL的
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
// 3. 设置请求相关属性
httpURLConnection.setRequestMethod("GET"); // 请求方法
httpURLConnection.setConnectTimeout(6000); // 超时时间
// 4. 获取响应码 200 成功 404 未请求到指定资源 500 服务器异常(此时已经发起请求)
// 5. 判断响应码并获取响应数据(响应的正文)
if (httpURLConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
InputStream inputStream = httpURLConnection.getInputStream(); // 获取了服务器返回的输入流
byte[] bytes = new byte[1024]; // bytes 数组,每次最多可以存放 1024 个字节
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();// 存储从输入流中读取的数据
int len = 0;
while ((len = inputStream.read(bytes)) > -1) {
// 将字节数组中的内容写入到缓存流
/* 参数1:要存入的字节数组
* 参数2:起点
* 参数3:要存入的长度*/
byteArrayOutputStream.write(bytes, 0, len);
}
httpDate = new String(byteArrayOutputStream.toByteArray());
Log.e("GET返回的数据", httpDate);
}
} catch (ProtocolException ex) {
throw new RuntimeException(ex);
} catch (MalformedURLException ex) {
throw new RuntimeException(ex);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
全部代码
package com.example.androidstudiostudy;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import com.example.androidstudiostudy.data.DataBean;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
public class HandleActivity extends AppCompatActivity {
TextView textView1, textView2, textView3;
String httpDate = "";
// 利用 handller 处理
// 1.实例化一个 Handler
Handler handler = new Handler(
// 只要 handle 发了消息,就一定会触发 handleMessage 方法,并传入一个 Message 对象
new Handler.Callback() {
@Override
// 3.由handle对象接收消息并处理,在Handler的内部类中处理
public boolean handleMessage(@NonNull Message msg) {
Log.e("handler 处理消息", "这是handler发送的消息,此时是空");
// 根据 Message 的 what 属性,区分来源于哪个线程
if (msg.what == 1) {
textView1 = findViewById(R.id.t1);
textView1.setText(httpDate);
} else if (msg.what == 2) {
textView2 = findViewById(R.id.t2);
textView2.setText(httpDate);
} else if (msg.what == 3) {
textView3 = findViewById(R.id.t3);
textView3.setText("msg.what = "+msg.what+"\n"+msg.obj.toString());
}
return false;
}
}
);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handle);
}
// 此时有两个线程,这两线程发送的消息都能被 Handle接收,但如何区分不同线程发送的消息从而做不同处理呢?
public void getDate(View view) {
int id = view.getId();
// 第一个线程
if (id == R.id.handle1) {
new Thread() {
@Override
public void run() {
super.run();
getHttp();
// 此时会报错:Only the original thread that created a view hierarchy can touch its views.
/*TextView textView = findViewById(R.id.t1);
textView.setText(httpDate);*/
// 解决办法 1 runOnUiThread 方法 ==> 相当于在主线程中跑 (初学阶段)
/*runOnUiThread(new Runnable() {
@Override
public void run() {
TextView textView = findViewById(R.id.t1);
textView.setText(httpDate);
}
});*/
// 解决办法 2 利用 Handle 处理
// 2.在子线程中发送(空)消息,此时发送的是空消息
handler.sendEmptyMessage(1);
}
}.start();
}
// 第二个线程
else if (id == R.id.handle2) {
new Thread() {
@Override
public void run() {
super.run();
getHttp();
httpDate = httpDate + "\n第2个线程";
handler.sendEmptyMessage(2);
}
}.start();
}
// 第三个线程
else if (id == R.id.handle3) {
new Thread() {
@Override
public void run() {
super.run();
getHttp();
// 当 handler 需要传送数据时,需要使用到 Message,使用 sendMessage 方法
/* 1. 实例化一个 Message
* 2. 参数
** what:用于区分 handler 发送消息的不同线程来源
** arg1/arg2: 子线程需要向主线程传递整型参数
** obj: Object 任意对象*/
Message message = new Message();
message.what = 3;
DataBean dataBean = new DataBean(1,"157181@qq.com","ybr","scl","asiudh");
message.obj = dataBean;
handler.sendMessage(message);
}
}.start();
}
}
// 网络操作
private void getHttp() {
try {
// 1. 实例化一个 URL 对象
URL url = new URL("https://reqres.in/api/users");
// 2. 获取 HttpURLConnection 实例,使用URL的
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
// 3. 设置请求相关属性
httpURLConnection.setRequestMethod("GET"); // 请求方法
httpURLConnection.setConnectTimeout(6000); // 超时时间
// 4. 获取响应码 200 成功 404 未请求到指定资源 500 服务器异常(此时已经发起请求)
// 5. 判断响应码并获取响应数据(响应的正文)
if (httpURLConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
InputStream inputStream = httpURLConnection.getInputStream(); // 获取了服务器返回的输入流
byte[] bytes = new byte[1024]; // bytes 数组,每次最多可以存放 1024 个字节
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();// 存储从输入流中读取的数据
int len = 0;
while ((len = inputStream.read(bytes)) > -1) {
// 将字节数组中的内容写入到缓存流
/* 参数1:要存入的字节数组
* 参数2:起点
* 参数3:要存入的长度*/
byteArrayOutputStream.write(bytes, 0, len);
}
httpDate = new String(byteArrayOutputStream.toByteArray());
Log.e("GET返回的数据", httpDate);
}
} catch (ProtocolException ex) {
throw new RuntimeException(ex);
} catch (MalformedURLException ex) {
throw new RuntimeException(ex);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
Looper 循环器
- 负责循环取出 MessageQueue 中当前需要处理的Message
- 交给对应的handler进行处理
- 处理完后,将Message缓存到消息池中,以备复用
每个线程都可以有一个关联的Looper对象,用于处理该线程的消息队列。主要功能是接收来自消息队列的消息并将其分发给对应的Handler处理。
在Android中,主线程已经自动创建了一个Looper对象,并启动了消息循环,我们可以在主线程中方便地使用Handler来处理UI事件。
而对于其他线程,如果需要处理消息,就需要手动创建一个Looper对象,并调用Looper.loop()方法来启动消息循环。
- 必须确保在创建Handler对象之前,在线程中调用了Looper.prepare()方法创建Looper对象并初始化。
- 通过调用Looper.loop()方法,线程会进入一个无限循环,不断地从消息队列中取出消息,并将其分发给对应的Handler进行处理。当调用Looper.quit()方法时,消息循环会停止,线程会退出循环。
Handler handler2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handle);
// 开一个新的线程,用于接收主线程向子线程传递消息
new Thread(new Runnable() {
@Override
public void run() {
// 系统会自动为主线程开启消息循环(自动调用这句代码),与此同时创建一个 Looper 对象,不断的从 MessageQue中读取消息,交给主线程处理
Looper.prepare(); // 准备开启一个消息循环
handler2 = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
Log.e("主线程向子线程中发送消息", "" + msg.what + "" + msg.arg1);
return false;
}
});
Looper.loop(); //开始消息循环,相当于 while(true) ,在这里等待消息
}
}).start();
}
// 此时有两个线程,这两线程发送的消息都能被 Handle接收,但如何区分不同线程发送的消息从而做不同处理呢?
public void getDate(View view) {
int id = view.getId();
// 第一个线程
if (id == R.id.handle1) {
new Thread() {
@Override
public void run() {
super.run();
getHttp();
// 此时会报错:Only the original thread that created a view hierarchy can touch its views.
/*TextView textView = findViewById(R.id.t1);
textView.setText(httpDate);*/
// 解决办法 1 runOnUiThread 方法 ==> 相当于在主线程中跑 (初学阶段)
/*runOnUiThread(new Runnable() {
@Override
public void run() {
TextView textView = findViewById(R.id.t1);
textView.setText(httpDate);
}
});*/
// 解决办法 2 利用 Handle 处理
// 2.在子线程中发送(空)消息,此时发送的是空消息
handler.sendEmptyMessage(1);
}
}.start();
}
// 第二个线程
else if (id == R.id.handle2) {
new Thread() {
@Override
public void run() {
super.run();
getHttp();
httpDate = httpDate + "\n第2个线程";
handler.sendEmptyMessage(2);
}
}.start();
}
// 第三个线程
else if (id == R.id.handle3) {
new Thread() {
@Override
public void run() {
super.run();
getHttp();
Message message = new Message();
message.what = 3;
DataBean dataBean = new DataBean(1, "157181@qq.com", "ybr", "scl", "asiudh");
message.obj = dataBean;
handler.sendMessage(message);
}
}.start();
}
// 第四个线程- 主线程向子线程发送消息
else if (id == R.id.handle4) {
Message message2 = new Message();
message2.what = 999;
message2.arg1 = 1234;
handler2.sendMessage(message2);
}
}
线程中更新UI
runOnUiThread 方法
==> 相当于在主线程中跑 (初学阶段)
public void getDate() {
new Thread() {
@Override
public void run() {
super.run();
getHttp();
// 此时会报错:Only the original thread that created a view hierarchy can touch its views.
/*TextView textView = findViewById(R.id.t1);
textView.setText(httpDate);*/
// 解决办法 1 runOnUiThread 方法 ==> 相当于在主线程中跑 (初学阶段)
runOnUiThread(new Runnable() {
@Override
public void run() {
TextView textView = findViewById(R.id.t1);
textView.setText(httpDate);
}
});
}
}.start();
}
利用Handler
new Thread() {
@Override
public void run() {
super.run();
getHttp();
// 此时会报错:Only the original thread that created a view hierarchy can touch its views.
// 2.在子线程中发送(空)消息,此时发送的是空消息
handler.sendEmptyMessage(1);
}
}.start();
Handler handler = new Handler(
new Handler.Callback() {
@Override
// 3.由handle对象接收消息并处理,在Handler的内部类中处理
public boolean handleMessage(@NonNull Message msg) {
Log.e("handler 处理消息", "这是handler发送的消息,此时是空");
textView = findViewById(R.id.t1);
textView.setText(httpDate);
return false;
}
}
);
什么是异步任务?
逻辑上:以多线程的方式完成的功能需求
API上:指AsyncTask类
在没有 AsyncTask 前,我们可以使用 Thread+Handler 实现异步任务,而 AsyncTask 是对Thread+Handler 功能的封装,使用更简洁,效率更高效
AsyncTask 是一个抽象类,需要指定3个泛型参数
public abstract class AsyncTask<Params, Progress, Result>
- Params: 这个泛型指定的是我们传递给异步任务执行时的参数的类型。
- Progress: 这个泛型指定的是我们的异步任务在执行的时候将执行的进度返回给UI线程的参数的类型。
- Result: 这个泛型指定的异步任务执行完后返回给UI线程的结果的类型。
而 AsyncTask目前已被弃用
上一篇: 88 张成长之表 - 第八十八张
下一篇: 小满 "广告事件背后是内容营销的全面崛起
推荐阅读
-
Android --- 消息机制和异步任务
-
在 Android 中延迟执行任务和轮询定时任务的几种常见方法
-
epoll简介及触发模式(accept、read、send)-epoll的简单介绍 epoll在LT和ET模式下的读写方式 一、epoll的接口非常简单,一共就三个函数:1. int epoll_create(int size);创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close关闭,否则可能导致fd被耗尽。2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epoll的事件注册函数,它不同与select是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create的返回值,第二个参数表示动作,用三个宏来表示:EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd;第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */};events可以是以下几个宏的集合:EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLIN事件:EPOLLIN事件则只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。现在明白为什么说epoll必须要求异步socket了吧?如果同步socket,而且要求读完所有数据,那么最终就会在堵死在阻塞里。 EPOLLOUT:表示对应的文件描述符可以写; EPOLLOUT事件:EPOLLOUT事件只有在连接时触发一次,表示可写,其他时候想要触发,那要先准备好下面条件:1.某次write,写满了发送缓冲区,返回错误码为EAGAIN。2.对端读取了一些数据,又重新可写了,此时会触发EPOLLOUT。简单地说:EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!其实,如果真的想强制触发一次,也是有办法的,直接调用epoll_ctl重新设置一下event就可以了,event跟原来的设置一模一样都行(但必须包含EPOLLOUT),关键是重新设置,就会马上触发一次EPOLLOUT事件。1. 缓冲区由满变空.2.同时注册EPOLLIN | EPOLLOUT事件,也会触发一次EPOLLOUT事件这个两个也会触发EPOLLOUT事件 EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);EPOLLERR:表示对应的文件描述符发生错误;EPOLLHUP:表示对应的文件描述符被挂断;EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待事件的产生,类似于select调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。-------------------------------------------------------------------------------------------- 从man手册中,得到ET和LT的具体描述如下EPOLL事件有两种模型:Edge Triggered (ET)Level Triggered (LT)假如有这样一个例子:1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符2. 这个时候从管道的另一端被写入了2KB的数据3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作4. 然后我们读取了1KB的数据5. 调用epoll_wait(2)......Edge Triggered 工作模式:如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。 i 基于非阻塞文件句柄 ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。Level Triggered 工作模式相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。然后详细解释ET, LT:LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。(未测试)另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,读数据的时候需要考虑的是当recv返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取: 这里只是说明思路(参考《UNIX网络编程》) while(rs) {buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);if(buflen < 0){// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读// 在这里就当作是该次事件已处理处.if(errno == EAGAIN)break; else return; }else if(buflen == 0) { // 这里表示对端的socket已正常关闭. } if(buflen == sizeof(buf) rs = 1; // 需要再次读取 else rs = 0; } 还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send内部,当写缓冲已满(send返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send内部,但暂没有更好的办法. ssize_t socket_send(int sockfd, const char* buffer, size_t buflen) { ssize_t tmp; size_t total = buflen; const char *p = buffer; while(1) { tmp = send(sockfd, p, total, 0); if(tmp < 0) { // 当send收到信号时,可以继续写,但这里返回-1. if(errno == EINTR) return -1; // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满, // 在这里做延时后再重试. if(errno == EAGAIN) { usleep(1000); continue; } return -1; } if((size_t)tmp == total) return buflen; total -= tmp; p += tmp; } return tmp; } 二、epoll在LT和ET模式下的读写方式 在一个非阻塞的socket上调用read/write函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK) 从字面上看, 意思是: * EAGAIN: 再试一次 * EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block * perror输出: Resource temporarily unavailable 总结: 这个错误表示资源暂时不够, 可能read时, 读缓冲区没有数据, 或者, write时,写缓冲区满了 。 遇到这种情况, 如果是阻塞socket, read/write就要阻塞掉。 而如果是非阻塞socket, read/write立即返回-1, 同 时errno设置为EAGAIN. 所以, 对于阻塞socket, read/write返回-1代表网络出错了. 但对于非阻塞socket, read/write返回-1不一定网络真的出错了. 可能是Resource temporarily unavailable. 这时你应该再试, 直到Resource available. 综上, 对于non-blocking的socket, 正确的读写操作为: 读: 忽略掉errno = EAGAIN的错误, 下次继续读 写: 忽略掉errno = EAGAIN的错误, 下次继续写 对于select和epoll的LT模式, 这种读写方式是没有问题的. 但对于epoll的ET模式, 这种方式还有漏洞. epoll的两种模式 LT 和 ET
-
Godot UI 线程、任务异步和消息弹出通知
-
最佳的 Android 启动优化:去启动页面和异步初始化
-
Spring 事件同步和异步以及实现机制
-
趣谈留言队列,搞清楚留言队列到底是什么!-说到消息队列,洪觉大概能猜到人们听到消息队列的反应,大致可以分为以下几类人。 第一类人,懵懵懂懂,刚上大学接触编程,还没用过消息队列,甚至还以为消息队列就是代码里面要新建一个List之类的;第二类人,听过消息队列,了解消息队列,但具体是什么还不是太明白,只知道一说到消息队列,脑海里马上出现了三组词,削峰、异步、解耦;第三类人,用过消息队列,对它有一定了解,但不知道为什么要这样设计,消息队列有什么样的前世今生,是如何演化到现在的模式的?**第四类人,已经对消息队列有了足够的了解,可以阅读本帖作为复习和温习。**你属于哪一类?无论你对消息队列了解多少,读完这篇文章后,我相信你都会有所收获。 什么是消息队列?我们为什么要使用消息队列?真的只是因为它看起来很勉强、很常用吗?当然不是,一项技术的出现往往是为了解决某种痛点,我们就从这个痛点出发,看看消息队列到底是为了解决什么问题而诞生的。 相信大家在工作之前,或者工作中接触单片机的次数会多一点,不管什么业务都一股脑塞进一个系统里,这种情况下接触消息队列的场景会比较少。但随着业务的增长,量上去了,单机系统就很难维护了,也扛不住并发量的增长,就需要把原来的单体应用拆分成多个服务。例如,牛奇网采用分布式架构,将原来的单体系统拆分成用户服务、题库服务、求职服务、论坛服务等,每个分布式节点都有一个集群,保证高可用性。 那虽然在这样的微服务架构下,如果某个核心业务并发量过大,系统就扛不住了。比如淘宝、淘票票、拼多多、京东等电商场景中的支付场景,你在某宝下单并支付后,调用支付服务,完成支付后,还需要更新订单的状态,这个时候就需要调用订单服务,那我们平时也下单,除了简单完成这些操作外,还会给你相应的积分;商家也会收到订单消息,并给您发送旺旺消息,确认订单无误;同时,也会给您发送消息,确认订单无误。确认订单无误;同时您还可以查看您的物流状态;还有系统为了给您推荐更适合您的商品,会根据您的订单做类似的推荐等等,我说的这些都是当我们下单后,肉眼可以感知到系统所做的动作。 **一个支付动作如果还需要调用那么多服务,等他们响应成功,最后再告诉用户你支付成功了,用户在系统中的整个体验会非常糟糕。**设想一下,假设请求服务+处理请求+响应总共需要 50ms,我们上面列出的场景:支付服务、订单服务、积分服务、商家服务、物流服务、推荐服务,总共需要 300ms。
-
C# 多线程和异步 (II) - 任务和 async/await 详解
-
深入探索 Android 活动:任务堆栈管理、启动模式和屏幕旋转策略的分析与应用 - III.
-
StompJS 文档摘要:如何创建 stomp 客户端、如何连接服务器、心跳机制、如何发送消息、如何订阅和取消订阅、事务、如何调试