如何在没有同步方法的情况下安全地从不同的线程刷新缓冲区?

时间:2011-02-12 09:22:38

标签: java multithreading synchronized

有多个线程,比如B,C和D,每个线程都以高频率将小数据包写入缓冲区。他们拥有自己的缓冲区,没有其他人写过它。写作必须尽可能快,我已经确定使用synchronized会让它变得无法接受。

缓冲区只是字节数组,以及第一个自由元素的索引:

byte[] buffer;
int index;

public void write(byte[] data) {
    // some checking that the buffer won't overflow... not important now
    System.arraycopy(data, 0, buffer, index, data.length);
    index += data.length;
}

每隔一段时间,线程A就会将每个人的缓冲区刷新到文件中。如果这部分有一些开销可以,所以在这里使用synchronized没问题。

现在麻烦的是,其他一些线程可能正在写入缓冲区,而线程A正在刷新它。这意味着两个线程会在同一时间尝试写入index。这会导致数据损坏,我想阻止,但没有synchronized方法中使用write()

我觉得,使用正确的操作顺序以及可能的某些volatile字段,这一定是可能的。有什么好主意吗?

7 个答案:

答案 0 :(得分:7)

您是否尝试使用同步的解决方案,并发现它的性能不够好?你说你已经确定它的速度慢得令人无法接受 - 速度有多慢,你们已经有了性能预算吗?通常情况下,获得无争议的锁是非常便宜的,所以我不会期待它是一个问题。

可能有一些聪明的无锁解决方案 - 但它可能显着比只需要访问共享数据时同步更复杂。我知道无锁编码风靡一时,并且在你可以做到的时候可以很好地扩展 - 但如果你有一个线程干扰另一个数据,那么很难安全地进行编码。为了清楚起见,当我可以使用专家创建的高级抽象时,我喜欢使用无锁代码 - 比如.NET 4中的Parallel Extensions。我只是不喜欢使用低级像volatile变量这样的抽象。

尝试锁定并对其进行基准测试。找出可接受的性能,并将简单解决方案的性能与该目标进行比较。

当然,一个选项是重新设计...刷新是否在不同的线程中主动发生?个别编写器线程是否可以定期将缓冲区切换到刷新线程(并启动不同的缓冲区)?这会让事情变得更简单。

编辑:关于你的“冲洗信号”的想法 - 我一直在思考类似的问题。但是你需要注意你是如何做到的,这样即使一个线程需要很长时间来处理它正在做的事情,信号也不会丢失。我建议你让线程A发布一个“刷新计数器”......并且每个线程在上次刷新时保持自己的计数器。

编辑:刚刚意识到这是Java,而不是C# - 更新:)

使用AtomicLong.incrementAndGet()从线程A递增,使用AtomicLong.get()从其他线程读取。然后在每个线程中,比较您是否“最新”,并在必要时刷新:

private long lastFlush; // Last counter for our flush
private Flusher flusher; // The single flusher used by all threads 

public void write(...)
{
    long latestFlush = flusher.getCount(); // Will use AtomicLong.get() internally
    if (latestFlush > lastFlush)
    {
        flusher.Flush(data);
        // Do whatever else you need
        lastFlush = latestFlush; // Don't use flusher.getCount() here!
    }
    // Now do the normal write
}

请注意,这假设您只需要在Write方法中检查是否需要刷新。显然情况可能并非如此,但希望你能适应这个想法。

答案 1 :(得分:1)

您可以单独使用volatile来安全地读取/写入缓冲区(如果您只有一个编写器),但是,只有一个线程可以安全地刷新数据。为此,您可以使用环形缓冲区。

我想补充@ Jon的评论,这个测试要复杂得多。例如我有一个“解决方案”,有一天一直工作10亿条消息,但由于盒子载满了而不断打破下一条消息。

通过同步,您的延迟应低于2微秒。使用Lock,您可以将其降低到1微秒。忙于等待一个易失性,你可以将其降低到每字节3-6 ns(在线程之间传输数据所需的时间变得很重要)

注意:随着数据量的增加,锁的相对成本变得不那么重要了。例如如果你通常写200字节或更多,我不会担心差异。

我采用的一种方法是将交换器与两个直接的ByteBuffers一起使用,并避免在关键路径中写入任何数据(即只有在我处理完所有内容之后才写入数据并且这并不重要)

答案 2 :(得分:1)

易失性变量和循环缓冲区

