FreeRTOS-05-佇列

zhengcixi發表於2021-08-16

說明

本文僅作為學習FreeRTOS的記錄文件,作為初學者肯定很多理解不對甚至錯誤的地方,望網友指正。
FreeRTOS是一個RTOS(實時作業系統)系統,支援搶佔式、合作式和時間片排程。適用於微處理器或小型微處理器的實時應用。
本文件使用的FreeRTOS版本:FreeRTOS Kernel V10.4.1
參考文件:《FreeRTOS_Reference_Manual_V10.0.0.pdf》《FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf》《STM32F4 FreeRTOS開發手冊_V1.1.pdf》
參考視訊:正點原子FreeRTOS手把手教學-基於STM32_嗶哩嗶哩_bilibili

8 佇列

8.1 簡介

佇列是為了任務與任務、任務與中斷之間的通訊而準備的,可以在任務與任務、任務與中斷之間傳遞訊息,佇列中可以儲存有限的、大小固定的資料專案。佇列所能儲存的最大資料專案數量叫做佇列的長度,建立佇列的時候會指定資料專案的大小和佇列的長度。佇列是用來傳遞訊息的,所以也稱為訊息佇列。訊號量也是依據佇列實現的。

8.1.1 資料儲存

佇列採用先進先出(FIFO)的儲存緩衝機制,往佇列中放資料叫做入隊(放隊尾),從佇列中取資料叫做出隊(從隊頭)。也可以使用LIFO的儲存緩衝,也就是後進先出,FreeRTOS也提供了LIFO的儲存緩衝機制。

資料存放到佇列中會導致值拷貝,也就是放資料到佇列中,而不是資料的指標,這叫值傳遞。採用值傳遞,當訊息放到佇列中後原始的資料緩衝區就可以刪除,緩衝區就可以重複使用。FreeRTOS使用的是資料拷貝,但是也可以採用引用(指標)來傳遞訊息,直接往佇列中放入傳送訊息緩衝區的地址的指標。

8.1.2 多工訪問

佇列不屬於某個特定的任務,任何任務都可以向佇列中傳送訊息,或者從佇列中提取訊息。

8.1.3 出隊阻塞

當任務從一個佇列中提取訊息的時候可以指定一個阻塞時間,這個阻塞時間就是當任務從佇列中提取訊息無效的時候任務阻塞的時間。出隊就是從佇列中提取訊息,出隊阻塞是針對從佇列中提取訊息的任務而言的。阻塞時間的單位是時鐘節拍數,阻塞時間為0就是不阻塞。如果阻塞時間是0~portMAX_DELAY,當任務沒有從佇列中獲取到訊息的話就會進入阻塞態,阻塞時間指定了任務進入阻塞態的時間,當阻塞時間到了以後還沒有接收到訊息就立即退出阻塞態;如果在阻塞時間收到了資料就立即返回。當阻塞時間設定為portMAX_DELAY,任務就會一直進入阻塞態等待,直到接收到資料為止。

8.1.4 入隊阻塞

入隊就是往佇列中傳送訊息,將訊息加入到佇列中。入隊也可以設定阻塞時間。比如入隊時,佇列已經滿了。

8.1.5 佇列操作過程

建立佇列:建立一個佇列,用於任務A和任務B之間通訊,佇列資料專案的個數為5,建立時佇列為空。

image-20210808195543746

往佇列中傳送第一個訊息:任務A往佇列中傳送了一個訊息,值為10。

image-20210808195721559

往佇列中再傳送一個訊息:任務A往佇列中再傳送了一個訊息,值為20,這時佇列剩餘空間大小為3。

image-20210808200109085

從佇列中取一個訊息:任務B從佇列中取一個訊息,從佇列頭開始取(值為10),佇列中剩下一個訊息,剩餘空間大小為4。

image-20210808200219586

下圖演示了一次完整的任務A和任務B通過佇列傳遞訊息的過程:

img

8.2 佇列結構體

佇列結構體為Queue_t,在queue.c中定義:

typedef struct QueuePointers
{
    int8_t * pcTail;      //指向佇列儲存區最後一個位元組
    int8_t * pcReadFrom;  //作為佇列使用時指向最後一個出隊的佇列項首地址
} QueuePointers_t;

