线程同步方式的对响应速度影响这么大?
更让人惊讶的是,这个结果有些违反常识——循环sleep的响应速度居然超过了条件变量和信号量这两种同步方式!为此,我咨询了DeepSeek,但它似乎也遇到了知识盲区。这让我不禁感慨,AI虽然能一本正经地回答问题,但有时也会误导人类啊。
前言
最近在排查TDengine stmt2写入性能问题时,发现了一个令人意外的现象:某处优化竟然导致数据写入速度下降了10%。更让人惊讶的是,这个结果有些违反常识——循环sleep的响应速度居然超过了条件变量和信号量这两种同步方式!为此,我咨询了DeepSeek,但它似乎也遇到了知识盲区。这让我不禁感慨,AI虽然能一本正经地回答问题,但有时也会误导人类啊。
条件变量、信号量、循环sleep 1微秒,这三种线程同步方式哪个响应速度快?
在条件变量、信号量和循环sleep 1微秒这三种线程同步方式中,条件变量的响应速度通常最快。原因如下:
- 条件变量:
- 条件变量允许线程在等待某个条件时进入休眠状态,直到条件满足时被唤醒。
- 当条件满足时,操作系统会立即唤醒等待的线程,响应速度非常快。
- 避免了忙等待(busy-waiting),减少了CPU资源的浪费。
- 信号量:
- 信号量用于控制对共享资源的访问,线程在等待信号量时会进入休眠状态。
- 虽然信号量的响应速度也较快,但相比条件变量,信号量的唤醒机制稍显复杂,响应速度略慢。
- 循环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_signal
或 pthread_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);
}
}
不过,需要注意以下几点:
- 忙等待的使用:忙等待虽然响应速度快,但会持续占用 CPU 资源,需谨慎使用。
sleep
时间的选择:sleep
时间过短可能导致频繁上下文切换,过长则可能影响响应速度,需根据实际场景调整。- 混合模式的实现复杂度:动态切换同步机制可能增加代码复杂度,需确保逻辑正确性和稳定性。
最后打个广告,了解 TDengine 更多内容欢迎访问:TDengine
更多推荐
所有评论(0)