使用循环缓冲区,并使刷新线程“追逐”缓冲区周围的写入,而不是在每次刷新后将索引重置为零。这允许在刷新期间进行写入而不进行任何锁定。

使用两个volatile变量 - writeIndex用于写入线程所在的位置,使用flushIndex用于刷新线程所在的位置。这些变量每个只由一个线程更新,并且可以由另一个线程原子读取。使用这些变量可以将线程约束为缓冲区的各个部分。不允许冲洗线程经过写入线程所在的位置(即冲洗缓冲区的未写入部分)。不允许写入线程经过冲洗线程所在的位置(即覆盖缓冲区的未刷新部分)。

编写线程循环:

  • 阅读writeIndex(原子)
  • 阅读flushIndex(原子)
  • 检查此写入不会覆盖未刷新的数据
  • 写入缓冲区
  • 计算writeIndex
  • 的新值
  • 设置writeIndex(原子)

冲洗线程循环:

  • 阅读writeIndex(原子)
  • 阅读flushIndex(原子)
  • 将缓冲区从flushIndex刷新到writeIndex - 1
  • flushIndex(原子)设置为为writeIndex
  • 读取的值

但是,警告:为了使其工作,缓冲区数组元素可能也需要是volatile,而你还不能用Java做。见http://jeremymanson.blogspot.com/2009/06/volatile-arrays-in-java.html

尽管如此,这是我的实施(欢迎更改):

volatile int writeIndex = 0;
volatile int flushIndex = 0;
byte[] buffer = new byte[268435456];

public void write(byte[] data) throws Exception {
    int localWriteIndex = writeIndex; // volatile read
    int localFlushIndex = flushIndex; // volatile read

    int freeBuffer = buffer.length - (localWriteIndex - localFlushIndex +
        buffer.length) % buffer.length;

    if (data.length > freeBuffer)
        throw new Exception("Buffer overflow");

    if (localWriteIndex + data.length <= buffer.length) {
        System.arraycopy(data, 0, buffer, localWriteIndex, data.length);
        writeIndex = localWriteIndex + data.length;
    }
    else
    {
        int firstPartLength = buffer.length - localWriteIndex;
        int secondPartLength = data.length - firstPartLength;

        System.arraycopy(data, 0, buffer, localWriteIndex, firstPartLength);
        System.arraycopy(data, firstPartLength, buffer, 0, secondPartLength);

        writeIndex = secondPartLength;
    }
}

public byte[] flush() {
    int localWriteIndex = writeIndex; // volatile read
    int localFlushIndex = flushIndex; // volatile read

    int usedBuffer = (localWriteIndex - localFlushIndex + buffer.length) %
        buffer.length;
    byte[] output = new byte[usedBuffer];

    if (localFlushIndex + usedBuffer <= buffer.length) {
        System.arraycopy(buffer, localFlushIndex, output, 0, usedBuffer);
        flushIndex = localFlushIndex + usedBuffer;
    }
    else {
        int firstPartLength = buffer.length - localFlushIndex;
        int secondPartLength = usedBuffer - firstPartLength;

        System.arraycopy(buffer, localFlushIndex, output, 0, firstPartLength);
        System.arraycopy(buffer, 0, output, firstPartLength, secondPartLength);

        flushIndex = secondPartLength;
    }

    return output;
}

答案 3 :(得分:1)

也许:

import java.util.concurrent.atomic;    

byte[] buffer;
AtomicInteger index;

public void write(byte[] data) {
    // some checking that the buffer won't overflow... not important now
    System.arraycopy(data, 0, buffer, index, data.length);
    index.addAndGet(data.length);
}

public int getIndex() {
    return index.get().intValue();
}

否则java.util.concurrent.lock包中的锁类比synchronized关键字更轻......

这样:

byte[] buffer;
int index;
ReentrantReadWriteLock lock;

public void write(byte[] data) {
    lock.writeLock().lock();
    // some checking that the buffer won't overflow... not important now
    System.arraycopy(data, 0, buffer, index, data.length);
    index += data.length;
    lock.writeLock.unlock();
}

并且在冲洗线程中:

object.lock.readLock().lock(); 
// flush the buffer      
object.index = 0;                     
object.lock.readLock().unlock();

<强>更新
您描述的用于读取和写入缓冲区的模式不会受益于使用ReadWriteLock实现,因此只需使用简单的ReentrantLock:

