"Se você achar os custos de desenvolvimento da arquitetura excessivos, considere quanto a arquitetura errada pode custar"
- Não me lembro exatamente a fonte
Certa vez, "há muito tempo, em uma galáxia distante", comprei o maravilhoso livro de Charles Weatherly, Etudes for Programmers, na introdução em que o autor comprovou a necessidade de estudar exemplos e tarefas educacionais antes de iniciar a programação independente. Eu recomendo fortemente que você encontre este livro, leia o prefácio (e sem parar, leia o resto e resolva os problemas nele contidos), pois não posso comprovar melhor a necessidade de tal prática. Mesmo se você seguir minha recomendação e adquirir muito conhecimento e habilidades práticas ao ler o livro, poderá voltar e ler este post, pois ele é dedicado a várias outras questões. E se você não seguir minhas recomendações, tanto mais você deve ir para baixo do gato.
Há pouco tempo, em um post em que repreendi, expressei minha opinião sobre um RTOS doméstico, mencionei que a implementação do buffer de anel na conhecida biblioteca mcucpp (e em certos aspectos, absolutamente maravilhoso) não pode ser considerada ideal. Vou tentar explicar meu ponto de vista e imaginar a implementação ideal (tanto quanto possível no mundo real). Nota - o texto oferecido a sua atenção fica no "inacabado" por algum tempo e, em seguida, aparece um caso tão conveniente.
Continuamos a desenvolver uma biblioteca para trabalhar com um dispositivo periférico e somos os próximos na fila para gerenciamento de memória e buffer (sim, ainda estamos continuando as operações preparatórias, mas sem elas de forma alguma). De onde vem a necessidade de organização dos buffers e que tipo de animal é esse? O fato é que uma parte significativa da periferia possui velocidade limitada e o processo de transmissão, iniciado de uma maneira ou de outra, leva um certo tempo, e às vezes muito significativo, em comparação com a criação de outra porção de informações para transmissão. Obviamente, antes que esse tempo tenha passado, a próxima transmissão não poderá ser realizada e, portanto, não poderá ser iniciada.
Temos um caso clássico de um par escritor-leitor com velocidades diferentes. É simplesmente impossível resolver esse problema de uma maneira geral, já que “com um excesso arbitrariamente pequeno, mas não zero, do fluxo de solicitações sobre o fluxo de serviço, o tamanho da fila tende ao infinito” e o infinito é fundamentalmente impossível. Mas um caso especial do problema, quando temos explosões locais de solicitações, mas, em média, o fluxo de serviço é capaz de lidar com a carga, uma memória buffer de capacidade suficiente pode ser resolvida. Vamos prestar atenção à frase “capacidade suficiente”; aprenderemos mais tarde como calculá-la, desde que o fato de que isso seja fundamentalmente possível seja suficiente para nós.
Se a memória buffer é um requisito absoluto, é claro que não. Para as informações transmitidas, você pode usar um registro de bloqueio, mas com as informações recebidas é um pouco pior, será necessário adicioná-lo em algum lugar antes do processamento, se você não tomar as medidas apropriadas no protocolo de nível superior (a expressão mágica xon / xoff não nasceu do zero), o que nem sempre é possível e, em qualquer caso, geralmente leva a uma limitação significativa da taxa de transmissão. Há também uma implementação de hardware de buffers internos em dispositivos periféricos (pelo menos para um elemento), mas isso nem sempre é feito e o tamanho do buffer é estritamente limitado a partir de cima.
Portanto, ainda implementaremos o buffer do programa, para o qual seria natural usar o método FIFO (isto é, a fila) para organizar esse buffer, e a fila, por sua vez, é melhor implementada em um buffer circular com dois ponteiros. Quando escrevo “best”, isso não significa que outras implementações (por exemplo, uma fila de referência) sejam impossíveis ou tenham falhas fatais que não sejam fatais. Essa expressão significa apenas que a implementação não será muito complicada e muito eficaz, embora outros possam ter vantagens inegáveis sobre ela, pelas quais terão que pagar por algo, porque DarZaNeBy.
Como é altamente improvável que o seu modelo MK tenha uma implementação de hardware de um dispositivo de uso geral (módulos periféricos individuais podem ter seus próprios buffers de anel, mas eles não têm nada a ver com o tópico desta postagem), teremos que criar um buffer de anel na memória linear (implemente no vetor, esse é, em geral, o único objeto natural na memória endereçável) e, para isso, será necessário um índice de buffer (ou talvez até dois índices, mas mais sobre isso posteriormente). Na minha opinião, um buffer circular com dois ponteiros (índices) é a única maneira aceitável de implementar uma fila em um vetor, mas existem pontos de vista diferentes sobre esse problema e vi com meus próprios olhos uma implementação no estilo “x1 = x2; x2 = x3; ... x8 = novo símbolo ", se preferir, não considerarei tão exótico. O fato de que o fragmento dado possa ter o direito de existir em uma situação específica e muito limitada não o torna aceitável em geral.
Consideraremos a implementação correta do módulo de programa para organizar o ponteiro e, para começar, preste atenção à primeira palavra na definição. A diferença entre um código correto e um errado não é apenas porque o código correto não contém erros, embora esse seja um requisito absoluto. Mesmo o código que executa totalmente suas funções pode estar incorreto se for incompreensível ou se houver uma opção que não seja menos clara, mas que seja executada mais rapidamente ou que seja executada com a mesma rapidez, mas com mais clareza, para que o conceito de correção seja um tanto relativo. Continuamos nossa consideração de nosso exemplo de implementação de buffer, o que nos permitirá demonstrar a diferença entre diferentes graus de correção.
Antes de voltar à essência, um ponto importante sobre a discussão adicional. Quero dizer que seu compilador está sempre ativado em um nível de otimização diferente de zero (-O2), portanto, não precisamos pensar em pequenas melhorias, como 1) modificação de prefixo versus postfix, ou 2) usar os resultados da operação anterior ou 3) a diferença entre incremento e adição unidades e assim por diante - assumimos que o compilador fará muito por nós. Obviamente, essa não é uma suposição estrita, mas, caso contrário, teremos que mergulhar nas entranhas da montadora, que em nosso tempo não é a corrente principal.
Deixe-me lembrá-lo de que fomos instruídos a implementar o índice (ponteiro) do buffer de anel, ou seja, precisamos criar o comportamento de uma variável que
executa sequencialmente uma série de valores, de alguns iniciais a outros finais . Suponha imediatamente que o valor inicial será zero; caso contrário, teremos que escrever imediatamente um código mais ou menos correto, e isso é contrário aos objetivos educacionais e não temos pressa, e o final é Max.
Esse comportamento da variável pode ser implementado usando a seguinte construção:
volatile int Counter = 0; Counter = (++Counter) % (Max+1);
e é precisamente esse código que podemos ver em muitos casos (isto é, com muita frequência). O que há de errado - bem, primeiro, por algum tempo (da execução da operação de incremento à atribuição do resultado), nossa variável será maior que o valor máximo permitido e, se nesse momento ocorrer uma interrupção que precise levar em consideração o valor dessa variável, eu pessoalmente prevejo Eu não presumo os resultados. Portanto, reescrevemos o programa:
int Counter=0; Counter = (Counter + 1) % (Max + 1);
Nós eliminamos um erro, e o código (a seguir, quero dizer que o código "executável" significa o código executável gerado pelo compilador) não se tornou mais longo e não é mais executado (na verdade, é executado mais rapidamente, mas apenas porque na primeira versão a palavra volátil é usada completamente redundante nesse caso) e não se tornou menos clara (e até mais clara, mas é uma questão de gosto).
Nota necessária sobre volátil - essa diretiva é necessária se queremos evitar a otimização de código que leva à execução incorreta e, neste caso específico (quando o valor da variável não muda fora do escopo do módulo e não há entradas sequenciais), ela (diretiva ) completamente redundante. Eu recomendo fortemente que você observe o código gerado para as duas opções no godbolt.org. Por que você não deve abusar da diretiva volátil, diferentemente da palavra-chave estática, que é recomendada para uso sempre que possível. Bem, em primeiro lugar, proibimos a otimização, ou seja, o código definitivamente não se tornará mais rápido (provavelmente, ele será maior e mais lento, mas preferimos formulações estritas). E segundo, neste caso em particular, essa palavra é enganosa, pois em relação ao nosso programa o valor do contador não pode mudar de maneira alguma fora do nosso controle. Em um programa que lê seu valor - ou seja, na implementação do próprio buffer de anel, você pode considerar o contador mutável fora do módulo, e é questionável; portanto, esse atributo simplesmente não é aplicável ao contador. Se uma variável deve ser interpretada de maneira diferente em módulos diferentes, nossos serviços devem ser combinados; se estamos falando de organizar uma seção crítica, por exemplo, ao implementar uma transação ou operações atômicas, essa diretiva não fornece nada.
Voltamos ao código e vemos que o programa ainda está errado - qual é o problema - e o fato é que ele não faz o que precisamos (veja a descrição da tarefa), mas outra coisa (calcula o restante da divisão), apenas os resultados combinar. Bem, achamos que sim (acho que não, mas os autores do código certamente), que os resultados coincidem; de fato, no caso geral, eles não coincidem, tivemos sorte com o intervalo da variável (valores positivos). Além disso, o processo de execução do código é mais longo do que poderia ser feito, uma vez que, na melhor das hipóteses, temos a operação de divisão inteira (se faz parte dos comandos de nossa arquitetura) e não é realizada de forma alguma em um ciclo do processador (um valor característico de 10 ciclos). para arquitetura de 8 bits) e, na pior das hipóteses, veremos o procedimento de divisão chamado da biblioteca padrão (e bem, se divisão curta), o tempo de execução será dezenas de ciclos de clock.
Então, por que uma abordagem completamente errada ainda é possível de ser encontrada com muita frequência. Aqui, da platéia, eles me dizem que, com o valor de Max + 1, que é uma potência de dois, o compilador adivinhará, em vez da operação de divisão, colocar a operação de multiplicação bit a bit na máscara correspondente (igual a Max), que será executada muito rapidamente e tudo ficará bem.
Eu concordaria com esta afirmação e adotaria essa abordagem, se não pelas seguintes circunstâncias:
- isso só é possível para Mach definido estaticamente na fase de compilação,
- isso só acontece quando a otimização está ativada,
- isso só acontece quando Mach atende a essa condição,
- isso não ocorre para todos os tipos de cardeais.
Além disso, é neste caso particular (quando a variável é definida como um sinal), além do comando de multiplicar (lógico) pela máscara, um comando de comparação com zero e um ramo para valores negativos será gerado, e embora esse ramo nunca seja do nosso alcance será executado, ocupará espaço na memória (e, no caso de uma função substituível, levará várias vezes) e levará tempo para executar a operação de comparação, se você não acreditar, novamente vamos ao site especificado e veremos por si mesmo. Outro argumento a favor dos cardeais não assinados, aos quais dediquei recentemente um post inteiro.
Portanto, se queremos usar a multiplicação lógica com uma máscara (obtida otimizando o cálculo do restante), devemos reescrever o módulo de acordo:
typedef uint8_t Counter_t; typedef int8_t sCounter_t; static inline Counter_t NextCounter(const Counter_t Counter) { #if IS_POWER2(Max + 1) return (Counter + 1) & Max #else return (Counter + 1) % (Max + 1); #endif };
Nesta versão, tudo é perfeitamente claro e controlável e tudo é verdade (embora várias deficiências tenham permanecido, mas agora são óbvias e não mascaradas), portanto, está correto, embora esteja mais correto e agora vamos procurá-las. A principal desvantagem, na minha opinião, é uma violação do princípio KISS, uma vez que o uso da operação restante por divisão negligencia completamente esse princípio. Portanto, agora destruiremos todas as deficiências de uma só vez (não se preocupe com o destino delas, elas renascerão 100.500 vezes, porque nem todos os programadores do Arduino leem minhas postagens).
Mas primeiro, um ligeiro desvio para o lado. Como podemos implementar uma verificação da potência de dois (um número binário pode ser representado como {0} 1 {0}) que acabamos de usar
não espie#define IS_POWER2 (N) (((((N) - 1) & (N)) == 0)
E como podemos implementar a verificação de que um número é uma sequência correta de unidades {0} 1 {1} em notação binária - uma opção é óbvia
#define IsRightSequence(N) IsPower2 ((N) + 1)
e o segundo é trivial
#define IsRightSequence(N) ( (((N) + 1) & (N)) == 0)
Nota: não consigo deixar de lembrar o magnífico teorema: "Um número transcendental em um grau transcendental é sempre transcendental, a menos que o inverso seja óbvio ou trivial".
E como podemos verificar se um número é uma sequência de unidades {0} 1 {1} {0}
#define IsSequence(N) IsPower2( (N) ^ ((N) << 1))
E finalmente - como selecionar o número menos significativo (não sei por que isso pode ser necessário, mas será útil)
#define LowerBit(N) ((((N) - 1) ^ (N)) & (N)).
Mas ele veio com o que pode ser útil
#define IsRightSequence(N) (IsSequence(N) && (LowerBit(N) == 1))
Uma observação curiosa - essas macros não são muito corretas; verifica-se que 0 é uma potência de dois e uma sequência correta (é claro, também é uma sequência), o que é um pouco estranho. Mas 1 é todos esses objetos com toda a razão; portanto, parece que zero precisa ser considerado separadamente. Outra propriedade interessante dessas macros é que não fazemos suposições sobre o tamanho do argumento, ou seja, elas funcionam corretamente com qualquer tipo de cardeal.
Há um livro maravilhoso, "Truques para programadores", onde você pode encontrar as macros mencionadas e muitas outras tarefas igualmente divertidas e instrutivas, recomendo a leitura, especialmente porque não há muitas letras nele.
Mas voltando ao nosso índice de buffer de anel. Demos a solução certa, mas prometemos ainda mais corretamente, o que significa que nossa última solução tem falhas (quem duvidaria). Um deles - o tamanho do buffer deve ser estaticamente determinado no estágio de compilação, o segundo - no caso de um comprimento sem êxito, o tempo de execução é muito longo e ainda há um certo número de erros em um pedaço relativamente pequeno do programa, o que nos faz lembrar de uma piada sobre 4 erros ao escrever a palavra "mais". Eliminaremos todos eles (alguns serão deixados para mais tarde) e imediatamente, para os quais, finalmente, escreveremos a solução para o problema original como ele é:
static inline Counter_t NextCounter(const Counter_t Counter) { if ((Counter + 1) > Max) { return 0; } else { return Counter + 1; }; };
(Como você já entendeu, sou um defensor dos colchetes egípcios e não há nada a ser feito sobre isso).
Vamos prestar atenção ao fato de que simplesmente reescrevemos a condição do problema a partir de uma linguagem natural na linguagem de programação escolhida, para que se torne extremamente claro e compreensível. É possível melhorá-lo - sem dúvida, mas apenas do ponto de vista da velocidade do código, já que simplesmente não há outras deficiências para esta solução (não existem deficiências óbvias, na verdade elas existem e as eliminaremos com êxito).
Vamos avaliar a complexidade computacional dessa solução - adição com unidade (1) e comparação (2) sempre, atribuindo zero (1) (raramente) ou adicionando (1) (quase sempre) - o que fornece 1 + 2 + 1 + Δ ~ 4 elementar operações e memória zero. É possível que um bom compilador no modo correto faça certas otimizações e reduza o tempo de execução do código, mas é melhor fazê-lo explicitamente. Aqui está a seguinte opção:
static inline Counter_t NextCouner(const Counter_t Counter) { register sCounter_t Tmp; Tmp = (Counter + 1); if (Tmp > Max) { Tmp = 0; }; return Tmp; };
Avaliamos a complexidade - adição e comparação sempre, atribuindo zero (raramente) - aproximadamente 3 operações e um elemento de memória. De fato, a versão anterior também tinha um elemento de memória (implícito), portanto, temos um ganho líquido em uma operação elementar. Além disso, a versão anterior apresentava mais duas desvantagens - 1) violava o princípio DRY (calculava o aumento em uma vez duas vezes) e 2) apresentava mais de um ponto de saída, o que não é bom. Também não perdemos o entendimento, ou seja, conseguimos matar um bando de coelhos com um tiro e também não gastamos cartuchos - é apenas uma história no estilo do Barão Munchausen.
Observe que eu não usei a construção
if ( (Tmp = Counter + 1) > Max)
, embora contenha uma instrução explícita para o compilador para tentar não fazer transferências redundantes. Isso é saboroso da forma mais flagrante, simplesmente não gosto do valor retornado pelo operador de atribuição e tento evitar usá-lo. Não sei explicar o motivo desse forte sentimento, segundo Freud, que é provavelmente um trauma psicológico na infância. Compiladores modernos são capazes de realizar otimizações simples por conta própria e, além disso, também adicionei um qualificador de registro, para que o código da minha versão e o correto (do ponto de vista da linguagem C) sejam compatíveis. Não obstante, não limitei sua liberdade de usar o método que lhe parece preferível.
Continuamos a melhorar, porque não há limite para a perfeição, e ainda não o alcançamos. Para alcançá-lo, reformulamos um pouco o problema original e deixamos apenas o requisito da variável no intervalo de valores, sem indicar a direção da mudança. Essa abordagem permite reescrever o programa da seguinte maneira
static inline Counter_t NextCouner(const Counter_t Counter) { register Counter_t Tmp; Tmp = (Counter - 1); if (Tmp < 0) { Tmp = ; }; return Tmp; };
À primeira vista, nada mudou muito, mas, no entanto, obtemos um ganho de tempo. Obviamente, não devido ao fato de que a operação de diminuir por um funciona mais rápido do que a operação de aumentar por ele (embora eu tenha ouvido uma versão semelhante), mas devido às peculiaridades da comparação. Se nas versões anteriores eu contei a comparação como duas operações elementares (primeiro subtraímos e depois tomamos uma decisão), nesse caso o resultado da operação anterior é usado para tomar uma decisão diretamente e a comparação leva uma operação elementar, o que leva a duas operações sempre e uma atribuição (raramente) e salvamos uma operação (sem perder nada), como diz o ditado, "um pouco, mas legal". A solução resultante é ideal - infelizmente não. É um pouco inferior à solução com uma máscara (que requer exatamente 2 operações elementares) em termos de velocidade e talvez essa seja sua única desvantagem.
Existe uma solução ainda mais rápida - basta aumentar (diminuir) o valor do contador e não fazer mais nada, mas isso só é possível no único caso em que o valor máximo coincide com o valor mais representativo no tipo aceito. Para um contador de 8 bits (ou seja, do tipo uint8_t), será 255, basta escrevermos Counter = Counter + 1 e acreditarmos que escrever Counter + = 1 ou ++ Counter é completamente opcional, embora muitos sejam e eles escreverão e estarão absolutamente certos. Se não considerarmos seriamente a versão sobre a necessidade de salvar caracteres (já que a primeira opção é a mais longa), isso não faz sentido, pelo menos se estivermos escrevendo um programa para a arquitetura ARM ou AVR (para outras que eu não verifiquei, suspeito que o resultado será o mesmo) no compilador GCC (o autor entende que está escrevendo um programa no editor do ambiente de programação integrado, isso é apenas uma revolução de fala do passado quando os computadores eram grandes e a memória pequena) e com a otimização ativada em qualquer nível, porque o código fornecido será absolutamente idêntico.
Os compiladores modernos são muito, muito avançados em termos de otimização e geram um código realmente muito bom, é claro, se você tiver ativado o modo correspondente. Embora eu esteja pronto para concordar que essas construções de linguagem não causam danos e podem ser úteis sob certas condições, a única coisa que observo é que as expressões do Counter ++ (nesse caso em particular, é claro) devem ser evitadas sem ambiguidade, pois são destinadas a situações completamente diferentes e podem dar origem a código mais lento, embora opcional.
Outra questão é que um buffer de 256 elementos nem sempre é aceitável, mas se você tiver memória suficiente, por que não? Com esta implementação, se você pode alinhar o buffer com a borda da página, o acesso aos elementos pode ser feito muito rapidamente, eliminando a operação de passar de índice para índice (a palavra-chave union informará a implementação de um recurso desse tipo, não o trarei para não aprender ruim), mas essa já é uma decisão muito, muito específica, com forte apego à arquitetura, que é perigosamente próximo de truques no pior sentido da palavra, e esse não é o nosso estilo.
Obviamente, ninguém nos proíbe escrever um invólucro que chamará este ou aquele método, dependendo do valor dos valores máximos (e mínimos, já que muitos métodos simplesmente não funcionam com valores mínimos diferentes de zero), eu já propus os princípios básicos de uma solução, portanto vamos oferecer isso como um exercício.
Bem, em conclusão, para resumir - reuniremos diferentes implementações de trabalho com um índice de anel e avaliaremos suas propriedades.
A segunda linha entre parênteses mostra o número de valores de tamanho do buffer (não excedendo 256) para os quais essa implementação está disponível, mas queremos dizer que um buffer de tamanho 0 não nos interessa.
Como você pode ver nesta tabela, DarZaNeBy (minha expressão favorita, como você deve ter notado) e vantagens são compradas à custa de desvantagens, a única coisa que pode ser declarada inequivocamente é que o incremento na verificação tem um concorrente mais bem-sucedido na forma de decréscimo na verificação e não passa para a próxima rodada sob nenhuma circunstância.
Uma observação necessária - existem linguagens de programação nas quais não teríamos que pensar na implementação do índice, mas simplesmente poderíamos usar o tipo de intervalo. Infelizmente, não posso chamar a implementação dessas construções no código como ideal, pois essas construções (e essas linguagens) não se destinam à otimização em tempo de execução, mas é uma pena.
Então, criamos o módulo certo (que nome forte para a função embutida) funcionava com o índice e agora estamos prontos para começar a implementar o próprio buffer de anel.
E para iniciantes, devemos decidir o que exatamente queremos desse objeto de programa. É absolutamente necessário poder colocar um elemento de dados em um buffer e extraí-lo - dois métodos principais, um tipo de getter e setter. É teoricamente possível imaginar um buffer sem um desses métodos, ou mesmo sem os dois (pouco se pode imaginar puramente teoricamente), mas o valor prático dessa implementação é uma grande questão. A próxima funcionalidade necessária - verificação de informações - pode ser implementada como um método separado ou como um valor (ou atributo) especial retornado pela leitura. Geralmente eles preferem o primeiro método, pois é mais compreensível e não muito caro.
Mas verificar a integridade do buffer já é uma grande questão - essa operação exigirá tempo adicional, que sempre será gasto na gravação, embora ninguém nos obriga a usá-lo - que assim seja. Não precisamos de mais nada do buffer, vamos lembrar esta frase para o futuro.
Voltar para a implementação. Precisamos de um local para armazenar os elementos da fila e dois índices - um para gravar no buffer e outro para ler a partir dele. Como exatamente conseguiremos esse lugar (e esses indicadores) é um tópico para uma discussão separada, por enquanto, vamos deixar esse momento entre parênteses e acreditar que simplesmente os temos. Alguns (incluindo os autores do livro "Programação para matemáticos", que eu respeito, recomendo a leitura) também usam o contador de locais preenchidos, mas não faremos isso e tentarei mostrar por que isso é ruim.
Primeiro, sobre os índices - notamos imediatamente que esses são índices, não indicadores, embora às vezes eu me permitisse ser assim. ( ), ( )- , , , . ( 256 ), , , ( , 8 , , 4- ), , , ( , ).
, 51 ( ) 2 ( ) 3 ( ), , , . , , GCC x51, AVR .
, , . , ( , , ), — .
— ( ), . : 1) , 2) , . , , , . , , . ( ) , — . — , .
(«, ») , . — 1) , 2) ( , ) 3) , , 4) 256 , 5) ( ), . , , , , .
, (, , ), 1 . :
#define NeedOverflowControl YES typedef uint8_t Data_t; static Data_t BufferData[Max]; static Counter_t BufferWriteCounter=0, BufferReadCounter=BufferWriteCounter; void BufferWrite(const data_t Data) { BufferData[BuffWriteCounter] = Data; register counter_t Tmp = NextCount(BufferWriteCounter); #if (NeedOverflowControl == YES) if (Tmp == BufferReadCounter) {BufferOverflow();} else #endif { BufferWriteCounter = Tmp; } };
, , … , :
inline int BufferIsEmpty(void) { return ( BufferReadCounter == BufferWriteCounter ); }; inline int BufferIsFull(void) { return ( BufferReadCounter == NextCounter(BufferWriteCounter) ); }; #define DataSizeIsSmaller (sizeof(data_t) < sizeof(counter_t)) data_t BufferRead(void) { #if DataSizeIsSmaller register data_t Tmp = BufferData[BufferReadCounter]; #else register counter_t Tmp = BufferReadCounter; #endif BufferReadCounter = NextCount(BufferReadCounter); #if DataSizeIsSmaller return Tmp; #else return BufferData[Tmp]; #endif };
, ( ) — , , , — , . , , , .
, , — , , , .
, ( )
1) ( — , , — , , ).
(, , )
2) — , .
:
3) 4) , (« »). — , , ( N N+1 ) , ?
3) ,
4) .
— « », - , — . 3, ( ), , .
— , ( , ),
5) — , , , , — , .
— , , .
, , , , , , , , , , , . , 4 , , . MRSW (Multi-Reader Single-Writer) «The Art of Mulpiprocessor Programming» ( , ) ( ) . — , , .
MRMW , «» (, , « » ). , , , , . , , , (, , — , , , ), .
, ( ) , . , , , , , , , .
typedef uint8_t data_t; static data_t BufferData[Max]; static counter_t BufferWriteCounter=0, BufferReadCounter=WriteCounter; static int8_t BufferHaveData = 0; void BufferWrite(const data_t Data) { if ((BufferWriteCounter == BufferReadCounter) && (BufferHaveDataFlag == 1)) {BufferOverflow();} else { BufferData[BufferWriteCounter] = Data; BufferHaveDataFlag = 1; BufferWriteCounter = NextCounter(BufferWriteCounter); }; }; inline int BufferIsEmpty(void) { return ((BufferReadCounter==BufferWriteCounter) && (BufferHaveDataFlag == 0));}; data_t BufferRead(void) { register counter_t Tmp; Tmp = BufferReadCounter; BufferReadCounter = NextCount(BufferReadCounter); if (BufferReadCount == BufferWriteCounter) { BufferHaveDataFlag = 1; }; return BufferData[Tmp]; };
, , , , , .
, ( 0 1, , , ), , , , , , ( ), , ,
- typedef (NoBufferHaveData= 0, BufferHaveData =1) BufferHave DataFlag_t; BufferHaveData_t BufferYaveDataFlag; inline void BufferHaveDataFlagSet(void) {BufferHaveDataFlag = NoBufferHaveData;}; inline void BufferHaveDataFlagClr(void) {BufferHaveDataFlag = BufferHaveData;}; inline int BufferHaveDataFlagIsSet(void) {return (int)(BufferHaveDataFlag == BufferHaveData);};
, , 0 1, . , , , 0 1. , , , , BufferFullFlag , BufferIsNotEmptyFlag . , KISS , , , , , « ».
, , .
, , .
PS , , :
- — (, , ), — , , , , .
- .
- .
- 2 .
- , ( ) , , .
- ,
return ((_writeCount - Atomic::Fetch(&_readCount)) & (size_type)~(_mask)) != 0;
— , , ,
size_type(~(_mask))
.
PPS , .