Programação Concorrente (CC3037), DCC/FCUP
Eduardo R. B. Marques, DCC/FCUP
Objectivos: Introdução ao uso da ferramenta Cooperari.
Certifique-se que a variável de ambiente PATH é configurada
export PATH=$HOME/progconex4/cooperari-0.4/bin:$PATH
Se quiser instalar no seu PC siga os passos disponibilizados no guia "Getting Started" para instalar o Cooperari no PC local e testar o funcionamento dos utilitários cjavac
e cjunit
.
Notas:
-
Os comandos
cjavac
ecjunit
deverão ser executados a partir do directório de topo. -
O código fonte de apoio aos exercícios encontra-se na pasta
src
. Usandocjavac
o código será compilado para o directórioclasses
. -
Logs e outros ficheiros produzidos pelo Cooperari serão armazenados na pasta
cdata
.
O interface Counter
define em abstracto as operações para um "contador" inteiro com as operações:
increment()
: incrementa o valor do contador;value()
: obtém valor do contador.
As seguintes classes definem implementações de Counter
, com diferentes
tipos de sincronização entre threads:
UCounter
: sem mecanismo de sincronização ("unsynchronized");LCounter
: baseada em locks (na forma de blocossynchronized
);ACounter
: baseada no uso de AtomicInteger.
Execute cjunit pc.counter.AllTests
onde cada uma destas implementações é testada pelo Cooperari. O teste feito é expresso pelo código abaixo,
correspondente à execução de 2 threads que incrementam cada uma o contador
uma vez, seguida da verificação do valor do contador (isto é,
que o contador foi incrementado em duas unidades).
@Test
public void test() {
Counter c = createCounter(0);
CSystem.forkAndJoin(
() -> c.increment(),
() -> c.increment()
);
assertEquals(2, c.value());
}
Executando no terminal a partir do directório de topo:
cjavac # para compilar ou re-compilar
cjunit pc.counter.AllTests
deverá obter um output semelhante a:
Configuring load-time weaving ...
JAR file for 'pc.counter.AllTests' saved to './cdata/pc.counter.AllTests-cooperari.jar'
== Cooperari 0.4 - JUnit test execution - mode: cooperative ==
pc.counter.UTest
test [failed: java.lang.AssertionError]
> trials: 1 time: 124 ms coverage: 100.0 % (5 / 5 yp)
> failure trace: '/Users/edrdo/Desktop/Worklog/2019/aulas/pc/homepage/aulas/lab2/pcex3/cdata/pc.counter.UTest/test.1.trace.log'
pc.counter.LTest
test [passed]
> trials: 25 time: 353 ms coverage: 100.0 % (7 / 7 yp)
pc.counter.ATest
test [failed: java.lang.AssertionError]
> trials: 2 time: 57 ms coverage: 100.0 % (8 / 8 yp)
> failure trace: '/Users/edrdo/Desktop/Worklog/2019/aulas/pc/homepage/aulas/lab2/pcex3/cdata/pc.counter.ATest/test.2.trace.log'
== Summary ==
Executed: 3; Skipped: 0; Failed: 2; Execution time: 637 ms
== Yield point coverage ==
Coverage rate: 100.0 % (18 / 18 yp)
Global coverage report: '/Users/edrdo/Desktop/Worklog/2019/aulas/pc/homepage/aulas/lab2/pcex3/cdata/all_yield_points.coverage.log'
Examine o log da execução assinalado em correspondência
à falha na execução de UTest.test()
. Dado que o contador não é sincronizado,
"race conditions" são possíveis e é isso que o log assinala nas entradas
em que o campo EVENT
tem valor R
, como no seguinte fragmento em que temos
uma corrida entre as threads 1 (lê) e 2 (escreve) no acesso ao valor do contador.
# EXECUTION TRACE
# TID STEP EVENT SOURCE FILE LINE YIELD POINT STAGE
...
10 1 2 R UCounter.java 12 field-get(pc.counter.UCounter.value) 0
11 1 2 - UCounter.java 12 field-set(pc.counter.UCounter.value) 0
12 2 3 R UCounter.java 12 field-set(pc.counter.UCounter.value) 0
O teste LCounter.test()
não falha. Observe que o código em LCounter
usa um bloco synchronized
em
increment()
prevenindo "race conditions". Experimente modificar o código da forma
ilustrada abaixo e repita a execução do teste. Já deverá obter falhas na execução para porque
"race conditions" se tornam possíveis.
@Override
public void increment() {
int v = value;
synchronized(this) {
value = v + 1;
}
}
O teste ATest.test()
falha. Note que ACounter
usa um objecto AtomicInteger
mas increment()
compreende 2 instruções atómicas sobre esse objecto, uma de leitura (get()
) e outra de escrita (set()
). Como pode resolver o problema?
O interface Register<T>
define um TAD para um registo com as operações:
T read()
: lê valor do registo;void write(T value)
: escreve valorvalue
no registo;transform(Function<T,T> func)
: aplicafunc
sobre o valor anterior do registo, e escreve o resultado obtido.
Analogamente ao exercício anterior temos classes URegister
, LRegister
e ARegister
que implementam Register
, respectivamente: sem uso de mecanismos de sincronização,
baseada em locks, e em instruções atómicas via AtomicReference
.
Em test
encontra o seguinte código sobre um registo r
com tipo Register<Integer>
em que:
- são executadas 3 threads cada uma executando uma operação diferente sobre o registo;
- são validados todos os valores possíveis para a leitura subsequente do registo numa execução correcta. Note que haverá 6 execuções (correctas) possíveis das 3 operações mas só 4 valores possíveis resultantes para o registo no final.
@Test
public void test() {
Register<Integer> r = createRegister(0);
CSystem.forkAndJoin(
() -> r.transform(x -> x + 1),
() -> r.transform(x -> x * x * x),
() -> r.transform(x -> 2 * x + 1)
);
int v = r.read();
assertTrue(v == 3 || v == 27 || v == 2 || v == 8);
}
Executando a partir do directório de topo:
cjavac
cjunit pc.register.AllTests
deverá obter um output semelhante a:
...
pc.register.UTest
test [failed: java.lang.AssertionError]
> trials: 2 time: 142 ms coverage: 83.3 % (5 / 6 yp)
> failure trace: '/Users/edrdo/Desktop/Worklog/2019/aulas/pc/homepage/aulas/lab2/pcex3/cdata/pc.register.UTest/test.2.trace.log'
pc.register.LTest
test [passed]
> trials: 25 time: 510 ms coverage: 69.2 % (9 / 13 yp)
pc.register.ATest
test [failed: java.lang.AssertionError]
> trials: 2 time: 65 ms coverage: 80.0 % (8 / 10 yp)
> failure trace: '/Users/edrdo/Desktop/Worklog/2019/aulas/pc/homepage/aulas/lab2/pcex3/cdata/pc.register.ATest/test.2.trace.log'
...
Analogamente a 1.1 examine o log de falha e perceba o que acontece em termos de "race conditions".
Analogamente a 1.2 introduza uma "race condition" em LRegister
e verifique
que LTest.test()
falha.
O teste ATest.test()
falha porque a implementação de ARegister.transform()
aplica a transformação do registo recorrendo a uma leitura e uma escrita sobre a instância de AtomicReference
.
Modifique o código por forma a funcionar correctamente usando AtomicReference.compareAndSet()
.
Valide as suas alterações voltando a executar os testes.
Numa 2ª versão poderá fazer uso de AtomicReference.getAndUpdate()
para código mais sucinto; note é que a implementação de getAndUpdate()
recorre na verdade a compareAndSet()
de uma forma que deverá ser muito similar à sua 1ª versão (como pode ver aqui).
O interface Stack<T>
define um TAD para uma stack com as operações:
T pop()
: obtém valor no topo da stack, ou lançaEmptyStackException
se stack estiver vazia;void push(T value)
: adicionavalue
ao topo da stack;int size()
: devolve número de elementos na stack.
Como nos exercícios anteriores são dadas 3 implementações de Stack
:
UStack
: sem qualquer mecanismo de sincronização;LStack
: baseada no uso de locks;AStack
: baseada no uso deAtomicReference
.
Todas as classes empregam uma lista ligada para guardar os elementos da stack,
com nós da lista expressos pela classe Node
, onde a cabeça da lista (caso definida, i.e.,
diferente de null
) contém o elemento no topo da stack.
Para testar as implementações, execute:
cjavac
cjunit pc.stack.AllTests
Faça uma análise análoga a 1.1 e 2.1 para UStack
.
Os testes de LTest
falham devido a um pequeno "bug" no código de LStack
!
Analise o código para perceber qual é o bug, e corrija-o.
Execute de novo LTest
para validar a correcção.
O código de AStack
está incompleto, pois o corpo do método push()
está vazio (e claro, os testes em ATest
falham)!
Defina uma implementação do método, baseado na ideia expressa na implementação de pop()
que emprega
AtomicReference.compareAndSet()
.