[英]Do Synchronized Methods run on a different thread from the UI thread (Android)?
[英]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
(原子) flushIndex
到writeIndex - 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[]
附加到队列中。 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.