final int SIZE = 99;
byte[] buffer = new byte[SIZE];
int index;
// Use default non-fair lock to maximise throughput (although some writer threads may wait longer)
ReentrantLock lock = new ReentrantLock();

// called by many threads
public void write(byte[] data) {
    lock.lock();
    // some checking that the buffer won't overflow... not important now        
    System.arraycopy(data, 0, buffer, index, data.length);
    index += data.length;
    lock.unlock();
}

// Only called by 1 thread - or implemented in only 1 thread:
public byte[] flush() {
    byte[] rval = new byte[index];
    lock.lock();
    System.arraycopy(buffer, 0, rval, 0, index);
    index = 0;
    lock.unlock();
    return rval;
}

当您使用单个读取器/刷新线程描述使用尽可能多的写入线程时,ReadWriteLock不是必需的,事实上,我相信它比简单的ReentrantLock(?)更重要。 ReadWriteLocks对许多读者线程都很有用,只需很少的写线程 - 与你描述的情况相反。

答案 4 :(得分:1)

反转控制。不要让A轮询其他线程,让他们推动。

我认为LinkedBlockingQueue可能是最简单的事情。

伪代码:

LinkedBlockingQueue<byte[]> jobs;//here the buffers intended to be flushed are pushed into 
LinkedBlockingQueue<byte[]> pool;//here the flushed buffers are pushed into for reuse

写线程:

while (someCondition) {
     job = jobs.take();
     actualOutput(job);
     pool.offer(job);
}

其他主题:

void flush() {
     jobs.offer(this.buffer);
     this.index = 0;
     this.buffer = pool.poll();
     if (this.buffer == null) 
          this.buffer = createNewBuffer();
}
void write(byte[] data) {
    // some checking that the buffer won't overflow... not important now
    System.arraycopy(data, 0, buffer, index, data.length);
    if ((index += data.length) > threshold) 
         this.flush();
}

LinkedBlockingQueue基本上封装了在线程之间安全传递消息的技术手段 这种方式不仅更简单,而且显然会引起关注,因为实际生成输出的线程确定何时需要刷新缓冲区,并且它们是唯一维持自己状态的线程。
两个队列中的缓冲区都会产生内存开销,但这应该是可以接受的。池不可能比线程总数大得多,并且除非实际输出存在瓶颈,否则作业队列在大多数情况下应该是空的。

答案 5 :(得分:0)

您可以尝试实施semaphores

答案 6 :(得分:0)

我喜欢无锁的东西,它让人上瘾:)。确保休息:他们消除了许多锁定缺点,带来了一些陡峭的学习曲线。他们仍然容易出错。

阅读一些文章,也许是一本书并试着回家1。 如何处理你的案子?您不能自动复制数据(和更新大小),但您可以自动更新对该数据的引用 简单的方法;注意:你总是可以从缓冲区中读取没有锁定这就是整个点。

final AtomicReference<byte[]> buffer=new AtomicReference<byte[]>(new byte[0]);
void write(byte[] b){
    for(;;){
        final byte[] cur = buffer.get();
        final byte[] copy = Arrays.copyOf(cur, cur.length+b.length);
        System.arraycopy(b, 0, cur, cur.length, b.length);
        if (buffer.compareAndSet(cur, copy)){
            break;
        }
            //there was a concurrent write
            //need to handle it, either loop to add at the end but then you can get out of order
            //just as sync
    }
}

你实际上你仍然可以使用更大的字节[]并附加到它但我自己离开了练习。


<强>续

我必须在紧要关头编写代码。简短描述如下: 由于使用了CLQ,代码无锁,但没有阻碍。正如您所看到的,无论条件如何,代码始终都会继续,并且除了CLQ之外,实际上不会循环(忙等待)。

许多无锁算法依靠所有线程的帮助来正确完成任务。 可能有一些错误,但我希望主要的想法是明确的:

  • 该算法允许许多作家,许多读者
  • 如果无法更改主state,因此只有一个编写器,请将byte[]附加到队列中。
  • 任何编写者(在CAS上成功)必须在编写自己的数据之前尝试刷新队列。
  • 在使用主缓冲区
  • 之前,读者必须检查挂起的写入并刷新它们
  • 如果放大(当前字节[]不够),则必须丢弃缓冲区和大小,并使用新一代的Buffer + Size。否则只增加size。该操作再次需要保持锁定(即CAS成功)

请欢迎任何反馈。 干杯,希望人们可以热身到无锁结构算法。

package bestsss.util;

