当前位置: 首页 > 工具软件 > AWTK > 使用案例 >

AWTK之多线程

谢典
2023-12-01

AWTK-多线程

由实际开发中的idle的使用,产生困惑,继而理解AWTK的线程相关的模块设计

reference: AWStudio社区版 (zlg.cn)

线程

thread src/tkc/thread.h

struct _tk_thread_t {
  /* 平台无关成员(移植时必须定义) */
  void*             args;       /* 线程回调函数上下文 */
  tk_thread_entry_t entry;      /* 线程回调函数 */
  bool_t            running;    /* 线程是否正在执行 */
  const char*       name;       /* 线程的名称 */
  uint32_t          stack_size; /* 线程的栈大小 */
  uint32_t          priority;   /* 线程的优先级 */

  /* 平台相关成员(移植到其他平台时,请根据实际需求定义) */
  pthread_t thread;  /* 此处以 pthread 为例 */
};

线程相关接口说明详见下表:

接口说明备注
tk_thread_create创建thread对象保存上下文 ctx 和回调函数 entry
tk_thread_set_name设置线程名称
tk_thread_set_stack_size设置线程栈大小
tk_thread_set_priority设置线程优先级某些平台可能不支持
tk_thread_get_priority_from_platform获取平台相关的优先级某些平台可能不支持
tk_thread_start启动线程将 running 属性设置为 TRUE
tk_thread_join等待线程退出将 running 属性设置为 FALSE
tk_thread_get_args获取线程的参数
tk_thread_destroy销毁thread对象
tk_thread_self获取当前线程的原生句柄

互斥锁

awtk/src/tkc/mutex.h

struct _tk_mutex_t {
  /* 定义一个互斥锁,请根据实际平台实现 */
  pthread_mutex_t mutex;  /* 此处以 pthread 为例 */
};

互斥锁相关接口详见下表:

接口说明
tk_mutex_create创建互斥锁对象
tk_mutex_lock加锁
tk_mutex_try_lock尝试加锁
tk_mutex_unlock解锁
tk_mutex_destroy销毁互斥锁对象

信号量

awtk/src/tkc/semaphore.h

struct _tk_semaphore_t {
  /* 定义一个信号量,请根据实际平台实现 */
  sem_t* sem;  /* 此处以 sem_t 为例 */
};

信号量相关接口详见下表:

接口说明
tk_semaphore_create创建信号量对象
tk_semaphore_wait获取资源
tk_semaphore_post释放资源
tk_semaphore_destroy销毁信号量对象

条件变量

awtk/src/tkc/cond.h

struct _tk_cond_t {
  /* 定义一个条件变量,请根据实际平台实现 */
  pthread_cond_t cond;  /* 此处以 pthread 为例 */
};

条件变量相关接口详见下表:

接口说明
tk_cond_create创建条件变量对象
tk_cond_wait等待
tk_cond_wait_timeout等待指定时间
tk_cond_signal唤醒
tk_cond_destroy销毁条件变量对象

异步

AWTK采取消息队列的方式来实现异步,使用的重要结构体如下:

typedef union _event_queue_req_t {
  event_t event;
  key_event_t key_event;
  wheel_event_t wheel_event;
  pointer_event_t pointer_event;
  multi_gesture_event_t multi_gesture_event;
  add_idle_t add_idle;
  add_timer_t add_timer;
} event_queue_req_t;

GUI 控件只能在 GUI 线程进行操作,非 GUI 线程想操作 GUI 控件,必须用 idle_queue 或 timer_queue 进行串行化。

  • idle_queue
  • timer_queue

示例:

调用 tk_thread_create 创建一个线程,在线程的 test_timer_queue 回 调函数中调用 timer_queue 添加 timer 事件请求,在 timer 定时器中更新 label 文本框的内容。

static widget_t* s_label = NULL;

static ret_t update_label(int nr) {
     if (s_label) {
     char str[16] = {0};
     tk_snprintf(str, sizeof(str), "%d", nr);
     widget_set_text_utf8(s_label, str);
 }
 	return nr > 0 ? RET_OK : RET_REMOVE;
}

/* GUI 线程中可调用与 GUI 相关的函数,如:widget_lookup */
static ret_t on_timer(const timer_info_t* timer) {
 	return update_label(*(int*)(timer->ctx));
}

/* 非 GUI 线程中不可调用与 GUI 相关的函数,如:widget_lookup */
void* test_timer_queue(void* args) {
     static int nr = 500000;
     while (nr-- > 0) {
         timer_queue(on_timer, &nr, 30);
         sleep_ms(30);
 }
 	return NULL;
}

ret_t application_init() {
    tk_thread_t* thread = NULL;
    widget_t* win = window_create(NULL, 0, 0, 0, 0);
    widget_t* label = label_create(win, 10, 10, 300, 20);
    s_label = label;
    thread = tk_thread_create(test_timer_queue, NULL);
    tk_thread_start(thread);
	return RET_OK;
}

ret_t application_exit() {
     log_debug("application_exit\n");
     return RET_OK;
}
#include "awtk_main.inc

**在上面的代码中,需要注意的是:如果在调用 tk_thread_create 函数创建线程的时候, 使用 label 对象作为线程的参数,并在 update_label 函数中使用该对象更新 label 文本框的内 容,会存在安全隐患。因为 label 是 GUI 界面中的一个文本框控件对象,在线程操作的过程 中,如果此时 label 对象在其他地方被销毁,再更新其文本内容就会出现问题。 **

