صف پیام سریع (FMQ)

اگر به دنبال پشتیبانی AIDL هستید، FMQ با AIDL را نیز ببینید.

زیرساخت فراخوانی روش راه دور (RPC) HIDL از مکانیزم‌های Binder استفاده می‌کند، به این معنی که تماس‌ها شامل سربار هستند، به عملیات هسته نیاز دارند و ممکن است اقدام زمان‌بندی را آغاز کنند. با این حال، برای مواردی که داده ها باید بین فرآیندهایی با سربار کمتر و بدون دخالت هسته منتقل شوند، از سیستم صف پیام سریع (FMQ) استفاده می شود.

FMQ صف های پیام را با ویژگی های مورد نظر ایجاد می کند. یک شی MQDescriptorSync یا MQDescriptorUnsync می تواند از طریق یک تماس HIDL RPC ارسال شود و توسط فرآیند دریافت برای دسترسی به صف پیام استفاده شود.

صف‌های پیام سریع فقط در C++ و دستگاه‌های دارای Android نسخه ۸.۰ و بالاتر پشتیبانی می‌شوند.

انواع MessageQueue

اندروید از دو نوع صف (معروف به طعم ها ) پشتیبانی می کند:

  • صف های غیرهمگام مجاز به سرریز شدن هستند و می توانند خواننده های زیادی داشته باشند. هر خواننده باید داده ها را به موقع بخواند یا آن را از دست بدهد.
  • صف های همگام سازی شده مجاز به سرریز شدن نیستند و فقط می توانند یک خواننده داشته باشند.

هر دو نوع صف مجاز به پایین آمدن نیستند (خواندن از صف خالی با شکست مواجه می شود) و فقط می توانند یک نویسنده داشته باشند.

هماهنگ نشده است

یک صف غیرهمگام فقط یک نویسنده دارد، اما می تواند هر تعداد خواننده داشته باشد. یک موقعیت نوشتن برای صف وجود دارد. با این حال، هر خواننده موقعیت خواندن مستقل خود را پیگیری می کند.

نوشتن در صف همیشه با موفقیت انجام می شود (برای سرریز بررسی نمی شود) تا زمانی که بزرگتر از ظرفیت صف پیکربندی شده نباشد (بزرگتر از ظرفیت صف می نویسد بلافاصله با شکست مواجه می شود). از آنجایی که هر خواننده ممکن است موقعیت خواندن متفاوتی داشته باشد، به جای اینکه منتظر بماند تا هر خواننده هر قطعه داده را بخواند، هر زمان که نوشته های جدید به فضا نیاز داشته باشند، داده ها از صف خارج می شوند.

خوانندگان مسئول بازیابی داده ها قبل از اینکه از انتهای صف بیفتند، هستند. خواندنی که تلاش می‌کند داده‌های بیشتری از موجود را بخواند یا فوراً با شکست مواجه می‌شود (اگر مسدود نباشد) یا منتظر می‌ماند تا داده‌های کافی در دسترس باشد (در صورت مسدود شدن). خواندنی که تلاش می‌کند داده‌های بیشتری از ظرفیت صف را بخواند، همیشه بلافاصله با شکست مواجه می‌شود.

اگر خواننده نتواند با رایتر همگام شود، به طوری که مقدار داده های نوشته شده و هنوز خوانده نشده توسط آن خواننده بیشتر از ظرفیت صف باشد، خواندن بعدی داده را بر نمی گرداند. در عوض، موقعیت خواندن خواننده را بازنشانی می کند تا با آخرین موقعیت نوشتن برابری کند و سپس شکست را برمی گرداند. اگر داده‌های موجود برای خواندن پس از سرریز بررسی شود، اما قبل از خواندن بعدی، داده‌های موجود برای خواندن بیشتر از ظرفیت صف را نشان می‌دهد، که نشان می‌دهد سرریز رخ داده است. (اگر صف بین بررسی داده های موجود و تلاش برای خواندن آن داده ها سرریز شود، تنها نشانه سرریز این است که خواندن ناموفق است.)

خوانندگان یک صف غیرهمگام احتمالاً نمی خواهند نشانگرهای خواندن و نوشتن صف را بازنشانی کنند. بنابراین، هنگام ایجاد صف از توصیفگر، خوانندگان باید از آرگومان «نادرست» برای پارامتر «resetPointers» استفاده کنند.