import java.util.Arrays;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;

//the code uses ConcurrentLinkedQueue to simplify the implementation
//the class is well - know and the main point is to demonstrate the  lock-free stuff
public class TheBuffer{
    //buffer generation, if the room is exhaused need to update w/ a new refence
    private static class BufGen{
        final byte[] data;
        volatile int size;

        BufGen(int capacity, int size, byte[] src){
            this.data = Arrays.copyOf(src, capacity);
            this.size  = size;
        }

        BufGen append(byte[] b){
            int s = this.size;
            int newSize = b.length+s;
            BufGen target;
            if (newSize>data.length){
                int cap = Integer.highestOneBit(newSize)<<1;
                if (cap<0){
                    cap = Integer.MAX_VALUE;                    
                }               
                target = new BufGen(cap, this.size, this.data);             
            } 
            else if(newSize<0){//overflow 
                throw new IllegalStateException("Buffer overflow - over int size");
            } else{ 
                target = this;//if there is enough room(-service), reuse the buffer
            }
            System.arraycopy(b, 0, target.data, s, b.length);
            target.size = newSize;//'commit' the changes and update the size the copy part, so both are visible at the same time
            //that's the volatile write I was talking about
            return target;
        }       
    }

    private volatile BufGen buffer = new BufGen(16,0,new byte[0]);

    //read consist of 3 volatile reads most of the time, can be 2 if BufGen is recreated each time
    public byte[] read(int[] targetSize){//ala AtomicStampedReference
        if (!pendingWrites.isEmpty()){//optimistic check, do not grab the look and just do a volatile-read
            //that will serve 99%++ of the cases
            doWrite(null, READ);//yet something in the queue, help the writers
        }
        BufGen buffer = this.buffer;
        targetSize[0]=buffer.size;
        return  buffer.data;
    }
    public void write(byte[] b){
        doWrite(b, WRITE);
    }

    private static final int FREE = 0;
    private static final int WRITE = 1;
    private static final int READ= 2;

    private final AtomicInteger state = new AtomicInteger(FREE);
    private final ConcurrentLinkedQueue<byte[]> pendingWrites=new ConcurrentLinkedQueue<byte[]>();
    private void doWrite(byte[] b, int operation) {
        if (state.compareAndSet(FREE, operation)){//won the CAS hurray!
            //now the state is held "exclusive"
            try{
                //1st be nice and poll the queue, that gives fast track on the loser
                //we too nice 
                BufGen buffer = this.buffer;
                for(byte[] pending; null!=(pending=pendingWrites.poll());){
                    buffer = buffer.append(pending);//do not update the global buffer yet
                }
                if (b!=null){
                    buffer = buffer.append(b);
                }
                this.buffer = buffer;//volatile write and make sure any data is updated
            }finally{
                state.set(FREE);
            }
        } 
        else{//we lost the CAS, well someone must take care of the pending operation 
            if (b==null)
                return;

            pendingWrites.add(b);           
        }
    }


    public static void main(String[] args) {
        //usage only, not a test for conucrrency correctness
        TheBuffer buf = new TheBuffer();        
        buf.write("X0X\n".getBytes());
        buf.write("XXXXXXXXXXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXXXXXXXXXXXXXXXXXXX\n".getBytes());
        buf.write("Hello world\n".getBytes());
        int[] size={0};
        byte[] bytes = buf.read(size);
        System.out.println(new String(bytes, 0, size[0]));
    }
}

简单案例

另一个更简单的解决方案,允许许多编写者,但读者。它将写入推迟到CLQ中,读者只是重建了它们。这次简化了施工规范。

package bestsss.util;

import java.util.ArrayList;
import java.util.concurrent.ConcurrentLinkedQueue;

public class TheSimpleBuffer {
    private final ConcurrentLinkedQueue<byte[]> writes =new ConcurrentLinkedQueue<byte[]>();
    public void write(byte[] b){
        writes.add(b);
    }

    private byte[] buffer;
    public byte[] read(int[] targetSize){
        ArrayList<byte[]> copy = new ArrayList<byte[]>(12);
        int len = 0;
        for (byte[] b; null!=(b=writes.poll());){
            copy.add(b);
            len+=b.length;
            if (len<0){//cant return this big, overflow 
                len-=b.length;//fix back;
                break;
            }
        }
        //copy, to the buffer, create new etc....
        //...

        ///
        targetSize[0]=len;
        return buffer; 
    }

}