FreeRTOS-信号量

  • 信号量其实就是队列的一种应用,信号量的各种操作都是在队列的基础上建立起来的。那么既然是在队列的基础上建立的,信号量一定具有和队列相同的属性。因此信号量也是为任务和任务、任务和中断之间通信做准备的,但是信号量一般用来进行资源管理和任务同步。因为信号量是一种共享资源,当它被创建之后,系统中所有任务和中断都能对信号量进行访问。同时也可以进行任务同步,即在一个任务(或中断)中告诉另一个任务它所等待的事件发生了,等到发生任务调度的时候,再切换到相应任务中,执行该事件发生的相关处理。FreeRTOS中的信号量有二值信号量、互斥信号量、计数信号量、递归互斥信号量,我们这里只讲述前三个。

相关API

  • 信号量使用的API函数最核心的就是xQueueGenericCreate()、xQueueGenericSend()、xQueueGenericReceive()这三个函数,看到这三个函数是不是有点眼熟?没错,这就是上一章中队列所使用的三个函数,只不过在信号量中对其做了一些封装而已,庆幸的是我们上一章已经着重分析过这些函数了,所以本章将不会再长篇大论的去分析这些源码了。下面来看一下信号量中的函数是如何对其进行封装的。代码定义如下。
#define semSEMAPHORE_QUEUE_ITEM_LENGTH       ( ( uint8_t ) 0U )
#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )#define semGIVE_BLOCK_TIME                   ( ( TickType_t ) 0U )
#define xSemaphoreGive( xSemaphore )        xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )#define xSemaphoreTake( xSemaphore, xBlockTime )      xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), NULL, ( xBlockTime ), pdFALSE )
  • xSemaphoreCreateBinary()是创建二值信号量的函数,从定义可见,该函数调用了xQueueGenericCreate(),只不过定义的队列长度是1,队列项大小为0。那么这样问题来了,队列项大小为0的队列怎么入队和出队呢?这个无需着急,接下来我们会详细分析。
  • xSemaphoreGive() 是发送信号量,只要是要发送信号量,不管是二值信号量、计数信号量、互斥信号量还是递归互斥信号量都是调用的这个函数。该函数调用了xQueueGenericSend()函数,从中可见,这时候入队的队列项是NULL,相应的将不会占用空间,所以上面在创建信号量的时候为其分配的空间是0也就能解释一部分了,但是问题还没有解决,既然队列始终是空的,那么如何来判断是否有信号发送呢?其实是根据uxMessagesWaiting这个变量进行判断的,因为即使我们入队的是一个空队列项,那么如果说这是候满足uxMessagesWaiting<uxLength这个条件,依旧会进行入队操作,既然会进行入队操作,那么根据我们上一章讲述的,uxMessagesWaiting必然会+1,如果这时候我们规定uxLength=1,那么uxMessagesWaiting只能在0到1之间取值,所以就相当于该信号量只有0、1两种状态,所以叫二值信号量。如果我们规定的uxLength>1,那么uxMessagesWaiting最大计数值就为uxLength,所以这时候就是计数信号量。
  • xSemaphoreTake()是接收信号量,和发送信号量一样,任何类型的信号量接收都会调用这个函数。该函数调用了xQueueGenericReceive()函数,输入参数为信号量句柄和阻塞时间,最后一个输入参数为pdFALSE,设置接收后是否要删除信号量中的信号,这里设置为删除。同样我们可以看到,接收的信号量的时候传入的接收数组也是NULL,对照上面的发送信号量可以知道,接收信号量其实也是操作uxMessagesWaiting的,因为这部分代码上章节中没有分析,这里简要分析一下源码。
