繁体   English   中英

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

[英]How to safely flush a buffer from a different thread, without synchronized methods?

有多个线程,比如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 这会导致数据损坏,我想阻止,但使用write()方法中的synchronized

我已经感觉到,使用正确的操作顺序和可能的一些volatile字段,这一定是可能的。 有什么好主意吗?

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

有可能是一些聪明的无锁的解决方案-但它很可能是显著比每当你需要访问共享数据只是同步更加复杂。 我知道无锁编码风靡一时,并且在你可以做到的时候可以很好地扩展 - 但如果你有一个线程干扰另一个数据,那么很难安全地进行编码。 为了清楚起见,当我可以使用专家创建的高级抽象时,我喜欢使用无锁代码 - 比如.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方法中检查是否需要刷新。 显然情况可能并非如此,但希望你能适应这个想法。

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

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

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

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

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

易失性变量和循环缓冲区

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

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

编写线程循环:

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

冲洗螺纹环:

  • writeIndex (原子)
  • flushIndex (原子)
  • 将缓冲区从flushIndexwriteIndex - 1
  • flushIndex (atomic)设置为为writeIndex读取的值

但是, 警告:为了实现这一点,缓冲区数组元素可能也需要是易失性的,而你现在还不能用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;
}

也许:

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对许多读者线程都很有用,只需很少的写线程 - 与你描述的情况相反。

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

您可以尝试实现信号量

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

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

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; 
    }

}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM