Uma "construção universal" é uma classe de wrapper para um objeto seqüencial que permite que ele seja linearizado (uma forte condição de consistência para objetos simultâneos). Por exemplo, aqui está uma construção sem espera adaptada, em Java, a partir de [1], que pressupõe a existência de uma fila sem espera que satisfaça a interface WFQ
(que requer apenas consenso único entre os segmentos) e assume uma Sequential
interface:
public interface WFQ<T> // "FIFO" iteration
{
int enqueue(T t); // returns the sequence number of t
Iterable<T> iterateUntil(int max); // iterates until sequence max
}
public interface Sequential
{
// Apply an invocation (method + arguments)
// and get a response (return value + state)
Response apply(Invocation i);
}
public interface Factory<T> { T generate(); } // generate new default object
public interface Universal extends Sequential {}
public class SlowUniversal implements Universal
{
Factory<? extends Sequential> generator;
WFQ<Invocation> wfq = new WFQ<Invocation>();
Universal(Factory<? extends Sequential> g) { generator = g; }
public Response apply(Invocation i)
{
int max = wfq.enqueue(i);
Sequential s = generator.generate();
for(Invocation invoc : wfq.iterateUntil(max))
s.apply(invoc);
return s.apply(i);
}
}
Essa implementação não é muito satisfatória, pois é muito lenta (você se lembra de todas as invocações e precisa repeti-las a cada aplicação - temos tempo de execução linear no tamanho do histórico). Existe alguma maneira de estender as interfaces WFQ
e Sequential
(de maneira razoável) para permitir salvar algumas etapas ao aplicar uma nova chamada?
Podemos tornar isso mais eficiente (tempo de execução não linear no tamanho do histórico, de preferência o uso da memória também diminui) sem perder a propriedade de espera?
Esclarecimento
Uma "construção universal" é um termo que eu tenho certeza que foi composto por [1], que aceita um objeto inseguro, mas compatível com o thread, que é generalizado pela Sequential
interface. Usando uma fila sem espera, a primeira construção oferece uma versão linearizável do objeto , segura para threads, também sem espera (isso pressupõe apply
operações de determinismo e parada ).
Isso é ineficiente, pois o método é efetivamente fazer com que cada encadeamento local inicie a partir de um slate limpo e aplique todas as operações já registradas a ele. Em qualquer caso, isso funciona porque atinge a sincronização de forma eficaz usando a WFQ
determinar a ordem em que todas as operações devem ser aplicadas: cada chamada segmento apply
vai ver o mesmo local, Sequential
objeto, com a mesma sequência de Invocation
s aplicadas a ele.
Minha pergunta é se podemos (por exemplo) introduzir um processo de limpeza em segundo plano que atualize o "estado inicial" para que não tenhamos que reiniciar do zero. Isso não é tão simples quanto ter um ponteiro atômico com um ponteiro inicial - esses tipos de abordagens perdem facilmente a garantia sem espera. Minha suspeita é que alguma outra abordagem baseada em fila possa funcionar aqui.
Jargão:
- sem espera - independentemente do número de threads ou da tomada de decisão do agendador,
apply
terminará em um número comprovadamente limitado de instruções executadas para esse thread. - livre de bloqueio - o mesmo que acima, mas admite a possibilidade de um tempo de execução ilimitado, apenas no caso de um número ilimitado de
apply
operações estar sendo realizado em outros threads. Normalmente, os esquemas de sincronização otimistas se enquadram nessa categoria. - bloqueio - eficiência à mercê do planejador.
Um exemplo de trabalho, conforme solicitado (agora em uma página que não expira)
[1] Herlihy e Shavit, a arte da programação de multiprocessadores .
CopyableSequential
sejam válidas - a linearizabilidade deve seguir o fato de que éSequential
.Respostas:
Aqui está uma explicação e exemplo de como isso é realizado. Deixe-me saber se há peças que não estão claras.
Gist com fonte
Universal
Inicialização:
Os índices de encadeamento são aplicados de maneira atomicamente incrementada. Isso é gerenciado usando um
AtomicInteger
nomenextIndex
. Esses índices são atribuídos aos threads por meio de umaThreadLocal
instância que se inicializa, obtendo o próximo índicenextIndex
e incrementando-o. Isso acontece na primeira vez que o índice de cada thread é recuperado na primeira vez. AThreadLocal
é criado para rastrear a última sequência criada por este encadeamento. É inicializado 0. A referência seqüencial do objeto de fábrica é passada e armazenada. DuasAtomicReferenceArray
instâncias são criadas de tamanhon
. O objeto final é atribuído a cada referência, tendo sido inicializado com o estado inicial fornecido pelaSequential
fábrica.n
é o número máximo de threads permitido. Cada elemento nessas matrizes 'pertence' ao índice de encadeamento correspondente.Aplicar método:
Este é o método que faz o trabalho interessante. Faz o seguinte:
Em seguida, o loop de seqüenciamento começa. Ele continuará até que a chamada atual tenha sido sequenciada:
decideNext()
A chave para o loop aninhado descrito acima é o
decideNext()
método Para entender isso, precisamos olhar para a classe Node.Classe de nó
Esta classe especifica nós em uma lista duplamente vinculada. Não há muita ação nesta classe. A maioria dos métodos são métodos de recuperação simples que devem ser bastante auto-explicativos.
método da cauda
isso retorna uma instância de nó especial com uma sequência de 0. Ele simplesmente atua como um marcador de posição até que uma chamada a substitua.
Propriedades e inicialização
seq
: o número de sequência, inicializado em -1 (significando sem sequência)invocation
: o valor da chamada deapply()
. Situado na construção.next
:AtomicReference
para o link direto. uma vez atribuído, isso nunca será alteradoprevious
:AtomicReference
para o link reverso atribuído após o seqüenciamento e limpo portruncate()
Decidir Próximo
Este método é apenas um no Nó com lógica não trivial. Em poucas palavras, um nó é oferecido como candidato para ser o próximo nó na lista vinculada. O
compareAndSet()
método verificará se sua referência é nula e, em caso afirmativo, defina a referência para o candidato. Se a referência já estiver definida, não fará nada. Esta operação é atômica, portanto, se dois candidatos forem oferecidos no mesmo momento, apenas um será selecionado. Isso garante que apenas um nó será selecionado como o próximo. Se o nó candidato estiver selecionado, sua sequência será configurada para o próximo valor e o link anterior será definido para este nó.Voltando à classe Universal, aplique o método ...
Depois de chamar
decideNext()
o último nó seqüenciado (quando marcado) com nosso nó ou um nó daannounce
matriz, há duas ocorrências possíveis: 1. O nó foi sequenciado com êxito 2. Algum outro encadeamento antecipou esse encadeamento.A próxima etapa é verificar se o nó foi criado para esta chamada. Isso pode acontecer porque esse encadeamento o sequenciou com êxito ou algum outro encadeamento o pegou da
announce
matriz e o sequenciou para nós. Se não foi sequenciado, o processo é repetido. Caso contrário, a chamada será concluída limpando a matriz de anúncios no índice desse encadeamento e retornando o valor resultante da chamada. A matriz de anúncio é limpa para garantir que não haja referências ao nó existente que impeçam a coleta de lixo do nó e, portanto, mantenha todos os nós na lista vinculada a partir desse ponto em tempo real no heap.Avaliar método
Agora que o nó da chamada foi sequenciado com êxito, a chamada precisa ser avaliada. Para fazer isso, o primeiro passo é garantir que as chamadas anteriores a esta tenham sido avaliadas. Se eles não tiverem esse segmento, não esperarão, mas farão esse trabalho imediatamente.
Método GuarantePrior
O
ensurePrior()
método faz esse trabalho verificando o nó anterior na lista vinculada. Se o estado não estiver definido, o nó anterior será avaliado. Nó que é recursivo. Se o nó anterior ao nó anterior não tiver sido avaliado, ele chamará a avaliação desse nó e assim por diante.Agora que o nó anterior é conhecido por ter um estado, podemos avaliar esse nó. O último nó é recuperado e designado a uma variável local. Se essa referência for nula, significa que algum outro encadeamento antecipou esse e já avaliou esse nó; definindo seu estado. Caso contrário, o estado do nó anterior é passado para o
Sequential
método de aplicação do objeto, juntamente com a invocação desse nó. O estado retornado é definido no nó e otruncate()
método é chamado, limpando o link para trás do nó, pois ele não é mais necessário.Método MoveForward
O método avançar tentará mover todas as referências principais para este nó se elas ainda não estiverem apontando para algo mais adiante. Isso é para garantir que, se um encadeamento parar de chamar, seu cabeçalho não reterá uma referência a um nó que não é mais necessário. O
compareAndSet()
método garantirá que apenas atualizemos o nó se algum outro encadeamento não o tiver alterado desde que foi recuperado.Anunciar matriz e ajudar
A chave para tornar essa abordagem livre de espera, em vez de simplesmente sem bloqueio, é que não podemos assumir que o agendador de encadeamentos dará prioridade a cada encadeamento quando necessário. Se cada thread simplesmente tentar sequenciar seus próprios nós, é possível que um thread possa ser continuamente esvaziado sob carga. Para explicar essa possibilidade, cada thread tentará primeiro 'ajudar' outros threads que talvez não possam ser sequenciados.
A idéia básica é que, conforme cada thread cria nós com êxito, as sequências atribuídas aumentam monotonicamente. Se um segmento ou segmentos estão antecipando continuamente outro segmento, o índice usado para encontrar nós não sequenciados na
announce
matriz avançará. Mesmo que todos os encadeamentos que atualmente tentam sequenciar um determinado nó sejam continuamente esvaziados por outro encadeamento, eventualmente todos os encadeamentos tentarão sequenciar esse nó. Para ilustrar, construiremos um exemplo com três threads.No ponto de partida, a cabeça dos três threads e os elementos de anúncio estão apontados para o
tail
nó. OlastSequence
para cada thread é 0.Neste ponto, o Thread 1 é executado com uma invocação. Ele verifica a matriz de anunciantes quanto à sua última sequência (zero), que é o nó que está atualmente programado para indexar. Sequencia o nó e
lastSequence
está definido como 1.O segmento 2 agora é executado com uma invocação, verifica a matriz de anúncios na sua última sequência (zero) e vê que não precisa de ajuda e, portanto, tenta sequenciar sua invocação. É bem-sucedido e agora
lastSequence
está definido como 2.O segmento 3 agora é executado e também vê que o nó em
announce[0]
já está sequenciado e sequencia sua própria invocação. AgoralastSequence
está definido como 3.Agora o segmento 1 é chamado novamente. Ele verifica a matriz de anúncios no índice 1 e descobre que ela já está sequenciada. Simultaneamente, o Thread 2 é chamado. Ele verifica a matriz de anúncios no índice 2 e descobre que ela já está sequenciada. Ambos Thread 1 e Thread 2 agora tentar sequenciar seus próprios nós. O segmento 2 vence e sequencia sua invocação. Ele
lastSequence
definido como 4. Enquanto isso, o segmento três foi chamado. Ele verifica o índicelastSequence
(mod 3) e descobre que o nó emannounce[0]
não foi sequenciado. O segmento 2 é novamente chamado ao mesmo tempo que o segmento 1 está na segunda tentativa. Tópico 1localiza uma chamada não sequencial naannounce[1]
qual é o nó recém-criado pelo Thread 2 . Ele tenta sequenciar a chamada do Thread 2 e obtém êxito. O segmento 2 encontra seu próprio nóannounce[1]
e foi sequenciado. É definidolastSequence
como 5. O thread 3 é chamado e descobre que o nó no qual o thread 1 colocadoannounce[0]
ainda não é sequenciado e tenta fazer isso. Enquanto isso, o Thread 2 também foi chamado e pré-impõe o Thread 3. Sequencia seu nó e o definelastSequence
para 6.Pobre Thread 1 . Mesmo que o Thread 3 esteja tentando sequenciá-lo, ambos os threads foram continuamente impedidos pelo agendador. Mas neste momento. Tópico 2 também está agora apontando para
announce[0]
(6 mod 3). Todos os três encadeamentos estão configurados para tentar sequenciar a mesma chamada. Não importa qual thread seja bem-sucedido, o próximo nó a ser sequenciado será a chamada em espera do Thread 1, ou seja, o nó referenciado porannounce[0]
.Isso é inevitável. Para que os encadeamentos sejam antecipados, outros encadeamentos devem ser nós de seqüenciamento e, ao fazê-lo, eles moverão continuamente seus
lastSequence
frente. Se o nó de um determinado encadeamento não for continuamente sequenciado, eventualmente todos os encadeamentos apontarão para seu índice na matriz de anúncios. Nenhum encadeamento fará mais nada até que o nó que está tentando ajudar seja sequenciado, o pior cenário é que todos os encadeamentos estejam apontando para o mesmo nó não sequenciado. Portanto, o tempo necessário para sequenciar qualquer chamada é uma função do número de threads e não do tamanho da entrada.fonte
previous
enext
para ser válido. Manter e criar um histórico válido de maneira sem espera parece difícil.Minha resposta anterior realmente não responde à pergunta corretamente, mas como o OP a considera útil, vou deixar como está. Com base no código no link da pergunta, aqui está minha tentativa. Fiz apenas testes realmente básicos sobre isso, mas parece calcular as médias corretamente. Comentários bem-vindos quanto a se isso é adequadamente sem espera.
OBSERVAÇÃO : removi a interface Universal e a tornei uma classe. Ter o Universal sendo composto de Sequenciais, além de ser um, parece uma complicação desnecessária, mas posso estar perdendo alguma coisa. Na classe média, marquei a variável de estado como
volatile
. Isso não é necessário para fazer o código funcionar. Ser conservador (uma boa idéia com o encadeamento) e impedir que cada thread faça todos os cálculos (uma vez).Sequencial e Fábrica
Universal
Média
Código de demonstração
Fiz algumas edições no código ao publicá-lo aqui. Deve estar tudo bem, mas deixe-me saber se você tiver problemas com isso.
fonte
wfq
, portanto você ainda precisa percorrer toda a história - o tempo de execução não melhorou, exceto por um fator constante.