需要注意的是:在非GUI线程中,不要调用操作GUI控件相关的函数,如:widget_lookup, 因为这些函数是非线程安全的。

使用场景

  • 分发输入设备事件

捕获到系统设备事件并将其转化为 AWTK 支持的事件后,可以调用 AWTK 提供的以下接口将事件分发到 GUI 的主循环中处理:

函数名称说明备注
main_loop_queue_event分发事件请求可分发 AWTK 中的任意事件,需提供事件请求结构体
main_loop_post_key_event分发按键事件需提供键值与按键状态
main_loop_post_pointer_event分发指针事件需提供指针坐标与指针状态
main_loop_post_multi_gesture_event分发多点触摸事件需提供多点触摸事件结构体
  • 分发输入设备事件的方式

常见的输入设备事件分发方式有两种,分别是在 GUI 线程中分发以及在非 GUI 线程中分发,AWTK 为这两种方式分别提供了简单的实现,以方便用户移植。

1、在 GUI 线程中分发事件

对于裸系统平台,AWTK 提供了 main_loop_raw.inc,该文件实现了裸系统 main_loop 的基本功能,在移植时用户只需实现负责输入设备事件的分发 platform_disaptch_input 函数即可,该函数在 GUI 主循环中被调用,会阻塞主线程,示例代码如下:

/* 分发输入设备事件 */
ret_t platform_disaptch_input(main_loop_t *l) {
  /* 捕获系统的触摸、鼠标、键盘等输入事件,并转化为 AWTK 支持的事件 */
  event_queue_req_t req;           /* AWTK 的事件请求 */
  memset(&req, 0x00, sizeof(req));
  req.xxx = system_get_input_event();
  ...
  /* 调用接口将事件分发到 GUI 主循环 */
  main_loop_queue_event(l, &req);
  return RET_OK; 
}

#include "main_loop/main_loop_raw.inc"

2、在非 GUI 线程中分发事件

在非 GUI 线程中获取并分发设备事件,不会阻塞主线程,可以在一定程度上提高 GUI 刷新效率。示例代码如下:

static void* input_run(void* ctx) {
  main_loop_t* loop = (main_loop_t*)ctx;
  while(1) {
    /* 捕获系统的触摸、鼠标、键盘等输入事件,并转化为 AWTK 支持的事件 */
    event_queue_req_t req;           /* AWTK 的事件请求 */
    memset(&req, 0x00, sizeof(req));
    req.xxx = system_get_input_event();
    ...
    /* 调用接口将事件分发到 GUI 主循环 */
    main_loop_queue_event(loop, &req);
  }
  return NULL;
}

ret_t platform_disaptch_input(main_loop_t *l) {
  if (run_once) { /* 保证仅执行一次,创建事件分发线程 */
      system_create_thread(input_run);
  }
}

#include "main_loop/main_loop_raw.inc"

线程安全

reference:线程安全和可重入

线程不安全函数类

  1. 不保护共享变量的函数。

    i++操作

    解决方法:对临界区加锁,或者使用PV操作的信号量来保护共享的变量。

  2. 保持跨越多个调用的状态函数。

    伪随机数生成器,当调用srand为rand设置一个种子后,如果多线程调用rand函数,就会造成线程的安全隐患。

    解决方法:重写rand函数,使得它不再使用任何static数据,而是依靠调用者在参数中传递状态信息。

  3. 返回指向静态变量的指针的函数。

    比如将一个计算结果放在一个static变量中,然后返回一个指向这个变量的指针。如果多线程调用这些函数,正在被一个线程使用的结构会被另一个线程覆盖掉。

    解决方法:① 选择重写函数,使得调用者传递存放结果的变量的地址,消除了所有共享数据。 ② 使用加锁-拷贝(lock-and-copy)技术。将线程不安全函数与互斥锁联系起来,在每一个调用位置,对互斥锁加锁,调用线程不安全函数,将函数返回的结果拷贝到一个私有的存储器位置,然后对互斥锁解锁。

  4. 调用线程不安全函数的函数。

    我们假设函数A安全,函数B不安全。

    情况①:如果函数A调用B,那么A不一定不安全。如果B是第2类的函数,即依赖于跨越多次调用的状态,那么A线程肯定不安全。

    解决方法:对函数B进行重写。

    情况②:如果B是第1类或者第3类。

    解决方法:需要用互斥锁保护调用位置和任何得到的共享数据,A仍可能是线程安全的。

可重入函数

重入:即重复调用,函数被不同的流调用,有可能会出现第一次调用还没有返回时就再次进入该函数开始下一次调用。

可重入:当程序被多个线程反复执行,结果总是正确的。

不可重入:当程序被多个线程反复调用,产生的结果会出错。

  • 可重入函数满足条件

    1. 不使用全局变量或静态变量;
    2. 不使用malloc或者new开辟出的空间;
    3. 不调用不可重入函数;
    4. 不返回静态或全局数据,所有数据都有函数的调用者提供;
    5. 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。

    很多时候,可重入函数与线程安全被用作同义词,但是它们还是有很明显的区别的,可重入函数仅仅是线程安全函数的一个真子集。

  • 可重入函数与线程安全函数的区别

    1. 线程安全不一定是可重入的,而可重入函数一定是线程安全的。
    2. 线程安全是多个线程下引起的,但可重入函数可以在只有一个线程的情况下发生。
    3. 若一个函数中存在全局变量,那么这个函数既不是线程安全的也不是可重入的。
    4. 线程安全函数能够使不同的线程访问同一块地址空间,而可重入函数要求不同的执行流对数据的操作互不影响结果是相同的。
 类似资料: