我有一个使用共享FIFO的实时应用程序。有几个编写器进程和一个阅读器进程。数据被定期写入FIFO并不断耗尽。从理论上讲,FIFO永远不会溢出,因为读取速度比所有写入器组合的速度快。但是,FIFO会溢出。
我试图重现这个问题,最后得出以下(简化)代码:
#include <stdint.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <pthread.h>
#include <semaphore.h>
#include <sys/time.h>
#include <unistd.h>
class Fifo
{
public:
Fifo() : _deq(0), _wptr(0), _rptr(0), _lock(0)
{
memset(_data, 0, sizeof(_data));
sem_init(&_data_avail, 1, 0);
}
~Fifo()
{
sem_destroy(&_data_avail);
}
void Enqueue()
{
struct timeval tv;
gettimeofday(&tv, NULL);
uint64_t enq = tv.tv_usec + tv.tv_sec * 1000000;
while (__sync_lock_test_and_set(&_lock, 1))
sched_yield();
uint8_t wptr = _wptr;
uint8_t next_wptr = (wptr + 1) % c_entries;
int retry = 0;
while (next_wptr == _rptr) // will become full
{
printf("retry=%u enq=%lu deq=%lu count=%d\n", retry, enq, _deq, Count());
for (uint8_t i = _rptr; i != _wptr; i = (i+1)%c_entries)
printf("%u: %lu\n", i, _data[i]);
assert(retry++ < 2);
usleep(500);
}
assert(__sync_bool_compare_and_swap(&_wptr, wptr, next_wptr));
_data[wptr] = enq;
__sync_lock_release(&_lock);
sem_post(&_data_avail);
}
int Dequeue()
{
struct timeval tv;
gettimeofday(&tv, NULL);
uint64_t deq = tv.tv_usec + tv.tv_sec * 1000000;
_deq = deq;
uint8_t rptr = _rptr, wptr = _wptr;
uint8_t next_rptr = (rptr + 1) % c_entries;
bool empty = Count() == 0;
assert(!sem_wait(&_data_avail));// bug in sem_wait?
_deq = 0;
uint64_t enq = _data[rptr]; // enqueue time
assert(__sync_bool_compare_and_swap(&_rptr, rptr, next_rptr));
int latency = deq - enq; // latency from enqueue to dequeue
if (empty && latency < -500)
{
printf("before dequeue: w=%u r=%u; after dequeue: w=%u r=%u; %d\n", wptr, rptr, _wptr, _rptr, latency);
}
return latency;
}
int Count()
{
int count = 0;
assert(!sem_getvalue(&_data_avail, &count));
return count;
}
static const unsigned c_entries = 16;
private:
sem_t _data_avail;
uint64_t _data[c_entries];
volatile uint64_t _deq; // non-0 indicates when dequeue happened
volatile uint8_t _wptr, _rptr; // write, read pointers
volatile uint8_t _lock; // write lock
};
static const unsigned c_total = 10000000;
static const unsigned c_writers = 3;
static Fifo s_fifo;
// writer thread
void* Writer(void* arg)
{
for (unsigned i = 0; i < c_total; i++)
{
int t = rand() % 200 + 200; // [200, 399]
usleep(t);
s_fifo.Enqueue();
}
return NULL;
}
int main()
{
pthread_t thread[c_writers];
for (unsigned i = 0; i < c_writers; i++)
pthread_create(&thread[i], NULL, Writer, NULL);
for (unsigned total = 0; total < c_total*c_writers; total++)
s_fifo.Dequeue();
}
当Enqueue()溢出时,调试打印指示Dequeue()被卡住(因为_deq不为0)。 Dequeue()可能卡住的唯一地方是sem_wait()。但是,由于fifo已满(也由sem_getvalue()确认),我不明白这是怎么回事。即使经过几次重试(每次等待500us),即使在Enqueue()完全停止(忙于重试)时Dequeue()肯定会耗尽,它仍然是满的。
在代码示例中,有3个写入器,每个写入器每200-400us。在我的计算机上(8核i7-2860运行centOS 6.5内核2.6.32-279.22.1.el6.x86_64,g ++ 4.47 20120313),代码将在几分钟内失败。我也尝试了其他几个centOS系统,也失败了。
我知道使fifo更大可以降低溢出概率(事实上,程序仍然失败,c_entries = 128),但在我的实时应用程序中,对enqueue-dequeue延迟有严格限制,因此必须排空数据很快。如果它不是sem_wait()中的错误,那么是什么阻止它获取信号量?
P.S。如果我更换
assert(!sem_wait(&_data_avail));// bug in sem_wait?
与
while (sem_trywait(&_data_avail) < 0) sched_yield();
然后程序运行正常。所以sem_wait()和/或调度程序似乎有问题。
答案 0 :(得分:0)
您需要结合使用sem_wait / sem_post调用来管理读写线程。
您的enqueue线程仅执行sem_post,您的dequeue只执行sem_wait调用。你需要将sem_wait添加到enqueue线程和dequeue线程上的sem_post。
很久以前,我实现了让多个线程/进程能够读取一些共享内存并且只有一个线程/进程写入共享内存的能力。我使用了两个信号量,一个写信号量和一个读取信号量。读线程将等到写信号量未设置,然后它将设置读信号量。写线程将设置写信号量,然后等待直到未设置读信号量。然后,读取和写入线程将在完成任务后取消设置信号量集。读取的信号量可以有n个线程一次锁定读取信号量,而写入信号量一次可以被一个线程锁定。
答案 1 :(得分:0)
如果它不是sem_wait()中的错误,那么阻止它获取的是什么 信号量?
你的节目不耐烦会阻止它。无法保证Dequeue()
线程在给定的重试次数内进行调度。如果你改变了
assert(retry++ < 2);
到
retry++;
你会发现程序有时会在8次或甚至更多次重试之后继续读取程序。
为什么Enqueue必须重试?
必须重试,因为当时尚未安排main
主题的Dequeue()
。
出队速度比所有作家组合快得多。
您的程序显示此假设有时是错误的。虽然显然Dequeue()
的执行时间比编写者的执行时间短得多(由于usleep(t)
),但这并不意味着Dequeue()
更频繁地由完全公平调度程序安排 - 为此,主要原因是您使用了非确定性调度策略。 man sched_yield
:
sched_yield() is intended for use with read-time scheduling policies (i.e., SCHED_FIFO or SCHED_RR). Use of sched_yield() with nondeterministic scheduling policies such as SCHED_OTHER is unspecified and very likely means your application design is broken.
如果您插入
struct sched_param param = { .sched_priority = 1 };
if (sched_setscheduler(0, SCHED_FIFO, ¶m) < 0)
perror("sched_setscheduler");
在main()
开始时,您可能会看到您的程序按预期执行(使用适当的权限运行时)。