前言

最近在排查TDengine stmt2写入性能问题时,发现了一个令人意外的现象:某处优化竟然导致数据写入速度下降了10%。更让人惊讶的是,这个结果有些违反常识——循环sleep的响应速度居然超过了条件变量和信号量这两种同步方式!为此,我咨询了DeepSeek,但它似乎也遇到了知识盲区。这让我不禁感慨,AI虽然能一本正经地回答问题,但有时也会误导人类啊。

条件变量、信号量、循环sleep 1微秒,这三种线程同步方式哪个响应速度快?

在条件变量、信号量和循环sleep 1微秒这三种线程同步方式中,条件变量的响应速度通常最快。原因如下:

  1. 条件变量
    • 条件变量允许线程在等待某个条件时进入休眠状态,直到条件满足时被唤醒。
    • 当条件满足时,操作系统会立即唤醒等待的线程,响应速度非常快。
    • 避免了忙等待(busy-waiting),减少了CPU资源的浪费。
  2. 信号量
    • 信号量用于控制对共享资源的访问,线程在等待信号量时会进入休眠状态。
    • 虽然信号量的响应速度也较快,但相比条件变量,信号量的唤醒机制稍显复杂,响应速度略慢。
  3. 循环sleep 1微秒
    • 这种方式属于忙等待的一种变体,线程在每次循环中休眠1微秒,然后检查条件是否满足。
    • 由于存在固定的休眠时间,响应速度受限于休眠间隔,无法做到即时响应。
    • 频繁的休眠和唤醒会增加系统开销,降低整体性能。

总结:

  • 条件变量的响应速度最快,适合需要高效同步的场景。
  • 信号量次之,适合资源访问控制。
  • 循环sleep 1微秒的响应速度最慢,通常不推荐用于高响应要求的场景。

下面导致性能下降的代码修改,这么合理的改动,没想到导致写入性能下降10%!

  // 看到这种代码,肯定就是顺手改了的,否则cpu空跑,岂不是白白浪费资源?
  while (0 == atomic_load_64(&pStmt->queue.qRemainNum)) {
    taosUsleep(1);
  }
  // 下面是修改后的代码
  while (0 == atomic_load_64((int64_t*)&pStmt->queue.qRemainNum)) {
    (void)taosThreadCondWait(&pStmt->queue.waitCond, &pStmt->queue.mutex);
  }

测试

于是我干脆放弃求助AI,选择自己动手测测响应时间,让AI帮我写了下面测试的脚本,测试条件变量、信号量和循环sleep 1微秒这三种线程同步方式各自的响应时间:

#include <pthread.h>
#include <semaphore.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>

#define NANOSECONDS_IN_SECOND 1000000000L
#define MICROSECONDS_IN_SECOND 1000000L

// 用于存储测试的时间
struct timespec start_time1, end_time1;
struct timespec start_time2, end_time2;
struct timespec start_time3, end_time3;
long duration1, duration2, duration3 = 0;

// case 1 : 用于测量sleep(1us)的函数

atomic_int x = ATOMIC_VAR_INIT(0);
void *test_sleep(void *arg) {
  while (true) {
    while (atomic_load(&x) == 0) {
      usleep(1);
    }
    clock_gettime(CLOCK_MONOTONIC, &end_time2);

    duration2 +=
        (end_time2.tv_sec - start_time2.tv_sec) * NANOSECONDS_IN_SECOND +
        (end_time2.tv_nsec - start_time2.tv_nsec);
    atomic_fetch_sub(&x, 1);
  }
}

// case 2 : 用于测试 pthread_cond_wait 的函数
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *test_cond_wait(void *arg) {
  while (true) {
    pthread_mutex_lock(&mutex);
    pthread_cond_wait(&cond, &mutex);
    pthread_mutex_unlock(&mutex);
    clock_gettime(CLOCK_MONOTONIC, &end_time1);

    duration1 +=
        (end_time1.tv_sec - start_time1.tv_sec) * NANOSECONDS_IN_SECOND +
        (end_time1.tv_nsec - start_time1.tv_nsec);
  }

  return NULL;
}

// case 3 : 用于测试 sem_wait 的函数
sem_t *sem;
void *test_sem_wait(void *arg) {
  while (true) {
    if (sem_wait(sem) == -1) {
      perror("sem_post failed");
      break;
    } // 获取结束时间
    clock_gettime(CLOCK_MONOTONIC, &end_time3);

    duration3 +=
        (end_time3.tv_sec - start_time3.tv_sec) * NANOSECONDS_IN_SECOND +
        (end_time3.tv_nsec - start_time3.tv_nsec);
  }
}