...
//非主要源码忽略
...
if( uxMessagesWaiting > ( UBaseType_t ) 0 )---1{pcOriginalReadPosition = pxQueue->u.pcReadFrom;prvCopyDataFromQueue( pxQueue, pvBuffer );---2if( xJustPeeking == pdFALSE )---3{traceQUEUE_RECEIVE( pxQueue );/* Actually removing data, not just peeking. */pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;----4...//互斥信号量相关的暂时忽略...if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )----5{if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ){queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}else//xJustPeeking == pdTRUE    ----6{traceQUEUE_PEEK( pxQueue );pxQueue->u.pcReadFrom = pcOriginalReadPosition;if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ){if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ){/* The task waiting has a higher priority than this task. */queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}taskEXIT_CRITICAL();return pdPASS;----7}
else----8
{...
//这一部分就是设置阻塞时间
...
}
  1. 队列不为空
  2. 将读取队列项,注意读取的时候是不会自动进行uxMessagesWaiting -1操作的
  3. 判断是否读取后要移除队列项
  4. 因为读取后要移除队列项,那么队列中队列项的数量-1
  5. 执行移除队列项操作
  6. 如果读取后不溢出队列项,那么相应的队列中队列项的数量不变
  7. 如果队列是空的,那么就没有办法读取队列项,所以设置阻塞时间,详细的代码上章节已经分析过了,这里不再分析了
  • 从上面的分析中可以看出,当要进行信号量读取的时候,本质上还是根据uxMessagesWaiting来操作的。
  • 因为计数信号量和互斥信号量创建的封装过程要比二值信号量稍微复杂一点,所以没在上面分析,下面我们就来分析这两种信号量的创建。
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )QueueHandle_t xQueueCreateCountingSemaphore( const UBaseType_t uxMaxCount, const UBaseType_t uxInitialCount ){QueueHandle_t xHandle;configASSERT( uxMaxCount != 0 );configASSERT( uxInitialCount <= uxMaxCount );xHandle = xQueueGenericCreate( uxMaxCount, queueSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_COUNTING_SEMAPHORE );if( xHandle != NULL ){( ( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount;traceCREATE_COUNTING_SEMAPHORE();}else{traceCREATE_COUNTING_SEMAPHORE_FAILED();}return xHandle;}
  • xSemaphoreCreateCounting()其实是调用了xQueueCreateCountingSemaphore()函数,输入参数为最大计数量和初始计数值。在xQueueCreateCountingSemaphore()中我们可以看出,其实最终创建计数信号量还是调用了xQueueGenericCreate()函数,只不过创建的队列长度为uxMaxCount,初始时候uxMessagesWaiting 为uxInitialCount而已,其他和创建二值信号量并没有什么本质区别。下面再分析互斥信号量创建过程。
  • 互斥信号量创建的时候调用的函数是xQueueCreateMutex()函数源码如下。
QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType ){Queue_t *pxNewQueue;const UBaseType_t uxMutexLength = ( UBaseType_t ) 1, uxMutexSize = ( UBaseType_t ) 0;pxNewQueue = ( Queue_t * ) xQueueGenericCreate( uxMutexLength, uxMutexSize, ucQueueType );prvInitialiseMutex( pxNewQueue );return pxNewQueue;}
  • 从上面可以看出,创建互斥信号量依旧调用了xQueueGenericCreate()函数,并且对垒的定义与二值信号量相似,队列长度均为1。但是后面紧接着调用了函数prvInitialiseMutex()来初始化互斥信号量。下面来看一下该函数定义。
static void prvInitialiseMutex( Queue_t *pxNewQueue ){if( pxNewQueue != NULL ){pxNewQueue->pxMutexHolder = NULL;----1pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX;----2/* In case this is a recursive mutex. */pxNewQueue->u.uxRecursiveCallCount = 0;----3traceCREATE_MUTEX( pxNewQueue );/* Start with the semaphore in the expected state. */( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U, queueSEND_TO_BACK );----4}else{traceCREATE_MUTEX_FAILED();}}
  • 注意,在Queue_t 结构体中并没有发现pxMutexHolder 、uxQueueType 这两个成员,那么为什么这里会有这两个成员变量呢,这里我们在quue.c文件中找到其相关定义如下。
#define pxMutexHolder                    pcTail
#define uxQueueType                     pcHead
#define queueQUEUE_IS_MUTEX             NULL
  • 从上面定义可以看出,其实就是对Queue_t 结构体中的一些成员进行了一个重命名,这样增强代码可读性。
  1. 初始化互斥信号量队列为指针
  2. 初始化队列的类型为互斥信号量
  3. 重置互斥信号量回调次数为0
  4. 调用信号量发送函数,表示初始时互斥信号量是有效的

  • 到这里我们就将这些相关的API介绍完了,从中可以看出,信号量和队列是水乳交融,密不可分的,信号量其实就是队列的一种应用,下面我们来讲述一下信号量的如何来使用。

二值信号量

实验目标:在中断和任务中分别实现二值信号量的发送和接收,
在任务中采用按键实现信号量的发送,在另一个任务中等待接
收信号量。在中断中,PC发送给串口信息,串口接收到信息后
将信号量发送给任务。
  • 创建二值信号量代码如下
 Binary_Handler = xSemaphoreCreateBinary(); if (Binary_Handler == NULL){printf("Semaphore Binary Creat Failed!!!\r\n");}
  • 创建按键任务,当按键按下时发送信号量。函数定义如下
void key_task(void* pvParameters)
{BaseType_t err;while(1){if (Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON){err = xSemaphoreGive(Binary_Handler);//发送新信号量if (err != pdTRUE)//判断信号量是否发送成功{printf("信号量发送失败!\r\n");}}vTaskDelay(10);}
}
  • 创建信号量接收函数,接收按键任务和中断发送的信号量。定义如下
void task2_task(void* pvParameters)
{u8 count = 0;BaseType_t err;while(1){count++;//记录信号量接收次数if (Binary_Handler != NULL)//判断句柄有效性{err = xSemaphoreTake(Binary_Handler,portMAX_DELAY);//接收信号量if (err != pdTRUE)//判断是否接收成功{printf("接收信号量失败!!\r\n");}else//LED翻转 并且输出接收到的值{LED1 = ~LED1;printf("count = %d   Receive %s\r\n",count,USART_RX_BUF);memset(USART_RX_BUF,0,USART_REC_LEN);USART_RX_STA = 0;}}else{vTaskDelay(10);}}
}
  • 在串口中断服务函数中我们做以下更改。代码如下。
void USART1_IRQHandler(void)                 //串口1中断服务程序
{u8 Res;BaseType_t  HigherPriorityTaskWoken=pdFALSE;//用以判断是否需要进行任务切换BaseType_t     err;if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾){Res =USART_ReceiveData(USART1);   //读取接收到的数据if((USART_RX_STA&0x8000)==0)//接收未完成{if(USART_RX_STA&0x4000)//接收到了0x0d{if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始else USART_RX_STA|=0x8000;  //接收完成了 }else //还没收到0X0D{   if(Res==0x0d)USART_RX_STA|=0x4000;else{USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;USART_RX_STA++;if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收   }      }}          } if ( (Binary_Handler!=NULL) && (USART_RX_STA&0x8000) )//判断是否接收完成{err = xSemaphoreGiveFromISR(Binary_Handler,&HigherPriorityTaskWoken);//从中断中发送信号量if (err != pdTRUE)//判断是否出错{printf("Semaphore Give Failed!!!\r\n");}portYIELD_FROM_ISR(HigherPriorityTaskWoken);//根据HigherPriorityTaskWoken判断是否进行任务切换}}

优先级翻转

  • 上面使用二值信号量似乎没什么问题,但是在某些情况下,二值信号量可能会导致任务的优先级发生翻转,什么叫任务优先级翻转?假设我们这里有三个任务,分别为low_task、middle_task、high_task,且优先级依次升高,这里有一种可能,就是当高优先级的任务请求信号量进入阻塞状态,而低优先级任务一直控制着信号量不发出,这时,中等优先级的任务恰巧进入了准备态,所以中等优先级的任务就会打断低优先级任务,使得中等优先级的任务先执行,待其执行完毕后,再回到低优先级中,直到低优先级任务释放信号量后,高优先级的任务才能收到信号量而解除阻塞,这中间有一段时间导致中等优先级的任务优先级高于高优先级的任务优先级。如下图所示
  • 接下来就用代码测试一下优先级翻转。首先定义三个任务,且优先级依次从小到达,定义如下
#define LOW_TASK_PRIO  2
#define LOW_STACK_SIZE 50
TaskHandle_t LowTask_Handler;
void low_task(void *pvParameters);#define MIDDLE_TASK_PRIO  3
#define MIDDLE_STACK_SIZE 50
TaskHandle_t MiddleTask_Handler;
void middle_task(void *pvParameters);#define HIGH_TASK_PRIO  4
#define HIGH_STACK_SIZE 50
TaskHandle_t HighTask_Handler;
void high_task(void *pvParameters);void  low_task(void* pvParameters)
{long i;while(1){printf("low task running\r\n");xSemaphoreTake(Binary_Handler,portMAX_DELAY);for (i=0; i<22222222; i++);xSemaphoreGive(Binary_Handler);printf("low task give semaphore\r\n");vTaskDelay(1000);}
}void  middle_task(void* pvParameters)
{while(1){printf("middle task running\r\n");vTaskDelay(1000);}
}void  high_task(void* pvParameters)
{while(1){printf("high task ask the semaphore\r\n");xSemaphoreTake(Binary_Handler,portMAX_DELAY);printf("high task running\r\n");xSemaphoreGive(Binary_Handler);vTaskDelay(1000);}
}
  • 接下来用在start_task任务中创建信号量和任务
void start_task(void *pvParameters)
{               Binary_Handler = xSemaphoreCreateBinary(); if (Binary_Handler == NULL){printf("Semaphore Binary Creat Failed!!!\r\n");}xSemaphoreGive(Binary_Handler);xTaskCreate((TaskFunction_t )high_task,            //任务函数(const char*    )"high_task",          //任务名称(uint16_t       )HIGH_STACK_SIZE,        //任务堆栈大小(void*          )NULL,                  //传递给任务函数的参数(UBaseType_t    )HIGH_TASK_PRIO,       //任务优先级(TaskHandle_t*  )&HighTask_Handler);   //任务句柄 xTaskCreate((TaskFunction_t )middle_task,            //任务函数(const char*    )"middle_task",          //任务名称(uint16_t       )MIDDLE_STACK_SIZE,        //任务堆栈大小(void*          )NULL,                  //传递给任务函数的参数(UBaseType_t    )MIDDLE_TASK_PRIO,       //任务优先级(TaskHandle_t*  )&MiddleTask_Handler);   //任务句柄  xTaskCreate((TaskFunction_t )low_task,            //任务函数(const char*    )"low_task",          //任务名称(uint16_t       )LOW_STACK_SIZE,        //任务堆栈大小(void*          )NULL,                  //传递给任务函数的参数(UBaseType_t    )LOW_TASK_PRIO,       //任务优先级(TaskHandle_t*  )&LowTask_Handler);   //任务句柄      vTaskDelete(StartTask_Handler);
}
  • 核心代码就这些,下面来看一下运行结果
  • 从结果可以看出,其中有一段时间高优先级的任务一直在请求信号量而进入阻塞,这时中等优先级的任务会打断低优先级的任务而率先执行,高优先级的任务一直等待低优先级的任务释放信号量,相当于高优先级的任务优先级低了。

互斥信号量

  • 从上面可以看出,如果发生优先级翻转了,那么将不能保证高优先级的任务总是被率先执行,从而严重影响了系统的实时性,所以为了解决这个问题,FreeRTOS又提供了互斥信号量。互斥信号量相比于二值信号量,很大的改善了优先级翻转的问题,当高优先级任务等待低优先级的任务是释放信号量而进入阻塞状态时,会将低优先级任务的任务优先级提高到与高优先级一样,确保低优先级任务先执行,从而尽早释放信号量,释放信号量后,再将低优先级任务的优先级复原。这就是互斥信号量降低优先级翻转发生概率的原理。那么接下来看一下使用互斥信号量是否能有效地解决优先级翻转问题。
  • 同上面代码不变,这里仅将start_task中的信号量创建该为互斥信号量,代码如下。
void start_task(void *pvParameters)
{               Mutex_Handler = xSemaphoreCreateMutex();   if (Mutex_Handler == NULL){printf("Semaphore Binary Creat Failed!!!\r\n");}xSemaphoreGive(Mutex_Handler);xTaskCreate((TaskFunction_t )high_task,            //任务函数(const char*    )"high_task",          //任务名称(uint16_t       )HIGH_STACK_SIZE,        //任务堆栈大小(void*          )NULL,                  //传递给任务函数的参数(UBaseType_t    )HIGH_TASK_PRIO,       //任务优先级(TaskHandle_t*  )&HighTask_Handler);   //任务句柄 xTaskCreate((TaskFunction_t )middle_task,            //任务函数(const char*    )"middle_task",          //任务名称(uint16_t       )MIDDLE_STACK_SIZE,        //任务堆栈大小(void*          )NULL,                  //传递给任务函数的参数(UBaseType_t    )MIDDLE_TASK_PRIO,       //任务优先级(TaskHandle_t*  )&MiddleTask_Handler);   //任务句柄  xTaskCreate((TaskFunction_t )low_task,            //任务函数(const char*    )"low_task",          //任务名称(uint16_t       )LOW_STACK_SIZE,        //任务堆栈大小(void*          )NULL,                  //传递给任务函数的参数(UBaseType_t    )LOW_TASK_PRIO,       //任务优先级(TaskHandle_t*  )&LowTask_Handler);   //任务句柄       vTaskDelete(StartTask_Handler);
}
  • 运行结果如下
  • 从上图可以看出,使用互斥信号量能够解决优先级翻转问题,保证高优先级任务最先执行

计数信号量

  • 所谓计数型信号量,就是当有任务发送信号量的时候,uxMessagesWaiting +1(但必须满足uxMessagesWaiting <uxLength),当有任务请求接收信号量的时候,uxMessagesWaiting -1。这就是计数型信号量,但是要注意计数型信号量的计数值必须在0-uxLength之间。下面是计数型信号量的测试代码(通过按键发送信号量,另一个任务定时获取信号量)。

  • 按键发送信号量


void key_task(void* pvParameters)
{while(1){if (Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON){xSemaphoreGive(Count_Handler);//count = uxSemaphoreGetCount(Count_Handler);//printf("count=%ld\r\n",count);}if (Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON){xSemaphoreTake(Count_Handler,10);}vTaskDelay(10);}
}
  • 任务定时接收信号量
void task2_task(void* pvParameters)
{//BaseType_t err;while(1){count = uxSemaphoreGetCount(Count_Handler);printf("count=%ld\r\n",count);if (Count_Handler != NULL){if (count > 5){LED1 = 0;}else{LED1 = 1;}}vTaskDelay(100);}
}
  • 具体代码就不详细说明了,用法比较简单。

  • 到这里就将FreeRTOS中的信号量讨论完了,主要讨论了二值信号量、互斥信号量和计数信号量,这几种信号量使用方法大同小异,其中互斥信号量能够很大的弥补二值信号量可能导致的优先级翻转问题。

FreeRTOS-信号量相关推荐

  1. 22 freertos信号量

    二十二.freertos信号量 一.信号量 1. 二值信号量:只有两种状态,有或无. 2. 计数信号量:释放一个信号量时,信号量计数值加1.处理一个信号量时,信号量计数值减1. 3. 互斥信号量:是特 ...

  2. FreeRTOS信号量---二值信号量

    信号量可以用来进行资源管理和任务同步,FreeRTOS中信号量又分为二值信号量.计算型信号量.互斥信号量和递归互斥信号量. 0x01 二值信号量 二值信号量其实就是一个只有一个队列项的队列,这个特殊的 ...

  3. FreeRTOS系列第19篇---FreeRTOS信号量

    来自:http://blog.csdn.net/zhzht19861011/article/details/50835613 本文介绍信号量的基础知识,详细源码分析见<FreeRTOS高级篇6- ...

  4. FreeRTOS信号量

      信号量是操作系统中重要的一部分,信号量一般用来进行资源管理和任务同步, FreeRTOS中信号量又分为二值信号量. 计数型信号量.互斥信号量和递归互斥信号量.不同的信号量其应用场景不同,但有些应用 ...

  5. FreeRTOS记录(七、FreeRTOS信号量、事件标志组、邮箱和消息队列、任务通知的关系)

    我们在前面单独介绍过FreeRTOS的任务通知和消息队列, 但是在FreeRTOS中任务间的通讯还有信号量,邮箱,事件组标志等可以使用 这篇文章就这些成员与消息队列和任务通知的关系进行说明分析 ..增 ...

  6. FreeRTOS信号量详解第三讲(全网最全)——计数信号量

    一.计数型信号量简介 有些资料中也将计数型信号量叫做数值信号量,二值信号量相当于长度为1的队列,那么计数型信号量就是长度大于1的队列.**同二值信号量一样,用户不需要关心队列中存储了什么数据,只需要关 ...

  7. 【嵌入式操作系统】FreeRTOS信号量mutex和semaphore的区别

    今天学习信号量mutex和semaphore的区别,找到了正点原子的博客介绍,讲的挺详细的.建议大家阅读 转载自:https://blog.csdn.net/nippon1218/article/de ...

  8. FreeRTOS信号量详解第二讲(全网最全)——二值信号量

    一.二值信号量简介 二值信号量通常用于互斥访问或同步,二值信号量和互斥信号量非常类似,但是还是有一些细微的差别,互斥信号量拥有优先级继承机制,二值信号量没有优先级继承.因此二值信号另更适合用于同步(任 ...

  9. FreeRTOS信号量 基于STM32

    目录 概述 一.信号量基本概念 1.二值信号量 2.计数信号量 3.互斥信号量 4.递归信号量 二.二值信号量运作机制 三. 计数信号量运作机制 四.常用信号量函数接口讲解 1.创建二值信号量 xSe ...

  10. FreeRTOS信号量详解第四讲(全网最全)——互斥信号量

    一.互斥信号量简介 互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或中断与任务之间的同步)二值信号量最适合.互斥信号量适合用于那些需要互斥访问的应用中.在互斥访问中互斥信 ...

最新文章

  1. 平行志愿遵循分数优先php,2015年黑龙江普通高校招生平行志愿“平行志愿”投档录取模式有哪些优点?...
  2. 二进制安装mysql5.6_轻松使用二进制安装Mysql5.6
  3. mate10的html5跑分,麒麟970逆天!华为Mate10跑分首曝:媲美骁龙835
  4. php登录框注入,分享一个php的防火墙,拦截SQL注入和xss
  5. jacoco底层原理解析
  6. java怎么设置多个输入_Java中从键盘输入多个整数的方法
  7. mysql要将语句反复执行15次_MySQL多表查询疑问
  8. 性能测试:性能测试实施全过程指南
  9. 「镁客·请讲」南京布塔:用动作捕捉世界的精彩
  10. python 流式编程_Java8 Stream流式编程,极大解放你的生产力!
  11. 说说TCP,UDP和socket,Http之间联系和区别
  12. 黑科技智能家电新生儿“智能冰箱”
  13. [leetcode]: 506. Relative Ranks
  14. 自动驾驶漫谈之二:无人驾驶与高精度地图
  15. Jenkins 更新网站静态文件
  16. excel 公式 单引号 concat_2019会计必备:934套带公式的实用Excel模板!【收藏】
  17. 安装vs2005 sp1 出现错误 1718
  18. java 内存 pdf_jvm内存模型高清版.pdf
  19. 记:《洛克菲勒留给儿子的38封信》-- 21
  20. Python合并同类项的字符串,一对多

热门文章

  1. 「SP122」STEVE - Voracious Steve 解题报告
  2. 磨刀霍霍向猪羊Android第一次
  3. 神州信息董事长郭为率队拜访广电运通,围绕金融科技海外业务与金融联合解决方案进行探讨
  4. 商标注册类别如何选择?商标申请人注意了!
  5. 如果2021能重开,我会告诉自己去做这些
  6. (转)DataTable与结构不同实体类之间的转换
  7. JSP的基本使用总结
  8. 5G NGC — CHF 融合计费
  9. 从柴静的片子谈人力资源需求的趋势
  10. Qt商业组件Qtitan系列大更新:适用于Qt5.15.2和Visual Studio2019