分类 边缘计算软件 下的文章

C 语言应用开发中的注意事项

C 语言作为一门经典的编程语言,广泛应用于系统编程、嵌入式开发和高性能计算等领域。然而,在进行 C 语言应用开发时,开发者需要关注一系列问题,以确保代码的正确性、性能和可维护性。

笔者在基于 C 语言开发 EdgeX Foundry 设备服务实践之中,遇到了一些问题,如全局变量的使用.c 模块文件的划分多线程编程注意事项深浅拷贝问题以及编译链接时的多重定义问题。本文将对其展开讨论,并提供具体的代码示例和实用建议。

1.全局变量

全局变量是在函数外部定义的变量,其作用域从定义处开始到文件结束。它们在程序整个生命周期内存在,通常不会被轻易时放掉。可被程序中的任何函数访问。它能够方便多个函数共享数据,并减少函数参数传递的开销,但同时也不利于模块化编程和代码重用,增加调试难度。

以下展示的 global_pp 全局变量,在 EdgeX Foundry 设备服务中用于保存设备协议的详细信息,为部分关键代码:

#include "device_openvino.h"
#include "circular_buffer.h"

 /*  protocols  */
 typedef struct
 {
   char *input_model;
   char *input_image_path; /* Uri */
   char *device_name;      /* CPU, GPU[0,1,...], NPU */
   char *Score;
 } protocol_properties;

protocol_properties *global_pp;

// malloc a protocol_properties
void func() {
  if (!global_pp)
  {
    iot_log_debug(driver->lc, "malloc a global_pp (protocol_properties)");
    global_pp = (protocol_properties *)malloc(sizeof(protocol_properties));
    global_pp->input_model = NULL;
    global_pp->input_image_path = NULL;
    global_pp->device_name = NULL;
    global_pp->Score = NULL;
  }
}

int main() {
    func();
    return 0;
}

注意事项:

  • 尽量减少全局变量的使用,优先考虑局部变量或函数参数。

  • 使用有意义的命名(如 g_config),避免冲突。

  • 在多线程环境中,使用锁机制确保线程安全。

2.源代码文件夹中 .c 模块文件的划分

模块化编程将程序分解为独立的模块,每个模块负责特定功能。这种方法能提高代码的可读性、可维护性和可重用性,同时便于团队协作开发。划分 .c 文件的指导思想有以下几点:

  • 每个 .c 文件应包含一组相关功能的实现,例如字符串处理或网络通信。

  • 避免单个 .c 文件过大,建议控制在 1000 行以内。

  • 使用头文件 (.h) 声明对外接口,.c 文件实现具体逻辑。

  • 头文件用于声明函数、宏、类型和全局变量,供其他模块使用。

以下是 EdgeX Foundry 设备服务中环形 buffer 缓冲区的模块设计:

//circular_buffer.h
#pragma once
#include <stddef.h> // 包含 size_t 的定义

// Circular buffer structure
struct circular_buffer {
    struct infer_result* buffer;  // storing data
    size_t capacity;             // Maximum buffer capacity
    size_t size;                // The current number of stored elements
    size_t head;                // Head index (write location)
    size_t tail;                // Tail index (read position)
  };

struct circular_buffer* create_circular_buffer(size_t capacity);
...
//circular_buffer.c
#include "circular_buffer.h"
#include <stdlib.h>
#include "edgex_common.h"
// Initialize circular buffer
struct circular_buffer* create_circular_buffer(size_t capacity) {
    //malloc a circular_buffer
    struct circular_buffer* cb = (struct circular_buffer*)malloc(sizeof(struct circular_buffer));
    if (cb == NULL) {
        return NULL;
    }
    cb->buffer = (struct infer_result*)malloc(sizeof(struct infer_result) * capacity);
    if (cb->buffer == NULL) {
        free(cb);
        return NULL;
    }

    //Init
    cb->capacity = capacity;
    cb->size = 0;
    cb->head = 0;
    cb->tail = 0;

    return cb;
  }

要点:

  • 使用 #ifndef#define#pragma once 防止头文件重复包含。

  • 头文件仅声明,不定义变量或函数。

  • 保持头文件简洁,避免引入不必要的依赖。

3.多线程编程注意的问题

线程安全和互斥锁

在多线程程序中,多个线程可能同时访问共享资源,导致数据不一致。互斥锁(mutex)可用于保护临界区,确保同一时间只有一个线程操作共享数据。在 EdgeX Foundry 设备服务 SDK 设计中,服务实例结构体包含有互斥锁指针,在设备服务初始化时一并初始化 mutex 以便于在整个设备服务声明周期中进行资源同步管理:

/**
 * @brief Structure representing an OpenVINO driver instance for managing inference operations.
 */
typedef struct openvino_driver
{
  iot_logger_t *lc;
  pthread_mutex_t mutex;      // for synchronization
  pthread_t inference_thread; // 推理线程句柄
  bool stop_inference;        // 控制推理线程停止的标志
} openvino_driver;

viod *openvino_inference_thread (){
    ...
    // Thread-safe storage of results
    pthread_mutex_lock(&driver->mutex);
    push_result(global_cb, *results);
    pthread_mutex_unlock(&driver->mutex);
}

线程间通信

线程间可通过共享内存、信号量或消息队列通信。例如,使用条件变量实现生产者-消费者模型。在 EdgeX Foundry 设备服务的后续开发中也会频繁出现。

4.深浅拷贝问题

浅拷贝: 只复制基本数据类型和指针,指针指向的内存不复制。

深拷贝: 复制所有数据,包括指针指向的内存。

在 EdgeX Foundry 设备服务开发中,协议资源 global_pp;openvino_create_addr 中由深拷贝赋值,以此避免global_pp;在 SDK 回调函数中被错误释放资源:

/**
 * @brief Creates an address structure for the OpenVINO device service.
 * @details This callback function is used by the EdgeX Foundry framework to create and populate a protocol_properties
 *          structure with OpenVINO-specific configuration data extracted from the provided protocols.
 * @param impl Pointer to the implementation-specific data (cast to openvino_driver).
 * @param protocols Pointer to the protocol data containing OpenVINO configuration.
 * @param exception Pointer to store any exception data (not used in this implementation).
 * @return devsdk_address_t A pointer to the populated protocol_properties structure, cast to the EdgeX address type.
 */
static devsdk_address_t openvino_create_addr(void *impl, const devsdk_protocols *protocols, iot_data_t **exception)
{
...
  const iot_data_t *props = devsdk_protocols_properties(protocols, "Openvino");
  //从设备获取协议资源
  if (props)
  {
    result = iot_data_string_map_get_string(props, "Uri");
    pp->input_image_path = result;

    result = iot_data_string_map_get_string(props, "model");
    pp->input_model = result;

    result = iot_data_string_map_get_string(props, "CPU_GPU");
    pp->device_name = result;

    result = iot_data_string_map_get_string(props, "Score");
    pp->Score = result;
  }

  // Allocate and populate new global protocol properties

  // 深拷贝
  global_pp->input_model = malloc(strlen(pp->input_model) + 1);
  strcpy(global_pp->input_model,pp->input_model);

  global_pp->input_image_path = malloc(strlen(pp->input_image_path) + 1);
  strcpy(global_pp->input_image_path,pp->input_image_path);

  global_pp->device_name = malloc(strlen(pp->device_name) + 1);
  strcpy(global_pp->device_name,pp->device_name);

  global_pp->Score = malloc(strlen(pp->Score) + 1);
  strcpy(global_pp->Score,pp->Score);

  return (devsdk_address_t)pp;
}

深拷贝的原因:

  • 对象包含动态分配的内存。
  • 需要独立副本,避免共享内存导致的问题。

总结

在 C 语言应用开发中,开发者需要综合考虑全局变量的合理使用、模块化设计的规范性、多线程编程的线程安全性、深浅拷贝的适用场景以及编译链接的正确性。

通过谨慎使用全局变量、合理划分模块、确保线程安全、正确处理拷贝和避免多重定义问题,可以编写出健壮、高效且易于维护的 C 语言程序。