IO ไม่ได้รู้สึกเกี่ยวข้องกับมัลติเธรดมากนัก แต่ NIO ได้เปลี่ยนวิธีการใช้เธรดในระดับแอปพลิเคชันและแก้ไขปัญหาในทางปฏิบัติบางอย่าง AIO เป็น IO แบบอะซิงโครนัสและซีรี่ส์ก่อนหน้าก็เกี่ยวข้องกันเช่นกัน ที่นี่สำหรับการเรียนรู้และการบันทึกฉันยังเขียนบทความเพื่อแนะนำ Nio และ AIO
1. NIO คืออะไร
NIO เป็นตัวย่อของ I/O ใหม่ซึ่งตรงกันข้ามกับวิธี I/O ที่ใช้สตรีมแบบเก่า ตัดสินจากชื่อมันแสดงถึงชุดใหม่ของมาตรฐาน Java I/O มันถูกรวมเข้ากับ JDK ใน Java 1.4 และมีคุณสมบัติดังต่อไปนี้:
การอ่านและเขียนทั้งหมดจากช่องจะต้องผ่านบัฟเฟอร์และช่องเป็นนามธรรมของ IO และอีกด้านหนึ่งของช่องทางคือไฟล์ที่ถูกจัดการ
2. บัฟเฟอร์
การใช้งานบัฟเฟอร์ใน Java ชนิดข้อมูลพื้นฐานมีบัฟเฟอร์ที่สอดคล้องกัน
ตัวอย่างง่ายๆของการใช้บัฟเฟอร์:
การทดสอบแพ็คเกจ; นำเข้า java.io.file; นำเข้า java.io.fileinputstream; นำเข้า java.nio.bytebuffer; นำเข้า java.nio.channels.filechannel; การทดสอบคลาสสาธารณะ {โมฆะคงที่สาธารณะหลัก (สตริง [] args) พ่นข้อยกเว้น {fileInputStream fin = ใหม่ fileInputStream (ไฟล์ใหม่ ("d: //temp_buffer.tmp")); FileChannel fc = fin.getChannel (); bytebuffer bytebuffer = bytebuffer.allocate (1024); fc.read (bytebuffer); fc.close (); bytebuffer.flip (); // อ่านและเขียนการแปลง}}ขั้นตอนการใช้งานสรุป:
1. รับช่อง
2. สมัครบัฟเฟอร์
3. สร้างความสัมพันธ์อ่าน/เขียนระหว่างช่องและบัฟเฟอร์
4. ปิด
ตัวอย่างต่อไปนี้คือการใช้ NIO เพื่อคัดลอกไฟล์:
โมฆะสาธารณะคงที่ niocopyFile (ทรัพยากรสตริง, สตริงปลายทาง) พ่น IOException {FileInputStream FIS = ใหม่ FileInputStream (ทรัพยากร); fileOutputStream fos = ใหม่ fileOutputStream (ปลายทาง); fileChannel readChannel = fis.getChannel (); // อ่าน File Channel FileChannel WriteChannel = fos.getChannel (); // เขียนช่องไฟล์ bytebuffer buffer = byteBuffer.Allocate (1024); // อ่านในแคชข้อมูลในขณะที่ (จริง) {buffer.clear (); int len = readchannel.read (บัฟเฟอร์); // อ่านในข้อมูลถ้า (len == -1) {break; // อ่านใน} buffer.flip (); writechannel.write (บัฟเฟอร์); // เขียนไปยังไฟล์} readchannel.close (); writechannel.close (); -มีพารามิเตอร์สำคัญ 3 ประการในบัฟเฟอร์: ตำแหน่งความจุและขีด จำกัด
ที่นี่เราต้องแยกแยะความสามารถระหว่างขีด จำกัด และขีด จำกัด สูงสุด ตัวอย่างเช่นหากบัฟเฟอร์มี 10KB ดังนั้น 10KB คือความจุ ถ้าฉันอ่านไฟล์ 5KB ลงในบัฟเฟอร์ขีด จำกัด บนคือ 5KB
นี่คือตัวอย่างที่จะเข้าใจพารามิเตอร์ที่สำคัญทั้ง 3 นี้:
โมฆะคงที่สาธารณะหลัก (สตริง [] args) โยนข้อยกเว้น {bytebuffer b = bytebuffer.allocate (15); // 15-byte buffer system.out.println ("limit =" + b.limit () + "ความจุ =" + b.capacity () + "ตำแหน่ง =" + b.position ()); สำหรับ (int i = 0; i <10; i ++) {// บันทึก 10 ไบต์ของข้อมูล b.put ((ไบต์) i); } system.out.println ("limit =" + b.limit () + "ความจุ =" + b.capacity () + "ตำแหน่ง =" + b.position ()); b.flip (); // รีเซ็ตตำแหน่ง System.out.println ("limit =" + b.limit () + "ความจุ =" + b.capacity () + "ตำแหน่ง =" + b.position ()); สำหรับ (int i = 0; i <5; i ++) {system.out.print (b.get ()); } system.out.println (); System.out.println ("limit =" + b.limit () + "ความจุ =" + b.capacity () + "ตำแหน่ง =" + b.position ()); b.flip (); System.out.println ("limit =" + b.limit () + "ความจุ =" + b.capacity () + "ตำแหน่ง =" + b.position ()); -กระบวนการทั้งหมดแสดงในรูป:
ในเวลานี้ตำแหน่งอยู่ที่ 0 ถึง 10 และความสามารถและขีด จำกัด ยังคงไม่เปลี่ยนแปลง
การดำเนินการนี้รีเซ็ตตำแหน่ง โดยปกติเมื่อแปลงบัฟเฟอร์จากโหมดการเขียนเป็นโหมดอ่านคุณต้องดำเนินการวิธีนี้ การดำเนินการ FLIP () ไม่เพียง แต่รีเซ็ตตำแหน่งปัจจุบันเป็น 0 แต่ยังตั้งค่าขีด จำกัด ไว้ที่ตำแหน่งของตำแหน่งปัจจุบัน
ความหมายของขีด จำกัด คือการพิจารณาว่าข้อมูลใดมีความหมาย กล่าวอีกนัยหนึ่งข้อมูลจากตำแหน่งที่จะ จำกัด คือข้อมูลที่มีความหมายเนื่องจากเป็นข้อมูลจากการดำเนินการล่าสุด ดังนั้นการดำเนินการพลิกมักหมายถึงการอ่านและการเขียนการแปลง
ความหมายเดียวกับข้างต้น
วิธีการส่วนใหญ่ในบัฟเฟอร์เปลี่ยนพารามิเตอร์ 3 ตัวเหล่านี้เพื่อให้ได้ฟังก์ชั่นบางอย่าง:
Rewind บัฟเฟอร์สุดท้ายสาธารณะ ()
ตั้งตำแหน่งเป็นศูนย์และทำเครื่องหมายที่ชัดเจน
บัฟเฟอร์สุดท้ายสาธารณะ Clear ()
ตั้งค่าตำแหน่งศูนย์และกำหนดขีด จำกัด เป็นขนาดความจุและล้างเครื่องหมายสถานะ
พลิกบัฟเฟอร์สุดท้ายสาธารณะ ()
ขีด จำกัด ชุดแรกไปยังตำแหน่งที่ตำแหน่งอยู่จากนั้นตั้งตำแหน่งเป็นศูนย์และล้างเครื่องหมายบิตของธงซึ่งมักใช้ในระหว่างการแปลงและเขียนการเขียน
ไฟล์ที่แมปกับหน่วยความจำ
โมฆะคงที่สาธารณะหลัก (สตริง [] args) โยนข้อยกเว้น {randomaccessfile raf = new randomaccessFile ("c: //mapfile.txt", "rw"); FileChannel fc = raf.getChannel (); // แมปไฟล์ลงในหน่วยความจำ mappedbyTebuffer mbb = fc.map (filechannel.mapmode.read_write, 0, raf.length ()); ในขณะที่ (mbb.hasremaining ()) {system.out.print ((char) mbb.get ()); } mbb.put (0, (ไบต์) 98); // แก้ไขไฟล์ raf.close (); -การแก้ไข MappedByTebuffer นั้นเทียบเท่ากับการปรับเปลี่ยนไฟล์เองดังนั้นความเร็วในการดำเนินการจึงเร็วมาก
3. ช่อง
โครงสร้างทั่วไปของเซิร์ฟเวอร์เครือข่ายแบบมัลติเธรด:
เซิร์ฟเวอร์มัลติเธรดแบบง่าย ๆ :
โมฆะคงที่สาธารณะหลัก (String [] args) โยนข้อยกเว้น {serversocket echoserver = null; ซ็อกเก็ต clientsocket = null; ลอง {echoserver = ใหม่ Serversocket (8000); } catch (ioexception e) {system.out.println (e); } ในขณะที่ (จริง) {ลอง {clientSocket = echoserver.accept (); System.out.println (clientsocket.getRemotesocketAddress () + "เชื่อมต่อ!"); tp.execute (ใหม่ handlemsg (clientsocket)); } catch (ioexception e) {system.out.println (e); -ฟังก์ชั่นคือการเขียนข้อมูลกลับไปยังไคลเอนต์เมื่อใดก็ตามที่เซิร์ฟเวอร์อ่าน
ที่นี่ TP เป็นพูลเธรดและ handlemsg เป็นคลาสที่จัดการกับข้อความ
HandleMSG คลาสแบบคงที่ใช้งาน Runnable {omit ส่วนหนึ่งของข้อมูลโมฆะสาธารณะ run () {ลอง {is = ใหม่ bufferedReader (ใหม่ inputStreamReader (clientsOcket.getInputStream ()); OS = ใหม่ PrintWriter (clientsOcket.getOutputStream (), จริง); // อ่านข้อมูลที่ส่งโดยไคลเอนต์จากสตริงอินพุตอินพุตอินพุต = null; long b = system.currenttimeMillis (); ในขณะที่ ((inputline = is.readline ())! = null) {os.println (อินพุตไลน์); } Long E = ระบบ CurrentTimemillis (); ระบบ. out.println ("ใช้จ่าย:"+(e - b)+"ms"); } catch (ioexception e) {e.printstacktrace (); } ในที่สุด {ปิดทรัพยากร}}}ลูกค้า:
โมฆะคงที่สาธารณะหลัก (สตริง [] args) โยนข้อยกเว้น {ซ็อกเก็ตไคลเอนต์ = null; PrintWriter Writer = NULL; bufferedReader reader = null; ลอง {client = socket ใหม่ (); client.connect (ใหม่ inetSocketAddress ("localhost", 8000)); Writer = New PrintWriter (client.getOutputStream (), จริง); Writer.println ("สวัสดี!"); Writer.flush (); reader = ใหม่ bufferedReader (ใหม่ inputStreamReader (client.getInputStream ())); System.out.println ("จากเซิร์ฟเวอร์:" + reader.readline ()); } catch (exception e) {} ในที่สุด {// omit การปิดทรัพยากร}}}การเขียนโปรแกรมเครือข่ายข้างต้นเป็นพื้นฐานมากและจะมีปัญหาบางอย่างโดยใช้วิธีนี้:
ใช้หนึ่งเธรดสำหรับแต่ละไคลเอนต์ หากลูกค้าประสบกับข้อยกเว้นเช่นความล่าช้าเธรดอาจถูกครอบครองเป็นเวลานาน เพราะการเตรียมข้อมูลและการอ่านอยู่ในหัวข้อนี้ ในเวลานี้หากมีลูกค้าจำนวนมากอาจใช้ทรัพยากรระบบจำนวนมาก
สารละลาย:
ใช้ No-Blocking NIO (อ่านข้อมูลโดยไม่ต้องรอข้อมูลพร้อมก่อนทำงาน)
เพื่อสะท้อนประสิทธิภาพของการใช้ NIO
ที่นี่ก่อนอื่นเราจำลองลูกค้าที่ไม่มีประสิทธิภาพเพื่อจำลองความล่าช้าเนื่องจากเครือข่าย:
ExecutorService ส่วนตัว TP = Executors.newcachedThreadPool (); private static final int sleep_time = 1000*1000*1000; echoclient คลาสคงที่ public ใช้งาน {public void run () {ลอง {client = socket ใหม่ (); client.connect (ใหม่ inetSocketAddress ("localhost", 8000)); Writer = New PrintWriter (client.getOutputStream (), จริง); Writer.print ("H"); locksupport.parknanos (sleep_time); Writer.print ("E"); locksupport.parknanos (sleep_time); Writer.print ("L"); locksupport.parknanos (sleep_time); Writer.print ("L"); locksupport.parknanos (sleep_time); Writer.print ("O"); locksupport.parknanos (sleep_time); Writer.print ("!"); locksupport.parknanos (sleep_time); Writer.println (); Writer.flush (); } catch (Exception E) {}}}}เอาต์พุตฝั่งเซิร์ฟเวอร์:
ใช้จ่าย: 6,000ms
ใช้จ่าย: 6,000ms
ใช้จ่าย: 6,000ms
ใช้จ่าย: 6001ms
ใช้จ่าย: 6002ms
ใช้จ่าย: 6002ms
ใช้จ่าย: 6002ms
ใช้จ่าย: 6002ms
ใช้จ่าย: 6003ms
ใช้จ่าย: 6003ms
เพราะ
ในขณะที่ ((inputline = is.readline ())! = null)
มันถูกบล็อกดังนั้นเวลาจึงต้องรอ
จะเกิดอะไรขึ้นถ้า Nio ถูกใช้เพื่อจัดการกับปัญหานี้?
หนึ่งในคุณสมบัติที่ยิ่งใหญ่ของ NIO คือ: แจ้งให้ฉันทราบว่าข้อมูลพร้อมหรือไม่
ช่องทางค่อนข้างคล้ายกับสตรีมและช่องสามารถสอดคล้องกับไฟล์หรือซ็อกเก็ตเครือข่าย
ตัวเลือกเป็นตัวเลือกที่สามารถเลือกช่องและทำอะไรบางอย่าง
เธรดสามารถสอดคล้องกับตัวเลือกและตัวเลือกสามารถสำรวจความคิดเห็นหลายช่องทางและแต่ละช่องมีซ็อกเก็ต
เมื่อเปรียบเทียบกับเธรดข้างต้นที่สอดคล้องกับซ็อกเก็ตหลังจากใช้ NIO เธรดสามารถสำรวจซ็อกเก็ตหลายตัว
เมื่อตัวเลือกการโทรเลือก () จะตรวจสอบว่าไคลเอนต์ใด ๆ ได้เตรียมข้อมูลหรือไม่ เมื่อไม่มีข้อมูลพร้อมให้เลือก () จะบล็อก มักจะบอกว่า NIO ไม่ปิดกั้น แต่ถ้าข้อมูลยังไม่พร้อมก็จะยังคงมีการปิดกั้น
เมื่อข้อมูลพร้อมหลังจากโทรเลือก () จะส่งคืนการเลือกจะถูกส่งคืน SelectionKey ระบุว่าข้อมูลของช่องทางในตัวเลือกได้จัดทำขึ้นแล้ว
ช่องนี้จะถูกเลือกเฉพาะเมื่อข้อมูลพร้อมเท่านั้น
ด้วยวิธีนี้ NIO ใช้เธรดเพื่อตรวจสอบลูกค้าหลายราย
ไคลเอนต์ที่มีการหน่วงเวลาของเครือข่ายเพียงแค่จำลองจะไม่ส่งผลกระทบต่อเธรดภายใต้ NIO เนื่องจากเมื่อเครือข่ายซ็อกเก็ตล่าช้าข้อมูลยังไม่พร้อมและตัวเลือกจะไม่เลือก แต่จะเลือกไคลเอนต์ที่เตรียมไว้อื่น ๆ
ความแตกต่างระหว่าง selectnow () และ select () คือ selectnow () ไม่บล็อก เมื่อไม่มีลูกค้าเตรียมข้อมูล SelectNow () จะไม่บล็อกและจะส่งคืน 0. เมื่อไคลเอนต์เตรียมข้อมูล SELECTNOW () จะส่งคืนจำนวนไคลเอนต์ที่เตรียมไว้
รหัสหลัก:
การทดสอบแพ็คเกจ; นำเข้า java.net.inetaddress; นำเข้า java.net.inetsocketaddress; นำเข้า java.net.socket; นำเข้า java.nio.channels.SelectionKey นำเข้า java.nio.channels.Selector; java.nio.channels.socketChannel; นำเข้า java.nio.channels.spi.abstractselector; นำเข้า java.nio.channels.spi.SelectorProvider; นำเข้า java.util.hashmap; นำเข้า Java.util.iterator; java.util.set; นำเข้า java.util.concurrent.executorservice; นำเข้า java.util.concurrent.executors; Public Class MultithReadNioeChoserver {แผนที่สาธารณะคงที่ <ซ็อกเก็ต, ยาว> geym_time_stat = new hashmap <ซ็อกเก็ต, long> (); ระดับ echoclient {Private LinkedList <ByTebuffer> outQ; echoclient () {outq = new linkedList <bytebuffer> (); } Public LinkedList <ByTebuffer> getOutputqueue () {return outQ; } โมฆะสาธารณะ enqueue (bytebuffer bb) {outq.addfirst (bb); }} คลาส handlemsg ดำเนินการ runnable {selectionKey sk; Bytebuffer BB; Public Handlemsg (SelectionKey SK, Bytebuffer BB) {super (); this.sk = sk; this.bb = bb; } @Override โมฆะสาธารณะ Run () {// todo วิธีการที่สร้างอัตโนมัติ stub echoclient echoclient = (echoclient) sk.attachment (); Echoclient.enqueue (BB); sk.interestops (selectionkey.op_read | selectionKey.op_write); selector.wakeup (); }} ตัวเลือกส่วนตัว ExecutorService ส่วนตัว tp = executors.newcachedthreadpool (); Private Void starterver () โยนข้อยกเว้น {selector = selectorProvider.provider (). openSelector (); ServersocketChannel SSC = ServersocketChannel.open (); ssc.configureblocking (เท็จ); InetSocketAddress ISA = ใหม่ inetSocketAddress (8000); ssc.socket (). ผูก (isa); // ลงทะเบียนเหตุการณ์ที่น่าสนใจที่นี่มีความสนใจในการเลือกเหตุการณ์ ACCPET KEYKENTKEY = SSC.REGISTER (ตัวเลือก, SelectionKey.OP_ACCEPT); สำหรับ (;;) {selector.select (); ตั้งค่า readyKeys = selector.selectedKeys (); Iterator i = ReadyKeys.iterator (); ยาว e = 0; ในขณะที่ (i.hasnext ()) {selectionKey sk = (selectionKey) i.next (); i.remove (); if (sk.isacceptable ()) {doaccept (sk); } อื่นถ้า (sk.isvalid () && sk.isreadable ()) {ถ้า (! geym_time_stat.containskey (((socketchannel) sk .channel ()). ซ็อกเก็ต ()) {geym_time_stat.put.put.put.put.put.put.put.put.put.put.put.put.put.put.put.put.put.put.put. } DOREAD (SK); } อื่นถ้า (sk.isvalid () && sk.iswitable ()) {dowrite (sk); e = system.currenttimemillis (); long b = geym_time_stat.remove (((socketchannel) sk. channel ()). ซ็อกเก็ต ()); System.out.println ("ใช้จ่าย:" + (e - b) + "ms"); }}}} โมฆะส่วนตัว dowrite (SelectionKey SK) {// วิธีการที่สร้างอัตโนมัติแบบอัตโนมัติ Stub Socketchannel channel = (Socketchannel) sk.channel (); echoclient echoclient = (echoclient) sk.attachment (); LinkedList <ByTebuffer> outQ = echoclient.getOutputqueue (); bytebuffer bb = outq.getLast (); ลอง {int len = channel.write (bb); if (len == -1) {disconnect (sk); กลับ; } if (bb.remaining () == 0) {outq.remoVelast (); }} catch (exception e) {// todo: จัดการข้อยกเว้น disconnect (sk); } if (outq.size () == 0) {sk.interestops (selectionKey.op_read); }} โมฆะส่วนตัว DOREAD (SelectionKey SK) {// วิธีการที่สร้างขึ้นอัตโนมัติ todo stub socketchannel channel = (socketchannel) sk.channel (); bytebuffer bb = bytebuffer.allocate (8192); int len; ลอง {len = channel.read (bb); if (len <0) {disconnect (sk); กลับ; }} catch (exception e) {// todo: จัดการข้อยกเว้น disconnect (sk); กลับ; } bb.flip (); tp.execute (ใหม่ handlemsg (sk, bb)); } โมฆะส่วนตัวยกเลิกการเชื่อมต่อ (SelectionKey SK) {// วิธีการที่สร้างขึ้นอัตโนมัติ todo stub // ละเว้นการดำเนินการปิดแห้ง} โมฆะส่วนตัว Doaccept (SelectionKey SK) {// วิธีการที่สร้างอัตโนมัติ Socketchannel ClientChannel; ลอง {clientChannel = server.accept (); clientChannel.ConfigureBlocking (เท็จ); SelectionKey ClientKey = clientChannel.Register (ตัวเลือก, selectionKey.op_read); echoclient echoclinet = echoclient ใหม่ (); clientkey.attach (echoclinet); inetAddress clientAddress = clientChannel.Socket (). getInetAddress (); System.out.println ("การเชื่อมต่อที่ยอมรับจาก" + clientaddress.getHostaddress ()); } catch (exception e) {// toDo: จัดการข้อยกเว้น}} โมฆะคงที่สาธารณะหลัก (สตริง [] args) {// toDo วิธีการที่สร้างขึ้นอัตโนมัติ multithreadNioeChoserver echoserver = new MultithreadNioeChoserver (); ลอง {echoserver.startserver (); } catch (Exception e) {// todo: จัดการข้อยกเว้น}}}รหัสนี้ใช้สำหรับการอ้างอิงเท่านั้นและคุณสมบัติหลักของมันคือคุณสนใจในเหตุการณ์ต่าง ๆ เพื่อทำสิ่งต่าง ๆ
เมื่อใช้ไคลเอนต์ล่าช้าที่ได้รับการจำลองก่อนหน้านี้การใช้เวลาในเวลานี้อยู่ระหว่าง 2ms และ 11ms การปรับปรุงประสิทธิภาพนั้นชัดเจน
สรุป:
1. หลังจาก NIO เตรียมข้อมูลมันจะส่งมอบให้กับแอปพลิเคชันสำหรับการประมวลผล กระบวนการอ่าน/การเขียนข้อมูลยังคงเสร็จสมบูรณ์ในเธรดแอปพลิเคชันและจะตัดเวลารอไปยังเธรดแยกต่างหากเท่านั้น
2. บันทึกเวลาการเตรียมข้อมูล (เพราะตัวเลือกสามารถนำกลับมาใช้ใหม่ได้)
5. AIO
คุณสมบัติของ AIO:
1. แจ้งให้ฉันทราบหลังจากอ่าน
2. IO จะไม่ถูกเร่ง แต่จะได้รับแจ้งหลังจากอ่านมัน
3. ใช้ฟังก์ชั่นการโทรกลับเพื่อดำเนินการกับธุรกิจ
รหัสที่เกี่ยวข้อง AIO:
AsynchronousserversocketChannel
Server = AsynchronousSoRsocketChannel.open (). ผูก (ใหม่ inetSocketAddress (พอร์ต));
ใช้วิธีการยอมรับบนเซิร์ฟเวอร์
บทคัดย่อสาธารณะ <a> เป็นโมฆะยอมรับ (สิ่งที่แนบมา, เสร็จสิ้น handler <asynchronoussocketChannel, super a> handler);
PoltionHandler เป็นอินเทอร์เฟซโทรกลับ เมื่อมีลูกค้ายอมรับมันจะทำสิ่งที่อยู่ในตัวจัดการ
รหัสตัวอย่าง:
Server.Accept (NULL, ใหม่ HEPTIONHANDLER <AsyNCHRONOUSSOCKETCHANNEL, OBJECT> () {บัฟเฟอร์ byTebuffer สุดท้าย = byTebuffer.Allocate (1024); โมฆะสาธารณะเสร็จสิ้น {buffer.clear (); Server.Accept (null, สิ่งนี้);ที่นี่เราใช้อนาคตเพื่อให้ได้ผลตอบแทนทันที สำหรับอนาคตโปรดดูบทความก่อนหน้า
จากการทำความเข้าใจ NIO การดู AIO ความแตกต่างคือ AIO รอกระบวนการอ่านและเขียนเพื่อเรียกใช้ฟังก์ชันการโทรกลับก่อนที่จะเสร็จสิ้น
NIO เป็นแบบซิงโครนัสและไม่ปิดกั้น
AIO เป็นแบบอะซิงโครนัสและไม่ปิดกั้น
เนื่องจากกระบวนการอ่านและเขียน NIO ยังคงเสร็จสมบูรณ์ในเธรดแอปพลิเคชัน NIO จึงไม่เหมาะสำหรับผู้ที่มีกระบวนการอ่านและเขียนยาว
กระบวนการอ่านและเขียน AIO จะได้รับแจ้งหลังจากเสร็จสิ้นดังนั้น AIO จึงมีความสามารถสำหรับงานการอ่านและเขียนระยะยาวและงานระยะยาว