有多个线程,比如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
字段,这一定是可能的。有什么好主意吗?
答案 0 :(得分:7)
您是否尝试使用同步的解决方案,并发现它的性能不够好?你说你已经确定它的速度慢得令人无法接受 - 速度有多慢,你们已经有了性能预算吗?通常情况下,获得无争议的锁是非常便宜的,所以我不会期待它是一个问题。
可能有一些聪明的无锁解决方案 - 但它可能显着比只需要访问共享数据时同步更复杂。我知道无锁编码风靡一时,并且在你可以做到的时候可以很好地扩展 - 但如果你有一个线程干扰另一个数据,那么很难安全地进行编码。为了清楚起见,当我可以使用专家创建的高级抽象时,我喜欢使用无锁代码 - 比如.NET 4中的Parallel Extensions。我只是不喜欢使用低级如果我可以帮助它,那么em>像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[]
附加到队列中。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;
}
}