همگام شده است

یک صف هماهنگ دارای یک نویسنده و یک خواننده با یک موقعیت نوشتن و یک موقعیت خواندن واحد است. غیرممکن است که داده های بیشتری نسبت به صف موجود در صف بنویسید یا اطلاعات بیشتری را نسبت به صف موجود بخواند. بسته به اینکه تابع نوشتن یا خواندن مسدود یا غیرمسدود فراخوانی شود، تلاش می‌شود از فضای موجود یا داده‌ها فراتر رود، یا فوراً شکست را برمی‌گرداند یا تا زمانی که عملیات مورد نظر تکمیل شود، مسدود می‌شود. تلاش برای خواندن یا نوشتن داده های بیشتر از ظرفیت صف همیشه بلافاصله با شکست مواجه می شود.

یک FMQ راه اندازی کنید

یک صف پیام به چندین شیء MessageQueue نیاز دارد: یکی برای نوشتن، و یک یا چند مورد برای خواندن. هیچ پیکربندی واضحی وجود ندارد که کدام شی برای نوشتن یا خواندن استفاده می شود. این بر عهده کاربر است که اطمینان حاصل کند که هیچ شیئی هم برای خواندن و هم برای نوشتن استفاده نمی شود، حداکثر یک نویسنده وجود دارد، و برای صف های همگام، حداکثر یک خواننده وجود دارد.

اولین شی MessageQueue را ایجاد کنید

یک صف پیام با یک تماس ایجاد و پیکربندی می شود:

#include <fmq/MessageQueue.h>
using android::hardware::kSynchronizedReadWrite;
using android::hardware::kUnsynchronizedWrite;
using android::hardware::MQDescriptorSync;
using android::hardware::MQDescriptorUnsync;
using android::hardware::MessageQueue;
....
// For a synchronized non-blocking FMQ
mFmqSynchronized =
  new (std::nothrow) MessageQueue<uint16_t, kSynchronizedReadWrite>
      (kNumElementsInQueue);
// For an unsynchronized FMQ that supports blocking
mFmqUnsynchronizedBlocking =
  new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
      (kNumElementsInQueue, true /* enable blocking operations */);
  • مقداردهی اولیه MessageQueue<T, flavor>(numElements) یک شی را ایجاد و مقداردهی اولیه می کند که از عملکرد صف پیام پشتیبانی می کند.
  • مقداردهی اولیه MessageQueue<T, flavor>(numElements, configureEventFlagWord) یک شی را ایجاد و مقداردهی اولیه می کند که از عملکرد صف پیام با مسدود کردن پشتیبانی می کند.
  • flavor می تواند برای یک صف هماهنگ شده kSynchronizedReadWrite یا برای یک صف غیرهمگام kUnsynchronizedWrite باشد.
  • uint16_t (در این مثال) می‌تواند هر نوع تعریف‌شده توسط HIDL باشد که شامل بافرهای تودرتو (بدون نوع string یا vec )، دسته‌ها یا رابط‌ها نباشد.
  • kNumElementsInQueue اندازه صف را در تعداد ورودی ها نشان می دهد. اندازه بافر حافظه مشترکی که برای صف تخصیص داده می شود را تعیین می کند.

دومین شیء MessageQueue را ایجاد کنید

سمت دوم صف پیام با استفاده از یک شی MQDescriptor به دست آمده از سمت اول ایجاد می شود. شی MQDescriptor از طریق یک فراخوانی HIDL یا AIDL RPC به فرآیندی ارسال می شود که انتهای دوم صف پیام را نگه می دارد. MQDescriptor حاوی اطلاعاتی در مورد صف است، از جمله:

  • اطلاعاتی برای نگاشت بافر و نوشتن اشاره گر.
  • اطلاعاتی برای نگاشت نشانگر خواندن (اگر صف هماهنگ باشد).
  • اطلاعاتی برای نگاشت کلمه پرچم رویداد (اگر صف مسدود شده است).
  • نوع شی ( <T, flavor> ) که شامل نوع تعریف شده توسط HIDL از عناصر صف و طعم صف (همگام یا غیرهمگام) می شود.

شی MQDescriptor می تواند برای ساخت یک شی MessageQueue استفاده شود:

MessageQueue<T, flavor>::MessageQueue(const MQDescriptor<T, flavor>& Desc, bool resetPointers)

پارامتر resetPointers نشان می دهد که آیا هنگام ایجاد این شیء MessageQueue ، موقعیت های خواندن و نوشتن به 0 بازنشانی شود یا خیر. در یک صف غیرهمگام، موقعیت خواندن (که محلی برای هر شیء MessageQueue در صف‌های همگام‌نشده است) همیشه در حین ایجاد روی 0 تنظیم می‌شود. به طور معمول، MQDescriptor در طول ایجاد اولین شی صف پیام مقداردهی اولیه می شود. برای کنترل بیشتر بر روی حافظه مشترک، می توانید MQDescriptor به صورت دستی تنظیم کنید ( MQDescriptor در system/libhidl/base/include/hidl/MQDescriptor.h تعریف شده است) سپس هر شیء MessageQueue را همانطور که در این بخش توضیح داده شده است ایجاد کنید.

صف ها و پرچم های رویداد را مسدود کنید

به‌طور پیش‌فرض، صف‌ها از مسدود کردن خواندن/نوشتن پشتیبانی نمی‌کنند. دو نوع مسدود کردن تماس های خواندن/نوشتن وجود دارد:

  • فرم کوتاه ، با سه پارامتر (اشاره‌گر داده، تعداد موارد، مهلت زمانی). از مسدود کردن عملیات خواندن/نوشتن فردی در یک صف پشتیبانی می کند. هنگام استفاده از این فرم، صف پرچم رویداد و بیت ماسک ها را به صورت داخلی مدیریت می کند و اولین شی صف پیام باید با پارامتر دوم true مقداردهی شود. به عنوان مثال:
    // For an unsynchronized FMQ that supports blocking
    mFmqUnsynchronizedBlocking =
      new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
          (kNumElementsInQueue, true /* enable blocking operations */);
    
  • فرم طولانی ، با شش پارامتر (شامل پرچم رویداد و بیت ماسک). از استفاده از یک شی EventFlag مشترک بین چندین صف پشتیبانی می کند و اجازه می دهد تا ماسک های بیت اعلان مورد استفاده را مشخص کنید. در این مورد، پرچم رویداد و بیت ماسک باید برای هر تماس خواندن و نوشتن ارائه شود.

برای فرم طولانی، EventFlag می توان به طور صریح در هر فراخوان readBlocking() و writeBlocking() ارائه کرد. یکی از صفها ممکن است با یک پرچم رویداد داخلی مقداردهی شود، که سپس باید از اشیاء MessageQueue آن صف با استفاده از getEventFlagWord() استخراج شود و برای ایجاد اشیاء EventFlag در هر فرآیند برای استفاده با سایر FMQها استفاده شود. از طرف دیگر، اشیاء EventFlag می توانند با هر حافظه مشترک مناسب مقداردهی اولیه شوند.

به طور کلی، هر صف باید فقط از یکی از موارد غیر مسدود کننده، مسدود کردن فرم کوتاه یا بلوک طولانی استفاده کند. مخلوط کردن آنها خطایی نیست، اما برای به دست آوردن نتیجه مطلوب، برنامه ریزی دقیقی لازم است.

حافظه را به عنوان فقط خواندنی علامت گذاری کنید

به طور پیش فرض، حافظه مشترک دارای مجوز خواندن و نوشتن است. برای صف‌های همگام‌نشده ( kUnsynchronizedWrite )، نویسنده ممکن است بخواهد قبل از اینکه اشیاء MQDescriptorUnsync تحویل دهد، مجوزهای نوشتن را برای همه خواننده‌ها حذف کند. این تضمین می کند که سایر فرآیندها نمی توانند در صف بنویسند، که برای محافظت در برابر اشکالات یا رفتار بد در فرآیندهای خواننده توصیه می شود. اگر نویسنده بخواهد که خوانندگان بتوانند هر زمان که از MQDescriptorUnsync برای ایجاد سمت خواندنی صف استفاده می کنند، صف را بازنشانی کنند، در این صورت حافظه نمی تواند به عنوان فقط خواندنی علامت گذاری شود. این رفتار پیش‌فرض سازنده «MessageQueue» است. بنابراین، اگر از قبل کاربرانی از این صف وجود داشته باشند، کد آنها باید تغییر کند تا صف با resetPointer=false ساخته شود.

  • Writer: ashmem_set_prot_region را با یک توصیفگر فایل MQDescriptor و منطقه روی فقط خواندنی ( PROT_READ ) تماس بگیرید:
    int res = ashmem_set_prot_region(mqDesc->handle->data[0], PROT_READ)
  • Reader: ایجاد صف پیام با resetPointer=false (پیش‌فرض true است):
    mFmq = new (std::nothrow) MessageQueue(mqDesc, false);

از MessageQueue استفاده کنید

API عمومی شی MessageQueue این است:

size_t availableToWrite()  // Space available (number of elements).
size_t availableToRead()  // Number of elements available.
size_t getQuantumSize()  // Size of type T in bytes.
size_t getQuantumCount() // Number of items of type T that fit in the FMQ.
bool isValid() // Whether the FMQ is configured correctly.
const MQDescriptor<T, flavor>* getDesc()  // Return info to send to other process.

bool write(const T* data)  // Write one T to FMQ; true if successful.
bool write(const T* data, size_t count) // Write count T's; no partial writes.

bool read(T* data);  // read one T from FMQ; true if successful.
bool read(T* data, size_t count);  // Read count T's; no partial reads.

bool writeBlocking(const T* data, size_t count, int64_t timeOutNanos = 0);
bool readBlocking(T* data, size_t count, int64_t timeOutNanos = 0);

// Allows multiple queues to share a single event flag word
std::atomic<uint32_t>* getEventFlagWord();

bool writeBlocking(const T* data, size_t count, uint32_t readNotification,
uint32_t writeNotification, int64_t timeOutNanos = 0,
android::hardware::EventFlag* evFlag = nullptr); // Blocking write operation for count Ts.

bool readBlocking(T* data, size_t count, uint32_t readNotification,
uint32_t writeNotification, int64_t timeOutNanos = 0,
android::hardware::EventFlag* evFlag = nullptr) // Blocking read operation for count Ts;

//APIs to allow zero copy read/write operations
bool beginWrite(size_t nMessages, MemTransaction* memTx) const;
bool commitWrite(size_t nMessages);
bool beginRead(size_t nMessages, MemTransaction* memTx) const;
bool commitRead(size_t nMessages);

availableToWrite() و availableToRead() را می توان برای تعیین میزان انتقال داده در یک عملیات واحد استفاده کرد. در یک صف ناهمگام:

  • availableToWrite() همیشه ظرفیت صف را برمی‌گرداند.
  • هر خواننده موقعیت خواندن مخصوص به خود را دارد و محاسبه خود را برای availableToRead() انجام می دهد.
  • از نقطه نظر یک خواننده کند، صف مجاز به سرریز شدن است. این ممکن است منجر به این شود که availableToRead() مقداری بزرگتر از اندازه صف را برگرداند. اولین خواندن پس از سرریز ناموفق می شود و منجر به این می شود که موقعیت خواندن برای آن خواننده برابر با نشانگر نوشتن فعلی تنظیم شود، چه سرریز از طریق availableToRead() گزارش شده باشد یا نه.

متدهای read() و write() در صورتی که تمام داده‌های درخواستی می‌توانستند (و می‌شد) به/از صف منتقل شوند، true برمی‌گردانند. این روش ها مسدود نمی شوند. آن‌ها یا موفق می‌شوند (و true را برمی‌گردانند)، یا بلافاصله شکست ( false ) را برمی‌گردانند.

متدهای readBlocking() و writeBlocking() منتظر می‌مانند تا عملیات درخواستی تکمیل شود، یا تا زمان اتمام (مقدار timeOutNanos 0 به این معنی است که هرگز مهلت نمی‌رسد).

عملیات مسدود کردن با استفاده از کلمه پرچم رویداد اجرا می شود. به طور پیش‌فرض، هر صف کلمه پرچم خود را برای پشتیبانی از فرم کوتاه readBlocking() و writeBlocking() ایجاد می‌کند و از آن استفاده می‌کند. این امکان وجود دارد که چندین صف یک کلمه واحد را به اشتراک بگذارند، به طوری که یک فرآیند می تواند منتظر نوشتن یا خواندن هر یک از صف ها باشد. یک اشاره گر به کلمه پرچم رویداد صف را می توان با فراخوانی getEventFlagWord() به دست آورد، و آن اشاره گر (یا هر اشاره گر به یک مکان حافظه مشترک مناسب) می تواند برای ایجاد یک شی EventFlag استفاده شود تا به فرم طولانی readBlocking() منتقل شود و writeBlocking() برای یک صف دیگر. پارامترهای readNotification و writeNotification نشان می‌دهند که کدام بیت‌ها در پرچم رویداد باید برای سیگنال خواندن و نوشتن در آن صف استفاده شوند. readNotification و writeNotification بیت ماسک های 32 بیتی هستند.

readBlocking() روی بیت‌های writeNotification منتظر می‌ماند. اگر آن پارامتر 0 باشد، تماس همیشه ناموفق است. اگر مقدار readNotification 0 باشد، تماس با شکست مواجه نمی‌شود، اما خواندن موفقیت‌آمیز هیچ بیت اعلان را تنظیم نمی‌کند. در یک صف هماهنگ، این بدان معناست که فراخوانی مربوط به writeBlocking() هرگز بیدار نمی شود مگر اینکه بیت در جای دیگری تنظیم شده باشد. در یک صف غیر همگام، writeBlocking() منتظر نمی ماند (هنوز باید برای تنظیم بیت اعلان نوشتن استفاده شود)، و مناسب است برای خواندن هیچ بیت اعلان تنظیم نشود. به طور مشابه، در صورتی که readNotification 0 باشد، writeblocking() با شکست مواجه می شود و یک نوشتن موفق بیت های writeNotification مشخص شده را تنظیم می کند.

برای منتظر ماندن روی چندین صف به طور همزمان، از روش wait() یک شیء EventFlag استفاده کنید تا روی یک بیت ماسک از اعلان ها منتظر بمانید. متد wait() یک کلمه وضعیت را با بیت هایی که باعث تنظیم بیدار شدن شده اند برمی گرداند. سپس از این اطلاعات برای تأیید اینکه صف مربوطه دارای فضای کافی یا داده برای عملیات نوشتن/خواندن مورد نظر است و انجام یک write() / read() غیر مسدود کننده استفاده می شود. برای دریافت اعلان عملیات، از فراخوانی دیگری با متد wake() EventFlag استفاده کنید. برای تعریف انتزاع EventFlag ، به system/libfmq/include/fmq/EventFlag.h مراجعه کنید.

عملیات کپی صفر

APIهای read / write / readBlocking / writeBlocking() یک اشاره گر را به بافر ورودی/خروجی به عنوان آرگومان می گیرند و از فراخوانی های memcpy() به صورت داخلی برای کپی داده ها بین همان و بافر حلقه FMQ استفاده می کنند. برای بهبود عملکرد، Android 8.0 و بالاتر شامل مجموعه‌ای از APIها است که دسترسی اشاره گر مستقیم به بافر حلقه را فراهم می‌کند و نیاز به استفاده از تماس‌های memcpy را از بین می‌برد.

از APIهای عمومی زیر برای عملیات FMQ کپی صفر استفاده کنید:

bool beginWrite(size_t nMessages, MemTransaction* memTx) const;
bool commitWrite(size_t nMessages);

bool beginRead(size_t nMessages, MemTransaction* memTx) const;
bool commitRead(size_t nMessages);
  • متد beginWrite نشانگرهای پایه را در بافر حلقه FMQ ارائه می دهد. پس از نوشتن داده ها، آن را با استفاده از commitWrite() commit کنید. متدهای beginRead / commitRead به همین ترتیب عمل می کنند.
  • روش‌های beginRead / Write تعداد پیام‌های خوانده شده/نوشته شده را به عنوان ورودی می‌گیرند و یک بولی نشان می‌دهند که آیا خواندن/نوشتن امکان‌پذیر است یا خیر. اگر خواندن یا نوشتن امکان پذیر باشد، ساختار memTx با نشانگرهای پایه پر می شود که می توانند برای دسترسی مستقیم اشاره گر به حافظه مشترک بافر حلقه استفاده شوند.
  • ساختار MemRegion شامل جزئیات مربوط به یک بلوک حافظه، از جمله نشانگر پایه (آدرس پایه بلوک حافظه) و طول بر حسب T (طول بلوک حافظه بر حسب نوع تعریف شده توسط HIDL از صف پیام) است.
  • ساختار MemTransaction شامل دو ساختار MemRegion است، first و second به عنوان خواندن یا نوشتن در بافر حلقه ممکن است نیاز به بسته‌بندی در ابتدای صف داشته باشد. این بدان معناست که برای خواندن/نوشتن داده ها در بافر حلقه FMQ به دو نشانگر پایه نیاز است.

برای دریافت آدرس و طول پایه از ساختار MemRegion :

T* getAddress(); // gets the base address
size_t getLength(); // gets the length of the memory region in terms of T
size_t getLengthInBytes(); // gets the length of the memory region in bytes

برای دریافت ارجاع به اولین و دومین MemRegion در یک شی MemTransaction :

const MemRegion& getFirstRegion(); // get a reference to the first MemRegion
const MemRegion& getSecondRegion(); // get a reference to the second MemRegion

به عنوان مثال با استفاده از API های صفر کپی در FMQ بنویسید:

MessageQueueSync::MemTransaction tx;
if (mQueue->beginRead(dataLen, &tx)) {
    auto first = tx.getFirstRegion();
    auto second = tx.getSecondRegion();

    foo(first.getAddress(), first.getLength()); // method that performs the data write
    foo(second.getAddress(), second.getLength()); // method that performs the data write

    if(commitWrite(dataLen) == false) {
       // report error
    }
} else {
   // report error
}

روش های کمکی زیر نیز بخشی از MemTransaction هستند:

  • T* getSlot(size_t idx);
    یک اشاره گر به شکاف idx در MemRegions که بخشی از این شی MemTransaction هستند برمی‌گرداند. اگر شی MemTransaction نواحی حافظه را برای خواندن/نوشتن N مورد از نوع T نشان می دهد، آنگاه محدوده معتبر idx بین 0 و N-1 است.
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
    موارد nMessages از نوع T را در نواحی حافظه توصیف شده توسط شیء بنویسید، از فهرست startIdx شروع کنید. این روش از memcpy() استفاده می کند و قرار نیست برای عملیات کپی صفر استفاده شود. اگر شی MemTransaction نشان دهنده حافظه برای خواندن/نوشتن N مورد از نوع T باشد، آنگاه محدوده معتبر idx بین 0 و N-1 است.
  • bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
    روش کمکی برای خواندن موارد nMessages از نوع T از مناطق حافظه توصیف شده توسط شی که از startIdx شروع می شود. این روش از memcpy() استفاده می کند و برای عملیات کپی صفر استفاده نمی شود.

صف را از طریق HIDL ارسال کنید

در سمت ایجاد:

  1. شیء صف پیام را همانطور که در بالا توضیح داده شد ایجاد کنید.
  2. بررسی کنید که شی با isValid() معتبر است.
  3. اگر با ارسال یک EventFlag به فرم طولانی readBlocking() / writeBlocking() در صف های متعدد منتظر هستید، می توانید نشانگر پرچم رویداد (با استفاده از getEventFlagWord() ) را از یک شی MessageQueue که برای ایجاد پرچم مقداردهی اولیه شده است استخراج کنید، و از آن پرچم برای ایجاد شئ EventFlag لازم استفاده کنید.
  4. از متد MessageQueue getDesc() برای دریافت یک شی توصیفگر استفاده کنید.
  5. در فایل .hal به متد پارامتری از نوع fmq_sync بدهید یا fmq_unsync که در آن T یک نوع HIDL تعریف شده مناسب است. از این برای ارسال شیء برگشتی توسط getDesc() به فرآیند دریافت استفاده کنید.

در سمت دریافت کننده:

  1. از شی توصیفگر برای ایجاد یک شیء MessageQueue استفاده کنید. مطمئن شوید که از همان طعم صف و نوع داده استفاده کنید، در غیر این صورت قالب کامپایل نمی شود.
  2. اگر پرچم رویداد را استخراج کردید، پرچم را از شی MessageQueue مربوطه در فرآیند دریافت استخراج کنید.
  3. از شی MessageQueue برای انتقال داده استفاده کنید.