Ứng dụng đƣợc xây dựng với 3 thành phần chính:
- UI – User Interface (giao diện ngƣời dùng) là các trang web hiển thị giao diện tìm kiếm, kết quả và các thông tin khác. Từ giao diện này ngƣời dùng chọn lựa danh sách cơ sở dữ liệu cần tìm, đƣa vào câu truy vấn và gửi tới ZetMonitor – Bộ quản lý các luồng Z39.50 client. UI cũng nhận kết quả trả về và các thông tin khác để hiển thị cho ngƣời dùng.
- ZetMonitor – Bộ quản lý luồng Z39.50 client. ZetMonitor nhận câu lệnh truy vấn và danh sách cơ sở dữ liệu đích từ tầng UI. Khởi tạo các luồng
Z39.50 client, mỗi luồng làm việc với 1 cơ sở dữ liệu. Số lƣợng các luồng đƣợc qui định bằng kích thƣớc mảng các luồng Z39.50 client theo cấu hình đảm bảo hệ thống hoạt động hiệu quả. Mỗi luồng khi gặp lỗi hoặc hoàn thành nhiệm vụ sẽ đƣợc loại bỏ khỏi mảng các luồng và một luồng khác làm việc với cơ sở dữ liệu mới sẽ đƣợc đƣa vào cho đến khi hết danh sách cơ sở dữ liệu đích, hoặc lệnh hủy ZetMonitor đƣợc gọi. ZetMonitor quản lý một mảng kết quả trả về từ phía các luồng Z39.50 client và cung cấp theo yêu cầu của tầng UI. Mảng kết quả trả về không có độ dài cố định, nó sẽ đƣợc mở rộng khi ngƣời dùng duyệt tới gần cuối mảng.
- ZetThread – Luồng Z39.50 client là một Z39.50 client, khi đƣợc khởi tạo nhận câu truy vấn và thông tin cơ sở dữ liệu đích từ ZetMonitor. Khi chạy sẽ kết nối và gửi yêu cầu đến Z39.50 server. Nếu gặp lỗi sẽ gửi tình trạng đến ZetMonitor để ZetMonitor loại bỏ ZetThread này khỏi mảng và thêm ZetThread khác vào nếu có. ZetThread nhận thông tin số biểu ghi trả về từ Z39.50 server, thực hiện tải về bản ghi lần lƣợt. Với mỗi bản ghi tải về xong ZetThread cố gắng gửi tới ZetMonitor, nếu đƣợc ZetMonitor chấp nhận nó sẽ lấy bản ghi tiếp và gửi cho ZetMonitor, nếu không, nó nằm nghỉ một lát và gửi lại. Cứ nhƣ vậy cho đến khi hết kết quả hoặc nó bị hủy đi.
Việc xây dựng một lƣợc đồ cho loại ứng dụng đa luồng gặp nhiều khó khăn do tính chất phức tạp của các trạng thái. Do vậy tôi cố gắng đƣa ra mô tả về cấu trúc và hoạt động của các thành phần chƣơng trình nhƣ trên.
3.2.4 Sơ đồ lớp và mã nguồn chính 3.2.4.1 Class Diagram
Hình 4: Mô hình lớp
3.2.4.2 Lớp ThreadHelper
Lớp ThreadHelper giúp khởi tạo và quản lý luồng đƣợc kế thừa trong lớp ZetThread
using System;
using System.Threading; namespace ZetPortal {
public class ThreadHelper
{
protected Thread threadField; public ThreadHelper()
{
threadField = new Thread(new ThreadStart(Run)); }
public ThreadHelper(String name) {
threadField = new Thread(new ThreadStart(Run)); threadField.Name = name;
}
public virtual void Run() {
}
public virtual void Start() {
threadField.Start(); }
public virtual void Interrupt() {
threadField.Interrupt(); }
public virtual void Abort() {
threadField.Abort(); }
public virtual void Join() {
threadField.Join(); }
public virtual void Join(int miliseconds) {
threadField.Join(miliseconds); }
public bool IsAlive {
get { return threadField.IsAlive; } }
public bool IsBackground {
get { return threadField.IsBackground; }
set { if (threadField.IsAlive) threadField.IsBackground = value; }
} } }
3.2.4.3 Lớp ZetThread
Kế thừa từ lớp ThreadHelper để khởi tạo một Z39.50 client hoạt động nhƣ một luồng -Thread. using System; using CSZOOMC; using System.Threading; using MarcXmlParser; namespace ZetPortal {
public class ZetThread : ThreadHelper
{
private ZoomConnection _zc; private ZoomQuery _zqry; private ZoomResultSet _zrs; public event Thread_Done Done;
public event Thread_Send_Record Send_Record; public event Thread_Send_FoundCount Send_Size; public event Thread_Status_Changed Status_Changed; private int _iReceivedCount = 0;
private int _iFoundCount = 0; private ZDb _DbInfo = new ZDb(); private string _strQuery;
private int _index=0;
public ZetThread(int index,String sName,String sDbInfo, String
strQuery): base(sName) {
//string Name, string Host,int Port,string Database,string Charset
_index = index; // indentifier of the thread in thread array of Monitor _DbInfo.load_Xml(sDbInfo); _strQuery= strQuery; } ~ZetThread() { try { if (threadField.IsAlive) { threadField.Abort(); threadField.Join(); } _zrs = null; _zc = null; }
catch(Exception ex) { //do nothing String errorString=ex.Message; } finally { //do nothing } }
public override void Run() {
try {
createConnection();
_zrs = _zc.Search(_zqry); int count = _zrs.GetSize();
if ((count > 0) && (_iFoundCount == 0)) {
Send_Size(count); _iFoundCount = count; }
int start = _iReceivedCount;
Status_Changed(_index, "Running", _iReceivedCount, _iFoundCount);
for (int i = start; i < count; i++) {
ZoomRecord zr = _zrs.GetRecord(i); String xmlRecord = string.Empty; CRecord myRec = new CRecord();
if (_DbInfo.Charset.ToLower().Trim() == "a") {
xmlRecord = zr.GetXMLRecord(_DbInfo.Charset); }
else //auto detect charset
{
xmlRecord = zr.GetXMLRecord(); }
myRec.load_Xml(xmlRecord);
{
myRec.Datafields.Clean(); myRec.Datafields.Refresh(); }
CDatafield Df = new CDatafield(); Df.Tag="999";
CSubfield Sf = new CSubfield(); Sf.Code="a";
Sf.Value = _DbInfo.Name; Df.Subfields.Add(Sf); myRec.Datafields.Add(Df); xmlRecord = myRec.OuterXml; int icheck = _iReceivedCount; Send_Record(this, xmlRecord);
while (icheck == _iReceivedCount) //while record has not been added to ResultCache
{ if (threadField.IsAlive) { Thread.Sleep(500); Send_Record(this, xmlRecord); } else { break; } }
Status_Changed(_index, "Running", _iReceivedCount, _iFoundCount);
}
if (_iFoundCount == 0) {
Status_Changed(_index, "Not found", _iReceivedCount, _iFoundCount);
} else {
Status_Changed(_index, "Finished", _iReceivedCount, _iFoundCount);
}
//Done(this);
}
catch (CSZOOMCException czex) {
Status_Changed(_index, czex.Message, _iReceivedCount, _iFoundCount);
//Done(this);
}
catch (ThreadAbortException) {
Status_Changed(_index, "Abort", _iReceivedCount, _iFoundCount);
}
catch (Exception ex) { Status_Changed(_index,ex.Message, _iReceivedCount, _iFoundCount); //Done(this); } finally { Done(this); } }
public int FoundCount { get { return _iFoundCount; } set { _iFoundCount = value; } }
public int ReceivedCount { get { return _iReceivedCount; } set { _iReceivedCount = value; } }
public int Index { get { return _index; } }
public string Name { get { return _DbInfo.Name; } }
private void createConnection() {
ZoomFactory zf = new ZoomFactory();
_zc = zf.CreateZoomConnection(_DbInfo.Host, _DbInfo.Port); _zqry = zf.CreateZoomQuery(_strQuery, 0); if (_DbInfo.Name != "") { _zc.SetOption("user", _DbInfo.UserID); _zc.SetOption("pass", _DbInfo.Password); } _zc.SetOption("databaseName", _DbInfo.Database); _zc.SetOption("preferredRecordSyntax", "USmarc"); _zc.SetOption("elementSetName", "F"); } } } 3.2.4.4 Lớp ZetMonitor
Lớp ZetMonitor nhận câu lệnh truy vấn và danh sách các cơ sở dữ liệu đích Z39.50 từ giao diện ngƣời dùng, khởi tạo và quản lý các luồng Z39.50 client, quản lý bộ đệm kết quả và nhận kết quả trả về từ các luồng Z39.50 client.
using System; using System.Collections; using System.Threading; using MarcXmlParser; namespace ZetPortal {
public sealed class ZetMonitor
{
public event Monitor_Search_Finished Search_Done; public event Monitor_First_Record_Found First_Found; public event Monitor_Send_Status Send_Status; private int _iCacheSize = 50; private int _iCacheStepSize = 25; private int _iMaxThreadArraySize =15; private int _iLastDatabaseIndex = 0;
private Hashtable _ResultRecords=new Hashtable();
private ArrayList _ThreadArray = new ArrayList(); private int _iTotalFoundCount = 0; private int _iTotalReceivedCount = 0; private Hashtable _ZDbRecords = new Hashtable(); private String _RPNQuery = String.Empty; private bool _isDisposing = false; private int _index=0; public int index { set { _index=value; } get { return _index; } } public ZetMonitor() { } ~ZetMonitor() {
Object obj = _ThreadArray; lock (obj) {
if (_ThreadArray.Count > 0) {
for (int i = 0; i < _ThreadArray.Count; i++) {
ZetThread JThread = _ThreadArray[i] as ZetThread; if (JThread.IsAlive) { JThread.Abort(); JThread.Join(500); } } } } }
public int CurrentCacheSize {
get {
lock(obj) { return _iCacheSize; } } }
public int LastDatabaseIndex {
get {
Object obj = _iLastDatabaseIndex; lock (obj) { return _iLastDatabaseIndex; } } }
public int CurrentThreadCount {
get {
Object obj = _ThreadArray; lock(obj) { return _ThreadArray.Count; } } }
public int CacheSize {
get {
Object obj = _iCacheSize; { return _iCacheSize; } } set {
Object obj = _iCacheSize; lock(obj) { _iCacheSize = value; } } }
public int CacheStepSize { get { Object obj=_iCacheStepSize; lock (obj) { return _iCacheStepSize; } } set {
Object obj = _iCacheStepSize; lock(obj)
{
_iCacheStepSize = value; }
}
}
public int MaxThreadArraySize {
get {
Object obj = _iMaxThreadArraySize; lock(obj) { return _iMaxThreadArraySize; } } set {
Object obj = _iMaxThreadArraySize; lock(obj) { _iMaxThreadArraySize = value; } } }
public String ZDbRecordsXml {
set {
Object obj = _ZDbRecords; _ZDbRecords.Clear(); lock (obj) {
CRecords DbInfos = new CRecords(); DbInfos.load_Xml(value); for (int i = 0; i < DbInfos.Count; i++) { _ZDbRecords.Add(i, DbInfos.Record(i).OuterXml); } } } get { lock (this) {
CRecords DbInfos = new CRecords(); for (int i = 0; i < _ZDbRecords.Count; i++) {
CRecord myRec = new CRecord(); myRec.load_Xml(_ZDbRecords[i].ToString()); DbInfos.Add(myRec); } return DbInfos.OuterXml; } } } public String RPNQuery {
get {
Object obj = _RPNQuery; lock (obj) { return _RPNQuery; } } set
{
Object obj = _RPNQuery; lock(obj) { _RPNQuery = value; } } }
public void Start() {
//initialize default value
lock (this) { _iTotalFoundCount = 0; _iTotalReceivedCount = 0; _isDisposing = false; _ResultRecords.Clear();
//Decide SafeThreadArray Length
_iLastDatabaseIndex = _ZDbRecords.Count >
_iMaxThreadArraySize ? _iMaxThreadArraySize : _ZDbRecords.Count; for (int i = 0; i < _iLastDatabaseIndex; i++) {
ZetThread JThread; ZDb dbInfo = new ZDb();
dbInfo.load_Xml(_ZDbRecords[i].ToString()); JThread = new ZetThread(i, dbInfo.Name, dbInfo.OuterXml, _RPNQuery);
JThread.Send_Record += new
Thread_Send_Record(JThread_Send_Record);
JThread.Done += new Thread_Done(JThread_Done); JThread.Send_Size += new Thread_Send_FoundCount(JThread_Send_Size); JThread.Status_Changed += new Thread_Status_Changed(JThread_Status_Changed); _ThreadArray.Add(JThread); }
for (int i = 0; i < _ThreadArray.Count; i++) {
(_ThreadArray[i] as ZetThread).Start(); }
} }
public void JThread_Send_Record(ZetThread sender, String strValue) {
lock (this) {
if (!_isDisposing) {
int count = _ResultRecords.Count; if (count < _iCacheSize) { _ResultRecords.Add(count, strValue); sender.ReceivedCount++; count++; _iTotalReceivedCount = count; if(count==1) {
// raise a first found event
if (First_Found != null) { First_Found(this); } } if (Send_Status != null)
{ Send_Status(sender.Name + " Found = " + sender.FoundCount.ToString() + " Received=" + sender.ReceivedCount.ToString()); } } } } }
public void JThread_Status_Changed(int index, string message, int ReceivedCount, int FoundCount) {
Object obj = _ZDbRecords; lock (obj) { if (index < _ZDbRecords.Count) { ZDb dbInfo = new ZDb(); dbInfo.load_Xml(_ZDbRecords[index].ToString()); if (Send_Status != null) { Send_Status(dbInfo.Name + ": " + message); } dbInfo.StatusMessage = message; dbInfo.ReceivedCount = ReceivedCount; dbInfo.FoundCount = FoundCount; _ZDbRecords[index] = dbInfo.OuterXml; } } }
public void Dispose() {
lock (this) {
_isDisposing = true; for (int i = 0; i < _ThreadArray.Count; i++) {
ZetThread JT = _ThreadArray[i] as ZetThread; if (JT != null) { if (JT.IsAlive) { JT.Abort(); JT.Join(500); } } } _ThreadArray.Clear(); _iLastDatabaseIndex = 0; _ResultRecords.Clear(); _iTotalFoundCount = 0; _iTotalReceivedCount = 0; } }
private void JThread_Done(ZetThread DoneThread) {
lock (this) {
if ((!_isDisposing) &&
(_ThreadArray.Count<_iMaxThreadArraySize)) //prevent add new thread while dispose call { if (_iLastDatabaseIndex < _ZDbRecords.Count) { ZDb dbInfo = new ZDb(); dbInfo.load_Xml(_ZDbRecords[_iLastDatabaseIndex].ToString()); ZetThread JThread = new
ZetThread(_iLastDatabaseIndex, dbInfo.Name, dbInfo.OuterXml, _RPNQuery); JThread.Send_Record += new Thread_Send_Record(JThread_Send_Record); JThread.Done += new Thread_Done(JThread_Done); JThread.Send_Size += new Thread_Send_FoundCount(JThread_Send_Size); JThread.Status_Changed += new Thread_Status_Changed(JThread_Status_Changed); _ThreadArray.Add(JThread); _iLastDatabaseIndex++; JThread.Start(); } else if((_iLastDatabaseIndex ==
_ZDbRecords.Count) && (_ThreadArray.Count==0))//end of database list raise Finish the search session event
{ if (Search_Done != null) { Search_Done(this); } } } } }
private void JThread_Send_Size(int count) {
Object obj = _iTotalFoundCount; lock (obj)
{
_iTotalFoundCount += count; }
}
public String get_Record(int i) { if (!_isDisposing) { if (i >= _iTotalReceivedCount) return ""; else {
if (i >= (_iCacheSize - _iCacheStepSize) && (_iTotalFoundCount > _iCacheSize))
{
Object obj = _iCacheSize; lock (obj)
{
_iCacheSize += _iCacheStepSize; }
}
Object obj1 = _ResultRecords; lock (obj1)
{
return _ResultRecords[i].ToString(); }
} } { return ""; } }
public int TotalFoundCount {
get {
Object obj = _iTotalFoundCount; lock (obj) { return _iTotalFoundCount; } } }
public int TotalReceivedCount {
get {
Object obj = _iTotalReceivedCount; lock (obj) { return _iTotalReceivedCount; } } }
public bool IsAllThreadDead {
get {
Object obj = _ThreadArray; lock(obj) { return (_ThreadArray.Count == 0); } } } } } 3.3 Thử nghiệm 3.3.1 Cấu hình Cấu hình ứng dụng
Ứng dụng đƣợc xây dựng trên công nghệ ASP.NET 2.0 ngôn ngữ C#. Cần cài đặt trên hệ điều hành Windows 2003/2008 server với máy chủ web IIS 6.0 trở lên và Microsoft NET Framework 2.0 hoặc 3.x.
Sao chép chƣơng trình vào máy chủ và đăng ký thƣ mục ứng dụng web ASP.NET 2.0 (Virtua Directory) vào máy chủ web IIS.
Cấu hình danh mục cơ sở dữ liệu Z39.50
ZDatabasesInfo: vùng khai báo danh mục các cơ sở dữ liệu, key là mã biểu ghi thông tin, value là chuỗi thông tin về cơ sở dữ liệu với cấu trúc: “mã cơ sở dữ liệu, địa chỉ IP/tên, tên cơ sở dữ liệu, cổng, mô tả”.
<ZDatabasesInfo>
<add key="DB1" value="LOC,140.147.249.38,voyager,7090,TV Quốc Hội Mỹ"/>
<add key="DB2" value="NLA,catalogue.nla.gov.au,VOYAGER,7090,TV Quốc Gia Úc"/>
<add key="DB4" value="NGB,blpcz.bl.uk,BLPC-ALL,21021,TV Quốc Gia Anh"/>
<add key="DB5" value="NLC,amicus.nlc-bnc.ca,NL,210,TV Quốc Gia Canada"/>
<add key="DB6" value="NLM,tegument.nlm.nih.gov,voyager,7090,TV Y Khoa Mỹ"/>
</ZDatabasesInfo>
ZDbGroup: vùng khai báo các nhóm cơ sở dữ liệu hiển thị trong hộp thoại cho phép ngƣời dùng chọn lựa danh mục cơ sở dữ liệu tìm kiếm.
<ZDbGroup>
<add key="DBCAT0" value="LOC,NLA,NGB,NLC,NLM|Tất cả"/> <add key="DBCAT1" value="LOC|--TV Quốc Hội Mỹ"/>
<add key="DBCAT11" value="NLA|--TV Quốc Gia Úc"/> <add key="DBCAT12" value="NGB|--TV Quốc Gia Anh"/> <add key="DBCAT13" value="NLC|--TV Quốc Gia Canada"/> <add key="DBCAT14" value="NLM|--TV Y Khoa Quốc Gia Mỹ"/> </ZDbGroup>
ZRPNFields: Khai báo các trƣờng tìm kiếm theo tập thuộc tính kiểu RPN – reserve polish notation (ký hiệu đảo kiểu Balan).
<ZRPNFields>
<add key="RPNF0" value="@attr 1=1016|Mọi trường"/> <add key="RPNF1" value="@attr 1=4|Nhan đề"/>
<add key="RPNF2" value="@attr 1=1003|Tác giả"/> <add key="RPNF3" value="@attr 1=45|Chủ đề"/>
<add key="RPNF4" value="@attr 1=31|Năm xuất bản"/> </ZRPNFields>
3.3.2 Giao diện và kết quả thử nghiệm
Các phƣơng thức tìm kiếm: Chƣơng trình thử nghiệm cung cấp 2 phƣơng thức tìm kiếm, Tìm đơn giản và Tìm nâng cao.
Tìm đơn giản: Ngƣời sử dụng chọn một hoặc một nhóm cơ sở dữ liệu cần tìm,
chọn trƣờng cần tìm và đƣa vào từ khóa truy vấn, nhấn nút tìm để thực hiện.
Tìm nâng cao: Ngƣời sử dụng chọn một hoặc một nhóm cơ sở dữ liệu cần tìm,
chọn các trƣờng cần tìm và đƣa vào từ khóa truy vấn, chọn toán tử kết hợp các điều kiện tìm kiếm, tối đa là 3 điều kiện kết hợp với các toán tử AND và, OR hoặc, NOT không. Chƣơng trình sẽ kết hợp 2 điều kiện truy vấn đầu trƣớc sau đó kết hợp với điều kiện thứ 3. Nhấn nút tìm để thực hiện.
Hình 6: Giao diện tìm kiếm nâng cao
Kết quả tìm kiếm:
Kết quả tìm kiếm là danh sách các biểu ghi thỏa mãn đƣợc tải về từ các cơ sở dữ liệu đích không tuần tự - trộn lẫn nhau. Số lƣợng biểu ghi đƣợc tải về theo nhu cầu của ngƣời dùng để làm giảm bộ đệm không cần thiết. Ví dụ, khi bạn đọc mới mở trang đầu tiên của kết quả, chƣơng trình chỉ tải về tối đa 50 biểu ghi. Khi bạn đọc duyệt tới trang thứ 10 (5 biểu ghi/1 trang), chƣơng trình tiếp tục tải về bộ đệm 50 biểu ghi tiếp theo.
Biểu ghi nguyên gốc đƣợc lấy về dƣới định dạng MARCXML, chƣơng trình đã thêm vào trƣờng thông tin về cơ sở dữ liệu và hiển thị ra theo mẫu xslt – eXtensible Stylesheet Language Transformation có khả năng tùy biến.
Ở giao diện kết quả tìm kiếm, ngƣời dùng có thể di chuyển dễ dàng tới một trang trong danh mục, chuyển tới trang trƣớc hoặc trang sau. Ngƣời dùng có thể lựa chọn các biểu ghi cần thiết để đƣa vào danh mục lựa chọn, sau đó có thể tải về để phục vụ công tác xử lý tiếp theo.
Hình 7: Kết quả tìm kiếm qua Z39.50
Danh mục chọn từ Z39.50:
Các bản ghi đƣợc chọn lƣu trong bộ đệm chƣơng trình và hiển thị ra trong khung nhìn Danh mục chọn từ Z39.50 theo một mẫu xlst trình diễn có thể sửa đƣợc.
Ngƣời dùng có thể tải về các biểu ghi đã chọn và lƣu lại dƣới định dạng MARCXML. Ngƣời dùng có thể xóa các biểu ghi đã chọn trong bộ đệm để chọn danh mục mới.
Hình 8: Các biểu ghi đƣợc chọn
Để tiện cho việc theo dõi hoạt động của Z39.50 client với máy chủ. Chƣơng trình cung cấp khung nhìn Thông tin CSDL. Trên khung nhìn này, thông tin về các cơ sở dữ liệu Z39.50 trong phiên tìm kiếm đƣợc hiện ra đầy đủ.
- Mã: Mã cơ sở dữ liệu theo cấu hình - Tìm thấy: Số bản ghi thỏa mãn điều
kiện tìm kiếm của cơ sở dữ liệu này. - Lấy về: Số bản ghi đã tải về từ cơ sở
dữ liệu và lƣu vào bộ đệm của chƣơng trình.
- Tên nguồn: Tên nguồn tài nguyên - IP máy chủ: IP/ địa chỉ máy chủ - Tên CSDL: Tên cơ sở dữ liệu z39.50 - Cổng: Cổng làm việc của Z39.50 cho
cơ sở dữ liệu này. - Tình trạng:
o Running – đang làm việc
o Connect failed hoặc các lỗi cụ thể khác
o Finished – đã làm việc xong với cơ sở dữ liệu này, toàn bộ bản ghi nếu tìm thấy đã tải về hết.
Hình 9: Thông tin cơ sở dữ liệu
Đánh giá kết quả:
Ứng dụng đƣợc xây dựng thành công, đáp ứng mục tiêu tìm kiếm song song theo công nghệ đa luồng giao thức Z39.50. Quá trình thử nghiệm với số lƣợng cơ sở dữ liệu lớn tới hàng trăm chƣơng trình vẫn hoạt động bình thƣờng nhờ có giới hạn số luồng hoạt động đồng thời hợp lý (khoảng từ 10-30 tùy máy chủ và đƣờng truyền). Giải thuật khá tốt đáp ứng đƣợc hàng chục ngƣời dùng đồng thời trong quá trình thử nghiệm.
Ứng dụng đƣợc xây dựng mang tính chất trình diễn các chức năng cơ bản của một cổng tìm kiếm thông tin đa luồng giao thức Z39.50. Khi cần triển khai tích hợp cho một đơn vị cụ thể sẽ có thể đƣợc điều chỉnh và bổ sung thêm các tính năng khác.
PHẦN KẾT LUẬN
Thƣ viện điện tử là một bƣớc phát triển mạnh của ngành thƣ viện trong Nƣớc nói chung và trên toàn thế giới nói riêng, nó đã nâng cao hiệu quả trong