【发布时间】:2019-12-20 19:13:06
【问题描述】:
我有一个全局整数(下面程序中struct LogBufferDescriptor 的input_level 字段)将由一个线程写入,并由多个线程读取。我使用 gcc 内置的 __atomic_store_n() / __atomic_load_n() 函数或旧版 __sync_lock_test_and_set() / __sync_fetch_and_add() 函数来访问它。但两者都不能提供一致的结果。
gcc 内置的 __atomic 函数和遗留的 __sync 函数都不能提供对整数的一致访问。当前,只有信号量和 pthread_mutex 可以正常工作。
/*
gcc -g -o simulate_case simulate_case.c -lpthread
*/
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/time.h>
#include <semaphore.h>
#define LOG_BUFFER_INPUT_LEVEL_DEFAULT 2
#define LOG_BUFFER_INPUT_LEVEL_NO_PRINTS 6
#define MS_TO_US(ms) ((ms) * 1000)
#define TIMEOUT_MS 100
#define SYNC_GCC_SYNC
typedef struct LogBufferDescriptor
{
int input_level;
pthread_mutex_t log_buffer_lock;
pthread_mutex_t input_level_lock;
sem_t input_level_sem;
} LogBufferDescriptor;
static LogBufferDescriptor G_log_buffer;
/********** choose one implementation by defining corresponding macro ***********/
#ifdef SYNC_NONE
static void print_sync_mechanism(void)
{
printf("Sync mechanism = NONE\n");
}
static void set_input_level(int level)
{
G_log_buffer.input_level = level;
}
static int get_input_level(void)
{
return G_log_buffer.input_level;
}
#endif
#ifdef SYNC_GCC_MUTEX
static void print_sync_mechanism(void)
{
printf("Sync mechanism = MUTEX\n");
}
static void set_input_level(int level)
{
pthread_mutex_lock(&G_log_buffer.input_level_lock);
G_log_buffer.input_level = level;
pthread_mutex_unlock(&G_log_buffer.input_level_lock);
}
static int get_input_level(void)
{
int level;
pthread_mutex_lock(&G_log_buffer.input_level_lock);
level = G_log_buffer.input_level;
pthread_mutex_unlock(&G_log_buffer.input_level_lock);
return level;
}
#endif
#ifdef SYNC_SEM
static void print_sync_mechanism(void)
{
printf("Sync mechanism = semaphore\n");
}
static void set_input_level(int level)
{
sem_wait(&G_log_buffer.input_level_sem);
G_log_buffer.input_level = level;
sem_post(&G_log_buffer.input_level_sem);
}
static int get_input_level(void)
{
int level;
sem_wait(&G_log_buffer.input_level_sem);
level = G_log_buffer.input_level;
sem_post(&G_log_buffer.input_level_sem);
return level;
}
#endif
#ifdef SYNC_GCC_ATOMIC
static void print_sync_mechanism(void)
{
printf("Sync mechanism = GCC ATOMIC\n");
}
static void set_input_level(int level)
{
__atomic_store_n(&G_log_buffer.input_level, level, __ATOMIC_SEQ_CST);
}
static int get_input_level(void)
{
return __atomic_load_n(&G_log_buffer.input_level, __ATOMIC_SEQ_CST);
}
#endif
#ifdef SYNC_GCC_SYNC
static void print_sync_mechanism(void)
{
printf("Sync mechanism = GCC LEGACY SYNC\n");
}
static void set_input_level(int level)
{
__sync_lock_test_and_set(&G_log_buffer.input_level, level);
}
static int get_input_level(void)
{
return __sync_fetch_and_add(&G_log_buffer.input_level, 0);
}
#endif
/********** log collecting thread ***********/
static void dump_log_buffer()
{
set_input_level(LOG_BUFFER_INPUT_LEVEL_NO_PRINTS); // do not allow input during dumping
pthread_mutex_lock(&G_log_buffer.log_buffer_lock);
usleep(MS_TO_US(TIMEOUT_MS + 1)); // simulate dumping log buffer
pthread_mutex_unlock(&G_log_buffer.log_buffer_lock);
set_input_level(LOG_BUFFER_INPUT_LEVEL_DEFAULT); // restore
}
static void *log_thread(void *arg)
{
unsigned long count = 0;
int seedp = 1;
srand(seedp);
for (; ;)
{
dump_log_buffer();
usleep(MS_TO_US(rand_r(&seedp) % 5));
++count;
if (count % 160 == 0) printf("Dumped %lu times of logs.\n", count); // prove thread is running
}
return NULL;
}
/********** business thread ***********/
static void write_log(int severity)
{
static unsigned int count = 0;
struct timeval start, end;
gettimeofday(&start, NULL);
if (severity < get_input_level()) return; // abort
pthread_mutex_lock(&G_log_buffer.log_buffer_lock);
usleep(100); // simulate writing to log buffer
pthread_mutex_unlock(&G_log_buffer.log_buffer_lock);
gettimeofday(&end, NULL);
int diff = (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000; // in ms
if (diff >= TIMEOUT_MS) printf("***** Business delayed %u time(s)! *****\n", ++count);
}
/* business thread prints logs randomly */
static void *business_thread(void *arg)
{
unsigned long count = 0;
for (; ;)
{
write_log(LOG_BUFFER_INPUT_LEVEL_DEFAULT);
++count;
if (count % 131072000 == 0) printf("Printed %lu lines of logs.\n", count); // prove thread is running
}
return NULL;
}
/********** init ***********/
static void init()
{
G_log_buffer.input_level = LOG_BUFFER_INPUT_LEVEL_DEFAULT;
pthread_mutex_init(&G_log_buffer.log_buffer_lock, NULL);
pthread_mutex_init(&G_log_buffer.input_level_lock, NULL);
sem_init(&G_log_buffer.input_level_sem, 0, 1); // binary semaphore
}
static void deinit()
{
pthread_mutex_destroy(&G_log_buffer.log_buffer_lock);
pthread_mutex_destroy(&G_log_buffer.input_level_lock);
sem_destroy(&G_log_buffer.input_level_sem);
}
int main(int argc, char *argv[])
{
init();
print_sync_mechanism();
pthread_t thread_id1, thread_id2;
pthread_create (&thread_id1, NULL, &log_thread, NULL);
pthread_create (&thread_id2, NULL, &business_thread, NULL);
pthread_join(thread_id1, NULL);
pthread_join(thread_id2, NULL);
deinit();
return 0;
}
我在尝试锁定log_buffer_lock 之前检查了input_level,并且input_level 受到保护。理论上不应该耽误业务(因为等待log_buffer_lockmutex)。
但它实际上延迟了。使用 gcc 旧版 __sync 函数时的程序输出:
$ ./simulate_case
Sync mechanism = GCC LEGACY SYNC
......
Dumped 2080 times of logs.
Printed 5373952000 lines of logs.
***** Business delayed 1 time(s)! *****
操作系统和 gcc 信息:
$ cat /etc/redhat-release
Red Hat Enterprise Linux Server release 6.5 (Santiago)
$ uname -a
Linux *** 3.17.8-13.el6.x86_64 #1 SMP Tue Mar 28 20:56:38 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$ gcc -v
Using built-in specs.
Target: x86_64-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-languages=c,c++,objc,obj-c++,java,fortran,ada --enable-java-awt=gtk --disable-dssi --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-1.5.0.0/jre --enable-libgcj-multifile --enable-java-maintainer-mode --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --disable-libjava-multilib --with-ppl --with-cloog --with-tune=generic --with-arch_32=i686 --build=x86_64-redhat-linux
Thread model: posix
gcc version 4.4.7 20120313 (Red Hat 4.4.7-4) (GCC)
同样发生在 Ubuntu 机器(Windows 10 Linux 子系统)上:
$ uname -a
Linux N-5CG8205MFD 4.4.0-43-Microsoft #1-Microsoft Wed Dec 31 14:42:53 PST 2014 x86_64 x86_64 x86_64 GNU/Linux
$ cat /etc/issue
Ubuntu 18.04.1 LTS \n \l
$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 7.3.0-27ubuntu1~18.04' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 7.3.0 (Ubuntu 7.3.0-27ubuntu1~18.04)
【问题讨论】:
-
来自 GCC 文档:“请注意,'__atomic' 内置函数假定程序将符合 C++11 内存模型。特别是,它们假定程序没有数据竞争。请参阅详细要求的 C++11 标准。”
-
我认为您可能会遇到数据竞赛,但更懂 C 的人可以证实这一点。您的
init()函数向G_log_buffer.input_level发出普通写入。您应该使写入原子。另一方面,我认为pthread_create()包含必要的内存屏障以使写入与以后的读取同步。 -
在您检查输入级别和获取互斥锁之间有一个机会之窗。这意味着读取
LOG_BUFFER_INPUT_LEVEL_DEFAULT仅意味着互斥锁当时尚未锁定,而不是下一刻不会锁定。也许尝试使用pthread_mutex_trylock并在失败时重新检查级别? -
嗨@Eric
init()函数在main()的开头被调用,其他线程尚未创建。在那里使用普通的 write 是安全的。 -
@Eric 的设计是使用
pthread_mutex_timedlock(),但这里我只想展示原子函数的问题。如果对input_level的保护有效,业务线程甚至没有机会锁定log_buffer_lock。
标签: c linux multithreading gcc