typedef struct SemaphoreData
{
    TaskHandle_t xMutexHolder;
    UBaseType_t uxRecursiveCallCount;  //作為遞迴互斥量的時候用來記錄遞迴互斥量被呼叫的次數
} SemaphoreData_t;

typedef struct QueueDefinition
{
    int8_t * pcHead;     //指向佇列儲存區開始地址
    int8_t * pcWriteTo;  //指向儲存區中下一個空閒區域

    union
    {
        QueuePointers_t xQueue;
        SemaphoreData_t xSemaphore;
    } u;

    List_t xTasksWaitingToSend;     //等待傳送任務列表,因隊滿導致入隊失敗而阻塞的任務掛在這個列表上
    List_t xTasksWaitingToReceive;  //等待接收任務列表,因隊空導致出隊失敗而阻塞的任務掛在這個列表上

    volatile UBaseType_t uxMessagesWaiting; //佇列中當前資料項數量,也就是訊息數
    UBaseType_t uxLength;   //佇列建立時指定的佇列長度,佇列允許的最大佇列項個數
    UBaseType_t uxItemSize; //佇列建立時指定的每個佇列項最大長度,單位位元組

    volatile int8_t cRxLock;  //當佇列上鎖後用來統計接收到的佇列項個數,也就是出隊的佇列項個數
    volatile int8_t cTxLock;  //當佇列上鎖後用來統計傳送到佇列的佇列項個數,也就是入隊的佇列項個數

    #if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
        uint8_t ucStaticallyAllocated;  //如果使用靜態儲存,則設定欄位值為pdTURE
    #endif

    #if ( configUSE_QUEUE_SETS == 1 )
        struct QueueDefinition * pxQueueSetContainer;
    #endif

    #if ( configUSE_TRACE_FACILITY == 1 )
        UBaseType_t uxQueueNumber;
        uint8_t ucQueueType;
    #endif
} xQUEUE;

typedef xQUEUE Queue_t;

8.3 佇列建立

8.3.1 動態佇列建立

函式原型:

#include “FreeRTOS.h”
#include “queue.h”
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,
                           UBaseType_t uxItemSize );

函式描述:建立一個新的佇列,並返回指向佇列的控制程式碼。使用這個函式需要將巨集configSUPPORT_DYNAMIC_ALLOCATION置為1。每一個佇列需要儲存空間來存放佇列的狀態和佇列項,如果使用xQueueCreate()建立佇列,則儲存空間由系統自動分配。如果使用xQueueCreateStatic()建立,儲存空間由使用者指定。

函式引數:uxQueueLength:要建立的佇列項個數。uxItemSize:單個佇列項的大小,單位為位元組。

返回值:NULL:表示分配儲存空間失敗。其它值:佇列建立成功,返回值是指向建立佇列的控制程式碼。

8.3.2 靜態佇列建立

函式原型:

#include “FreeRTOS.h”
#include “queue.h”
QueueHandle_t xQueueCreateStatic( UBaseType_t uxQueueLength,
                                 UBaseType_t uxItemSize,
                                 uint8_t *pucQueueStorageBuffer,
                                 StaticQueue_t *pxQueueBuffer );

函式描述:建立一個新的佇列,並返回指向佇列的控制程式碼。儲存空間由使用者指定。

函式引數:uxQueueLength:要建立的佇列項個數。

uxItemSize:單個佇列項的大小,單位為位元組。

pucQueueStorageBuffer:指向佇列專案的儲存區,也就是訊息儲存區,儲存區要大於等於uxQueueLength * uxItemSizede 位元組。

pxQueueBuffer:指向用來存放佇列結構體的空間。

返回值:NULL:表示分配儲存空間失敗。其它值:佇列建立成功,返回值是指向建立佇列的控制程式碼。

佇列建立的具體實現過程可以參考原始碼,這裡給出建立一個有4個佇列項,每個佇列項長度為32位元組的佇列成功的示意圖。

image-20210808204617758

8.4 向佇列傳送訊息

向佇列中傳送訊息分為任務級入隊函式和中斷級入隊函式。

8.4.1 任務級入隊函式

任務級入隊函式有3個,分別為xQueueSend()、xQueueSendToFront()、xQueueSendToBack() 、xQueueOverwrite()。

先介紹xQueueSend()、xQueueSendToFront()、xQueueSendToBack() 。

函式原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xQueueSend( QueueHandle_t xQueue, 
                      const void * pvItemToQueue,
                      TickType_t xTicksToWait );
BaseType_t xQueueSendToFront( QueueHandle_t xQueue, 
                             const void * pvItemToQueue,
                             TickType_t xTicksToWait );
BaseType_t xQueueSendToBack( QueueHandle_t xQueue,
                            const void * pvItemToQueue,
                            TickType_t xTicksToWait );

函式描述:傳送一個佇列項到佇列頭或者佇列尾。xQueueSend()和xQueueSendToBack()是同樣的操作,都是傳送資料到佇列的尾部,xQueueSend()是原始版本,當前使用xQueueSendToBack()替換它。xQueueSendToFront()是傳送資料到佇列項的首部。

函式引數:xQueue:佇列控制程式碼,由建立佇列的函式返回。

pvItemToQueue:指向待傳送的佇列項的指標,佇列項是拷貝到佇列中的。佇列項的大小由建立佇列的時候指定。

xTicksToWait:阻塞時間,此參數列示佇列滿的時候任務進入阻塞態等待佇列空閒的最大時間。如果為0,任務立即返回;如果為portMAX_DELAY,任務會一直死等,巨集portMAX_DELAY需要置為1;其餘值為等到的時鐘計數值,可使用巨集pdMS_TO_TICKS()轉換時鐘計數值為毫秒。

返回值:pdPASS:資料成功傳送到佇列;errQUEUE_FULL:佇列滿,訊息傳送失敗。

另外還有xQueueOverwrite()函式為訊息覆蓋寫函式。

函式原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xQueueOverwrite( QueueHandle_t xQueue, const void *pvItemToQueue );

函式描述:向佇列中傳送資料,當佇列滿了以後會覆蓋掉舊的資料,不管這個舊的資料有沒有被其它任務或中斷取走。這個函式常用於向那些長度為1的佇列傳送訊息。

函式引數:xQueue:佇列控制程式碼,由建立佇列的函式返回。

pvItemToQueue:指向待傳送的佇列項的指標,佇列項是拷貝到佇列中的。佇列項的大小由建立佇列的時候指定。

返回值:pdPASS:資料成功傳送到佇列,因為佇列滿了之後會覆蓋寫,所以不存在失敗的情況。

8.4.2 中斷級入隊函式

任務級入隊函式有3個,分別為xQueueSendFromISR()、xQueueSendToBackFromISR()、xQueueSendToFrontFromISR() 、xQueueOverwriteFromISR ()。

先介紹xQueueSendFromISR()、xQueueSendToBackFromISR()、xQueueSendToFrontFromISR()。

函式原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xQueueSendFromISR( QueueHandle_t xQueue,
                             const void *pvItemToQueue,
                             BaseType_t *pxHigherPriorityTaskWoken );
BaseType_t xQueueSendToBackFromISR( QueueHandle_t xQueue,
                                   const void *pvItemToQueue,
                                   BaseType_t *pxHigherPriorityTaskWoken );
BaseType_t xQueueSendToFrontFromISR( QueueHandle_t xQueue,
                                    const void *pvItemToQueue,
                                    BaseType_t *pxHigherPriorityTaskWoken );

函式描述:傳送一個佇列項到佇列頭或者佇列尾,用於中斷服務函式中。xQueueSendFromISR()和xQueueSendToBackFromISR()是同樣的操作,都是傳送資料到佇列的尾部。xQueueSendToFrontFromISR()是傳送資料到佇列項的首部。和任務級入隊函式不同的是中斷級入隊函式不允許指定任務的阻塞時間。

函式引數:xQueue:佇列控制程式碼,由建立佇列的函式返回。

pvItemToQueue:指向待傳送的佇列項的指標,佇列項是拷貝到佇列中的。佇列項的大小由建立佇列的時候指定。

pxHigherPriorityTaskWoken:標記退出此函式以後是否進行任務切換,這個變數的值由函式指定,使用者不進行設定,使用者僅需要提供一個變數來儲存值。當此值為pdTRUE的時候在退出中斷服務函式之前一定要進行一次任務切換。