int main() {
  pthread_t thread1, thread2, thread3;
  sem = sem_open("/my_semaphore", O_CREAT, S_IRUSR | S_IWUSR, 0);
  if (sem == SEM_FAILED) {
    perror("sem_open failed");
    return 1;
  }

  pthread_create(&thread1, NULL, test_cond_wait, NULL);
  pthread_create(&thread2, NULL, test_sleep, NULL);
  pthread_create(&thread3, NULL, test_sem_wait, NULL);

  struct timespec start_time, end_time;
  clock_gettime(CLOCK_MONOTONIC, &start_time);
  usleep(100);
  clock_gettime(CLOCK_MONOTONIC, &end_time);
  long duration =
      (end_time.tv_sec - start_time.tv_sec) * NANOSECONDS_IN_SECOND +
      (end_time.tv_nsec - start_time.tv_nsec);
  printf("usleep 100: %ld ns\n", duration);

  for (int i = 0; i < 100000; i++) {
    clock_gettime(CLOCK_MONOTONIC, &start_time1);
    pthread_mutex_lock(&mutex);
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
    usleep(1);

    clock_gettime(CLOCK_MONOTONIC, &start_time2);
    atomic_fetch_add(&x, 1);
    usleep(1);

    clock_gettime(CLOCK_MONOTONIC, &start_time3);
    if (sem_post(sem) == -1) {
      perror("sem_post failed");
      return 1;
    }
    usleep(1);
  }

  printf("pthread_cond_wait duration: %ld ms\n",
         duration1 / MICROSECONDS_IN_SECOND);
  printf("usleep(1us) duration: %ld ms\n", duration2 / MICROSECONDS_IN_SECOND);
  printf("sem_wait duration: %ld ms\n", duration3 / MICROSECONDS_IN_SECOND);

  pthread_join(thread1, NULL);
  pthread_join(thread2, NULL);
  pthread_join(thread3, NULL);

  return 0;
}

结果是sleep1us > sem_wait = pthread_cond_wait

pthread_cond_wait duration: 225 ms
usleep(1us) duration: 201 ms
sem_wait duration: 221 ms

// 即使去掉mutex的影响,测试结果也是差了10%的响应时间

pthread_cond_wait duration: 223 ms
usleep(1us) duration: 205 ms
sem_wait duration: 224 ms

原因

sleep 在交出 CPU 使用权后,操作系统通过计时器机制直接唤醒等待队列中的线程,无需依赖其他线程的交互。而 pthread_cond_wait 则需要等待其他线程调用 pthread_cond_signalpthread_cond_broadcast 来改变条件变量,之后才会唤醒等待队列中的线程。由于涉及线程间的通信和上下文切换,pthread_cond_wait 的响应时间通常比 sleep 多出约 10%。

结论

在对响应时间要求较高且 CPU 计算资源充足的情况下,不宜使用 pthread_cond_wait 进行同步,而应考虑采用忙等待(busy-waiting)或极短时间的 sleep 来控制线程同步。

为了在避免 CPU 空跑的同时,又能在大吞吐量场景下保持高响应性能,可以采用一种混合模式进行线程同步:

  • 数据高峰期:使用 sleep 模式同步,确保高响应速度。
  • 数据空闲期:使用 pthread_cond_wait 模式,避免浪费 CPU 资源。

这种混合策略能够根据实际负载动态调整同步机制,兼顾性能和资源利用率。

  int i = 0;
  while (0 == atomic_load_64((int64_t*)&pStmt->queue.qRemainNum)) {
    if (i < 5000) {
      taosUsleep(1);
      i++;
    } else {
      (void)taosThreadMutexLock(&pStmt->queue.mutex);
      if (0 == atomic_load_64((int64_t*)&pStmt->queue.qRemainNum)) {
        (void)taosThreadCondWait(&pStmt->queue.waitCond, &pStmt->queue.mutex);
      }
      (void)taosThreadMutexUnlock(&pStmt->queue.mutex);
    }
  }

不过,需要注意以下几点:

  1. 忙等待的使用:忙等待虽然响应速度快,但会持续占用 CPU 资源,需谨慎使用。
  2. sleep 时间的选择sleep 时间过短可能导致频繁上下文切换,过长则可能影响响应速度,需根据实际场景调整。
  3. 混合模式的实现复杂度:动态切换同步机制可能增加代码复杂度,需确保逻辑正确性和稳定性。

最后打个广告,了解 TDengine 更多内容欢迎访问:TDengine

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