返回值:pdPASS:資料成功傳送到佇列;errQUEUE_FULL:佇列滿,訊息傳送失敗。

另外xQueueOverwriteFromISR()函式為訊息覆蓋寫函式。

函式原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xQueueOverwriteFromISR( QueueHandle_t xQueue,
                                  const void *pvItemToQueue,
                                  BaseType_t *pxHigherPriorityTaskWoken );

函式描述:向佇列中傳送資料,當佇列滿了以後會覆蓋掉舊的資料,從隊尾寫入。

函式引數:xQueue:佇列控制程式碼,由建立佇列的函式返回。

pvItemToQueue:指向待傳送的佇列項的指標,佇列項是拷貝到佇列中的。佇列項的大小由建立佇列的時候指定。

pxHigherPriorityTaskWoken:標記退出此函式以後是否進行任務切換,這個變數的值由函式指定,使用者不進行設定,使用者僅需要提供一個變數來儲存值。當此值為pdTRUE的時候在退出中斷服務函式之前一定要進行一次任務切換。

返回值:pdPASS:資料成功傳送到佇列,因為佇列滿了之後會覆蓋寫,所以不存在失敗的情況。

8.5 從佇列中讀取訊息

出隊就是從佇列中獲取佇列項。也分為任務級出隊函式和中斷級出隊函式。

8.4.1 任務級出隊函式

任務級出隊函式有2個,分別為xQueueReceive()、xQueuePeek()。

函式原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xQueueReceive( QueueHandle_t xQueue,
                         void *pvBuffer,
                         TickType_t xTicksToWait );

函式描述:從佇列中讀取一個佇列項,讀取成功以後就會將佇列中的這條訊息刪除。讀取訊息時是採用的拷貝方式,所以使用者需要提供一個陣列或緩衝區來儲存讀取到的資料,所讀取的資料長度是佇列建立的時候設定的每個佇列項的長度。

函式引數:xQueue:佇列控制程式碼,由建立佇列的函式返回。

pvBuffer:儲存資料的緩衝區,讀取佇列的過程中會將讀取到的資料拷貝到這個緩衝區中。

xTicksToWait:阻塞時間,此參數列示佇列空的時候任務進入阻塞態等待佇列空閒的最大時間。如果為0,任務立即返回;如果為portMAX_DELAY,任務會一直死等,巨集portMAX_DELAY需要置為1;其餘值為等到的時鐘計數值,可使用巨集pdMS_TO_TICKS()轉換時鐘計數值為毫秒。

返回值:pdPASS:成功從佇列中讀取到資料;errQUEUE_EMPTY:佇列空,訊息讀取失敗。

函式原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xQueuePeek( QueueHandle_t xQueue,
                      void *pvBuffer,
                      TickType_t xTicksToWait );

函式描述:從佇列中讀取一個佇列項,讀取成功以後不會將佇列中的這條訊息刪除。下一次讀取的時候仍然可以從這個佇列中讀取這個訊息。讀取訊息時是採用的拷貝方式,所以使用者需要提供一個陣列或緩衝區來儲存讀取到的資料,所讀取的資料長度是佇列建立的時候設定的每個佇列項的長度。

函式引數:xQueue:佇列控制程式碼,由建立佇列的函式返回。

pvBuffer:儲存資料的緩衝區,讀取佇列的過程中會將讀取到的資料拷貝到這個緩衝區中。

xTicksToWait:阻塞時間,此參數列示佇列空的時候任務進入阻塞態等待佇列空閒的最大時間。如果為0,任務立即返回;如果為portMAX_DELAY,任務會一直死等,巨集portMAX_DELAY需要置為1;其餘值為等到的時鐘計數值,可使用巨集pdMS_TO_TICKS()轉換時鐘計數值為毫秒。

返回值:pdPASS:成功從佇列中讀取到資料;errQUEUE_EMPTY:佇列空,訊息讀取失敗。

8.4.2 中斷級出隊函式

中斷級出隊函式xQueueReceiveFromISR()、xQueuePeekFromISR ()。

函式原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxHigherPriorityTaskWoken );

函式描述:用於在中斷服務函式中從佇列中讀取一條訊息,讀取成功後就會將佇列中的這條資料刪除。讀取訊息的方式採用值拷貝方式,需要使用者提供一個陣列或者緩衝區來儲存讀取到的資料,所讀取的資料長度是建立佇列的時候指定的每個佇列項的長度。

函式引數:xQueue:佇列控制程式碼,由建立佇列的函式返回。

pvBuffer:儲存資料的緩衝區,讀取佇列的過程中會將讀取到的資料拷貝到這個緩衝區中。

pxHigherPriorityTaskWoken:標記退出此函式以後是否進行任務切換,這個變數的值由函式指定,使用者不進行設定,使用者僅需要提供一個變數來儲存值。當此值為pdTRUE的時候在退出中斷服務函式之前一定要進行一次任務切換。

返回值:pdPASS:成功從佇列中讀取到資料;pdFAIL:佇列空,訊息讀取失敗。

函式原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue, void *pvBuffer );

函式描述:xQueuePeek()的中斷版本,函式讀取成功以後不會將訊息刪除。

函式引數:xQueue:佇列控制程式碼,由建立佇列的函式返回。

pvBuffer:儲存資料的緩衝區,讀取佇列的過程中會將讀取到的資料拷貝到這個緩衝區中。

返回值:pdPASS:成功從佇列中讀取到資料;pdFAIL:佇列空,訊息讀取失敗。

8.6 佇列操作實驗

目的:熟悉佇列的使用

設計:建立兩個任務,任務task0每隔2秒傳送1個資料到佇列中,並每隔4秒檢查佇列餘量;task1讀取佇列中的訊息,阻塞時間為5秒。佇列的容量為4,每個佇列項的大小為4位元組。

測試程式碼:

/* task00 info */
configSTACK_DEPTH_TYPE Task00_STACK_SIZE = 5;
UBaseType_t  Task00_Priority = 1;
TaskHandle_t Task00_xHandle;

/* task01 info */
configSTACK_DEPTH_TYPE Task01_STACK_SIZE = 5;
UBaseType_t  Task01_Priority = 1;
TaskHandle_t Task01_xHandle;

#define MSG_QUEUE_NUM        4
#define MSG_QUEUE_ITEM_SIZE  4
QueueHandle_t Message_Queue;


void check_msg_queue(void)
{
    unsigned char *p = NULL;
    unsigned char msgq_remain_size = 0;
    unsigned char msgq_total_size = 0;

    taskENTER_CRITICAL();
    msgq_remain_size = uxQueueSpacesAvailable(Message_Queue);
    msgq_total_size = uxQueueMessagesWaiting(Message_Queue) + msgq_remain_size;
    PRINT(" total size:%d  remain size:%d", msgq_total_size, msgq_remain_size);
    taskEXIT_CRITICAL();
}


void vTask00_Code(void *para)
{
    static unsigned int cnt = 0;
    BaseType_t err;
    unsigned int item = 0;
    for (;;)
    {
        PRINT(" task00 cnt %u...", cnt);
        if (cnt%2 == 0) {
            item = cnt*100 + 1;
            err = xQueueSend(Message_Queue, &item, pdMS_TO_TICKS(5000));
            if (err == errQUEUE_FULL)
                PRINT("queue full!");
        }
        if (cnt%4 == 0)
            check_msg_queue();
        cnt++;
        vTaskDelay(1000);
    }
}

void vTask01_Code(void *para)
{
    static unsigned int cnt = 0;
    unsigned int item = 0;
    BaseType_t err;
    for (;;)
    {
        PRINT(" task01 cnt %u...", cnt);
        err = xQueueReceive(Message_Queue, &item, pdMS_TO_TICKS(5000));
        if (err == errQUEUE_EMPTY)
            PRINT("queue empty!");
        else
            PRINT(" item: %d", item);
        cnt++;
        vTaskDelay(1000);
    }
}

void test_queue()
{
    Message_Queue = xQueueCreate(MSG_QUEUE_NUM, MSG_QUEUE_ITEM_SIZE);
    
    if (xTaskCreate(vTask00_Code, "task00 task", 
        Task00_STACK_SIZE, NULL, Task00_Priority,
        &Task00_xHandle) != pdPASS)
    {
        PRINT("creat task00 failed!\n");
    }
    
    if (xTaskCreate(vTask01_Code, "task01 task", 
        Task01_STACK_SIZE, NULL, Task01_Priority,
        &Task01_xHandle) != pdPASS)
    {
        PRINT("creat task01 failed!\n");
    }
}

void creat_task(void)
{
    test_queue();
}

編譯、執行,結果如下:

$ ./build/freertos-simulator 
 task00 cnt 0...
 total size:4  remain size:3
 task01 cnt 0...
 item: 1
 task00 cnt 1...
 task01 cnt 1...
 task00 cnt 2...
 item: 201
 task00 cnt 3...
 task01 cnt 2...
 task00 cnt 4...
 total size:4  remain size:3
 item: 401
 task00 cnt 5...
 task01 cnt 3...
 task00 cnt 6...
 item: 601
 task00 cnt 7...
 task01 cnt 4...

可以看出,task01會阻塞性的等待佇列中的值(見item:201)。

下面測試佇列空的情況,task00不往佇列中放入資料:

void vTask00_Code(void *para)
{
    static unsigned int cnt = 0;
    BaseType_t err;
    unsigned int item = 0;
    for (;;)
    {
        PRINT(" task00 cnt %u...", cnt);
        if (cnt%4 == 0)
            check_msg_queue();
        cnt++;
        vTaskDelay(1000);
    }
}

void vTask01_Code(void *para)
{
    static unsigned int cnt = 0;
    unsigned int item = 0;
    BaseType_t err;
    for (;;)
    {
        PRINT(" task01 cnt %u...", cnt);
        err = xQueueReceive(Message_Queue, &item, pdMS_TO_TICKS(5000));
        if (err == errQUEUE_EMPTY)
            PRINT("queue empty!");
        else
            PRINT(" item: %d", item);
        cnt++;
        vTaskDelay(1000);
    }
}

編譯、執行,結果如下,task01阻塞了5秒:

$ ./build/freertos-simulator 
 task00 cnt 0...
 total size:4  remain size:4
 task01 cnt 0...
 task00 cnt 1...
 task00 cnt 2...
 task00 cnt 3...
 task00 cnt 4...
 total size:4  remain size:4
queue empty!
 task00 cnt 5...
 task01 cnt 1...
 task00 cnt 6...

下面測試佇列空的情況,task01不從佇列中取資料:

void vTask00_Code(void *para)
{
    static unsigned int cnt = 0;
    BaseType_t err;
    unsigned int item = 0;
    for (;;)
    {
        PRINT(" task00 cnt %u...", cnt);
        item = cnt*100 + 1;
        err = xQueueSend(Message_Queue, &item, pdMS_TO_TICKS(5000));
        if (err == errQUEUE_FULL)
        	PRINT("queue full!");
        if (cnt%4 == 0)
            check_msg_queue();
        cnt++;
        vTaskDelay(1000);
    }
}

void vTask01_Code(void *para)
{
    static unsigned int cnt = 0;
    unsigned int item = 0;
    BaseType_t err;
    for (;;)
    {
        PRINT(" task01 cnt %u...", cnt);
        cnt++;
        vTaskDelay(1000);
    }
}

編譯、執行,結果如下,task00往佇列中放了4個資料項之後,就阻塞了,每次阻塞時間為5秒:

$ ./build/freertos-simulator 
 task00 cnt 0...
 total size:4  remain size:3
 task01 cnt 0...
 task00 cnt 1...
 task01 cnt 1...
 task00 cnt 2...
 task01 cnt 2...
 task00 cnt 3...
 task01 cnt 3...
 task00 cnt 4...
 task01 cnt 4...
 task01 cnt 5...
 task01 cnt 6...
 task01 cnt 7...
 task01 cnt 8...
queue full!
 total size:4  remain size:0
 task01 cnt 9...
 task00 cnt 5...
 task01 cnt 10...
 task01 cnt 11...
 task01 cnt 12...
 task01 cnt 13...
 task01 cnt 14...
queue full!
 task01 cnt 15...
 task00 cnt 6...

相關文章