Multithreading no LabWindows™/CVI

Visão geral

Apesar de serem usados freqüentemente como sinônimos, os termos multitask, multithread e multiprocessing se referem a conceitos totalmente distintos. O Multitask (Multi-tarefas) se refere à capacidade de um Sistema Operacional (SO) alternar entre tarefas rapidamente, dando a impressão de execuções simultâneas dessas tarefas. Quando executadas em um sistema multitask preemptivo (agendamento do SO baseado em prioridades), as aplicações podem ser suspensas a qualquer momento. Com o Multithreading, as aplicações podem separar suas próprias tarefas em threads individuais. Em um programa multithread, o SO direciona cada thread para executar um código por um período de tempo (time slice ou fatia de tempo), antes de mudar a execução para outra thread. A ação de parar a execução de uma thread e começar a execução de outra é referida como troca de thread (thread switch). O SO tipicamente pode realizar trocas de threads rápidas o suficiente para dar a impressão de execução simultânea de mais de uma thread ao mesmo tempo. O Multiprocessing se refere à utilização de múltiplos processadores em um único computador. Em um sistema de multiprocessamento simétrico (SMP – symmetric multiprocessing), o SO utiliza automaticamente todos os processadores no computador para executar qualquer thread que esteja pronta para ser executada. Com o poder de multiprocessamento, suas aplicações multithread podem executar múltiplos threads simultaneamente, finalizando mais tarefas em menos tempo. As aplicações Single-thread podem demonstrar baixo desempenho quando executadas em um equipamento com múltiplos processadores. Elas executam em qualquer um dos processadores, mas nunca em todos os processadores ao mesmo tempo, como as aplicações multithread fazem. As aplicações Single-thread podem experimentar até um desempenho inverso através da sobrecarga do SO ao trocar a aplicação de um processador para outro. Para alcançar o máximo desempenho em Sistemas Operacionais multithread e/ou de equipamentos com multiprocessamento, a aplicação também deve ser multithread.

Conteúdo

Razões para o Multithreading

Existem quatro razões principais para que você possa ter mais de uma thread em seu programa. A razão mais comum é a de separar múltiplas tarefas, uma ou mais das tarefas que são de tempo crítico e podem estar sujeitas a interferências devido à execução de outras tarefas. Por exemplo, um programa que realiza aquisição de dados e os mostra em uma interface de usuário é um ótimo candidato para multithreading. Nesse tipo de programa, a aquisição de dados é uma tarefa de tempo crítico que pode estar sujeita às interferências da interface de usuário. Enquanto for utilizada uma abordagem single-thread no programa LabWindows/CVI, você pode decidir por tirar os dados a partir do buffer de aquisição de dados, colocar os dados em uma interface de usuário gráfica e então processar eventos para permitir que a interface de usuário seja atualizada. Se o usuário escolher acionar a interface (por exemplo, arrastar o cursor no gráfico), a thread continua a processar os eventos da interface de usuário e não retorna para a tarefa de aquisição de dados ocasionando estouro no buffer de aquisição de dados. Usando uma abordagem multithreading no programa LabWindows/CVI, você pode colocar a operação de aquisição de dados em uma thread e a interface de usuário em outra thread. Desta forma, enquanto o usuário estiver operando a interface de usuário, o SO realiza trocas de threads reservando tempo para que a thread de aquisição de dados seja executada.

A segunda razão para você fazer o seu programa multithread é realizar operações simultâneas de entrada/saída lentas. Por exemplo, um programa que usa um instrumento para testar placas circuito pode ser beneficiado significativamente pelo multithread. Usando uma abordagem single-thread no programa LabWindows/CVI, você pode enviar dados para uma porta serial para instruir a placa de circuito a inicializar. Você espera a placa completar essa operação antes de inicializar o teste com o instrumento. Você deve esperar pelo teste do instrumento antes de capturar a medição. Com uma abordagem multithreading no programa LabWindows/CVI, você pode usar outra thread para inicializar o teste com o instrumento. Desta forma, você espera pela inicialização do instrumento, enquanto aguarda a inicialização da placa de circuito. As operações de entrada/saída lentas são feitas simultaneamente, reduzindo assim o tempo total de espera.

A terceira razão para você fazer o seu programa multithread é obter ganhos de desempenho em equipamentos multicore (computadores dotados de múltiplos processadores ou núcleos). Cada processador deste equipamento pode executar uma thread. Portanto, enquanto o SO dá a aparência de execução conjunta de múltiplas threads em um equipamento com um único processador, o SO na verdade só executa múltiplas threads ao mesmo tempo em um equipamento multicore. Um programa que seria beneficiado pelo multithreading em um equipamento com multiprocessamento é aquele que realiza mais de uma tarefa simultaneamente. Por exemplo, um programa que faz aquisição de dados, gravação em disco, análise dos dados e apresentação dos resultados em uma interface de usuário, provavelmente seria beneficiado pela existência de múltiplas threads e pela execução em um equipamento com múltiplos núcleos. Gravar os dados em um disco e a análise para exibição são tarefas que você pode realizar simultaneamente.

A quarta razão para você fazer o seu programa utilizando multithreading é a de realizar uma determinada tarefa em mais de um contexto ao mesmo tempo. Por exemplo, você pode usar o multithreading em uma aplicação que executa testes em estações de teste paralelas. Usando uma abordagem single-thread, a aplicação terá que alocar dinamicamente espaço para gravar os resultados do teste de cada estação. O programa deve manter a associação entre cada gravação e sua estação de teste manualmente. Usando uma abordagem multithreading, a aplicação pode criar uma thread separada para administrar cada estação de teste. A aplicação pode então usar variáveis thread-local para criar os resultados “por-thread”. A associação entre a estação de teste e esses resultados é mantida automaticamente, simplificando assim o código da aplicação.

Escolhendo o Seu SO

O Sistema Operacional Microsoft Windows 9x não trabalha com equipamentos multicore. Portanto, você deverá utilizar o Windows Vista/XP/2000/NT 4.0 em um equipamento multicore para aproveitar os benefícios dos múltiplos processadores. No entanto, mesmo em equipamentos com um único processador, programas multithread têm melhores resultados quando são executados em Windows Vista/XP/2000/NT 4.0 do que quando executados em Windows 9x. Isso é verdade porque a troca das threads é mais eficiente no Windows Vista/XP/2000/NT 4.0. Entretanto, essa diferença no desempenho geralmente não é perceptível na maioria dos programas multithread.

Os SOs Windows Vista/XP/2000/NT 4.0 são mais estáveis do que o Windows 9x para o desenvolvimento de programas, especialmente quando se está escrevendo e depurando aplicações multithread. Todas as vezes que você suspender ou encerrar uma thread que está executando um código do SO, existe uma chance de você deixar alguma parte do SO em mal estado. É muito mais comum que essa situação cause o travamento do equipamento quando executando um SO Windows 9x do que quando operado por um SO Windows Vista/XP/2000/ NT 4.0. Por essa razão, a National Instruments recomenda que você use um equipamento executando Windows Vista/XP/2000/NT 4.0 para o desenvolvimento de aplicações multithread.

Introdução ao Multithreading no LabWindows/CVI

O software NI LabWindows/CVI desde  meados dos anos 90 é compatível com aplicações multithread. Agora, com a grande oferta de CPUs multicore, você pode usar o LabWindows/CVI para aproveitar plenamente as capacidades da tecnologia multithreading.

A Biblioteca Multithreading do LabWindows/CVI fornece as seguintes otimizações de múltiplo desempenho sobre o padrão Windows SDK threading APIs:

  • Thread pools - auxilia no agendamento de funções para execução em diferentes threads. Esse recurso permite maior controle das threads, minimizando a sobrecarga associada à criação e a destruição de threads.
  • Thread-safe queues - abstrai a transferência de dados entre threads. Uma thread pode ler de uma queue (fila), ao mesmo tempo em que outra thread escreve na queue.
  • Thread-safe variables - combina de maneira eficaz uma seção crítica e um tipo de dado arbitrário. Você pode chamar uma única função para adquirir a seção crítica, configurar o valor da variável e liberar a seção.
  • Thread locks – simplificam a utilização de uma seção crítica ou mutex (mutuamente exclusivo) para providenciar uma API consistente e para automaticamente escolher o mecanismo apropriado, quando necessário. Por exemplo, o LabWindows/CVI automaticamente usa um mutex, se o recurso precisar ser compartilhado entre processos ou se a thread precisar processar mensagens enquanto estiver esperando pelo recurso. A seção critica é usada em outros casos porque é mais eficiente.
  • Thread-local variables - fornece instâncias de variáveis para threads. O sistema operacional coloca um limite no número de thread-local variables disponível para cada processo. A implementação do LabWindows/CVI consolida as thread-local variables, usando apenas um processo de thread-local variables para todas as thread-local variables no seu programa.

Você pode encontrar todas as funções multithreading da Biblioteca do LabWindows/CVI através  do menu Library»Utility»Multithreading.

Execução de Código em Threads Secundárias no LabWindows/CVI

A Thread em um programa single-thread é referida como sendo a thread principal. O SO cria a thread principal quando o usuário diz ao SO que inicie a execução de um determinado programa. Em um programa multithread, o programa informa ao SO que crie mais threads além da thread principal. Essas threads se referem às threads secundárias. A principal diferença entre a thread principal e a thread secundária é onde cada tipo de thread começa a execução. O SO começa a execução da thread principal nas funções main ou WinMain. O desenvolvedor especifica as funções nas quais cada thread secundária iniciará sua execução.

Em um típico programa multithread no LabWindows/CVI, você usa a thread principal para criar, mostrar e executar a interface de usuário. Você usa as threads secundárias para realizar outras operações de tempo crítico tal como a aquisição de dados. O LabWindows/CVI fornece dois mecanismos de alto nível para executar códigos em threads secundárias. Esses mecanismos são os recursos thread pools e os temporizadores assíncronos. O recurso thread pool é apropriado para tarefas que você precisa realizar um número determinado de vezes ou tarefas em que você precisa realizar em um loop (estrutura de repetição). O temporizador assíncrono é apropriado para tarefas em que você precisa realizar intervalos regulares.

Usando o Recurso Thread Pool

Para executar um código em uma thread secundária usando o recurso thread pool do LabWindows/CVI, utilize a função CmtScheduleThreadPoolFunction na Biblioteca de Utilidades. Passe o nome da função que você quer executar em uma thread secundária. O thread pool agenda sua função para ser executada em uma de suas threads. Dependendo da configuração e do estado atual do thread pool, o pool cria uma nova thread na qual para executar sua função, utiliza uma thread inativa já existente, ou espera até que uma thread ativa torne-se inativa e use essa thread para executar a função que você agendou.

A função que você passa para CmtScheduleThreadPoolFunction é referida como uma função thread. As funções thread do thread pool podem ter qualquer nome, mas devem seguir o seguinte modelo:

 int CVICALLBACK ThreadFunction (void *functionData);

O código a seguir mostra como você usa CmtScheduleThreadPoolFunction para executar em uma thread secundária uma função thread que realiza aquisição de dados.

int CVICALLBACK DataAcqThreadFunction (void *functionData);
int main(int argc, char *argv[])
{
    int panelHandle;
    int functionId;
   
    if (InitCVIRTE (0, argv, 0) == 0)
        return -1; /* out of memory */
    if ((panelHandle = LoadPanel(0, "DAQDisplay.uir", PANEL)) < 0)
        return -1;
    DisplayPanel (panelHandle);

    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, DataAcqThreadFunction, NULL, &functionId);
    RunUserInterface ();
    DiscardPanel (panelHandle);
    CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
    return 0;
}
int CVICALLBACK DataAcqThreadFunction (void *functionData)
{
    while (!quit) {
        Acquire(. . .);
        Analyze(. . .);
    }
    return 0;
}

No código anterior, a função main chama oCmtScheduleThreadPoolFunction fazendo com que o thread pool crie uma nova thread para executar a função thread DataAcqThreadFunction. A thread principal retorna a partir da CmtScheduleThreadPoolFunction sem esperar pelo término da função DataAcqThreadFunction. A função DataAcqThreadFunction é executada na thread secundária ao mesmo tempo que a thread principal executa as chamadas na função main.

O primeiro parâmetro para CmtScheduleThreadPoolFunction indica o thread pool com o qual você deseja agendar a função. A Biblioteca de Utilidades do LabWindows/CVI contém um thread pool padrão embutido. Você passa a constante DEFAULT_THREAD_POOL_HANDLE para indicar que você quer usar o thread pool padrão. Você não pode alterar o comportamento do thread pool padrão. Você pode chamar CmtNewThreadPool para criar um thread pool personalizado. CmtNewThreadPool retorna um thread pool handle que você passa no primeiro parâmetro para CmtScheduleThreadPoolFunction. Você tem que chamar CmtDiscardThreadPool para liberar os recursos de um thread pool que você criou com CmtNewThreadPool.

O último parâmetro para CmtScheduleThreadPoolFunction retorna um identificador que você usa para relacionar a função marcada na chamada da função subseqüente. A chamada para CmtWaitForThreadPoolFunctionCompletion faz com que a thread principal espere até que a função thread pool tenha terminado a execução. Se a thread principal terminar antes que as threads secundárias terminarem a execução, as threads secundárias podem não ter a chance para eliminar adequadamente os recursos que eles alocaram. Qualquer biblioteca usada pelas threads secundárias também podem não ter a oportunidade de realizarem a eliminação adequada dos dados.

Usando um Temporizador Assíncrono

Para executar um código em uma thread secundária usando o temporizador assíncrono do LabWindows/CVI, chame a função Toolslib NewAsyncTimer. Passe o nome da função que você quer executar na thread secundária e o intervalo de tempo entre as execuções da função. A função que você passou para NewAsyncTimer se refere ao callback do temporizador assíncrono. O Temporizador assíncrono para drive de instrumentos chama as funções de callback do temporizador assíncrono em intervalos que você especifica. O callback do temporizador assíncrono pode ter qualquer nome, mas devem ter o seguinte modelo:

 int CVICALLBACK FunctionName (int reserved, int timerId, int event, void *callbackData, int eventData1, int eventData2);

Pelo fato do temporizador assíncrono para driver de instrumentos do LabWindows/CVI usar o temporizador multimídia do Windows para implementar as chamadas de retorno do temporizador assíncrono, o intervalo mínimo válido que você pode especificar é diferente para diferentes equipamentos. Se você especifica um intervalo que é menor do que a resolução máxima disponível no seu sistema, um funcionamento não esperado pode ocorrer. O funcionamento não esperado, tipicamente ocorre quando você configura o intervalo do temporizador para menos do que 10 ms. Também, o temporizador assíncrono para driver de instrumentos usa uma thread do temporizador multimedia para executar todas as chamadas de retorno dos temporizadores assíncronos que você registra em um único programa. Deste modo, se você quer que seu programa execute múltiplas funções em paralelo, a National Instruments recomenda que você use as funções thread pool da Biblioteca de Utilidades do LabWindows/CVI em vez das funções do temporizador assíncrono.

Protegendo Dados

Um ponto crítico que você deve atentar quando está usando threads secundárias é a proteção dos dados. Você tem que proteger as variáveis globais, as variáveis locais estáticas e as variáveis alocadas dinamicamente vindas de acessos simultâneos por múltiplos threads. Não fazer isso pode ocasionar erros lógicos intermitentes que são difíceis de se encontrar. O LabWindows/CVI fornece vários mecanismos de alto nível que ajudam você a proteger seus dados de acessos simultâneos. Uma consideração importante quando se está protegendo dados é evitar deadlocks.

Se uma variável é acessada por mais do que uma thread, esta deve ser protegida para assegurar que o valor não será corrompido. Por exemplo, considere um programa multithread que incrementa uma variável inteira global de contagem vinda de múltiplas threads conforme a seguir:

count = count + 1;

Esse código é executado das instruções da CPU conforme a sequência a seguir:

1. Move o valor de count para um registrador do processador

2. Incrementa o valor no registrador do processador

3. Escreve o valor do registrador do processador de volta em count

Pelo fato do SO poder interromper uma thread em pontos arbitrários em sua execução, duas threads executanto essa sequência de instruções poderiam executá-las na seguinte ordem (supondo que o count começa com o valor 5):

Thread 1: Move o valor de count para o registrador do processador. (count =5, registrador=5)

Troca para Thread 2. (count = 5, registrador = ?)

Thread 2: Move o valor de count para o registrador do processador. (count =5, registrador =5)

Thread 2: Incrementa o valor no registrador  do processador. (count =5, registrador =6)

Thread 2: Escreve o valor no registrador do processador de volta em count. (count = 6, registrador = 6)

Troca para Thread 1. (count = 6, registrador = 5)

Thread 1: Incrementa o valor no registrador do processor. (count = 6, registrador = 6)

Thread 1: Escreve o valor do registrador do processador de volta em count. (count = 6, registrador = 6)

Porque a thread 1 foi interrompida antes que ela pudesse incrementar e atualizar o valor de volta, o valor final de count é 6 ao invés de 7. O SO mantém uma cópia separada dos registradores do processador para cada thread no sistema. Mesmo se você escrever o código como count++, você continua a ter o mesmo problema porque o processador continua a executar o código como múltiplas instruções. Note que uma condição de temporização particular ocasionou esta falha. Isso significa que o seu programa pode executar corretamente 1000 vezes para cada vez ele falhar. Esta evidência empírica mostrou que programas multithread com proteção de dados incorreta tipicamente são executados corretamente durante testes, mas falham imediatamente quando seu cliente os instala e os executa.

Tipos de Proteção de Dados

Todo o dado que é acessado por mais do que uma thread em um programa deve ser protegido. Variáveis globais, variáveis locais estáticas e memórias alocadas dinamicamente estão situadas em áreas de memória comum que são acessíveis por todas as threads de um programa. Os dados armazenados nesses tipos de posição de memória devem ser protegidos contra acessos simultâneos por múltiplas threads. Os parâmetros da função e as variáveis locais não estáticas estão situados no stack. O SO aloca um stack separado para cada thread. Por essa razão, cada thread obtém sua própria cópia dos parâmetros e das variáveis locais não estáticas, então os parâmetros e as variáveis locais não estáticas não tem que ser protegidos contra acessos simultâneos. O código a seguir mostra quais tipos de dados precisam ser protegidos contra acessos simultâneos por múltiplas threads.

int globalArray[1000];// Must be protected
static staticGlobalArray[500];// Must be protected
int globalInt;// Must be protected

void foo (int i)// i does NOT need to be protected
{
    int localInt;// Does NOT need to be protected
    int localArray[1000];// Does NOT need to be protected
    int *dynamicallyAllocdArray;// Must be protected
    static int staticLocalArray[1000];// Must be protected

    dynamicallyAllocdArray = malloc (1000 * sizeof (int));
}

Como Proteger os seus Dados

  • Em geral, você protege o seu dado em um programa multithread pela associação de um objeto do SO thread-locking com a variável que fixa o seu dado. A qualquer momento que você queira obter ou configurar o valor da variável, você primeiro chama uma função da API do SO para adquirir o objeto SO thread-locking. Depois de obter ou configurar a variável, você libera o objeto thread-locking. O SO permite que apenas uma thread adquira um determinado objeto thread-locking em um dado momento. Se uma thread chama a função a API do SO para adquirir um objeto thread-locking enquanto outra thread já o tem, a thread que tenta adquirir o objeto thread-locking não tem a permissão para retornar da função de aquisição da API do SO até que a thread que tem a posse do objeto thread-locking o libere. Uma thread que está tentando adquirir o objeto thread-locking que é uma propriedade de outra thread é referido como um blocked thread. A Biblioteca de Utilidades do LabWindows/CVI fornece três mecanismos para proteger os dados – thread lock, thread-safe variable e thread-safe queue.

    Um thread lock é uma proteção sobre um simples objeto thread-locking. Aqui existem três circunstâncias sob as quais você pode usar um thread lock. Se você tem uma seção de código que acessa várias variáveis de dados compartilhados, você pode querer adquirir um thread lock antes de executar o código e liberar o thread lock depois de executar o código. O benefício dessa abordagem é que o código é mais simples e menos propenso a erros do que em uma abordagem onde você protege cada parte do dado individualmente. A desvantagem é diminuir o desempenho porque as threads no seu programa tenderão a fixar o recurso por mais tempo do que é realmente necessário, o que faz com que outras threads bloqueiem (esperem) mais do que o necessário para obter um recurso. Outra circunstância sob a qual você pode querer usar um recurso é para proteger o acesso a bibliotecas de terceiros ou códigos que não são thread safe. Se, por exemplo, você tem uma DLL que não é thread safe e que controla um hardware e você quer chamar essa DLL para mais de uma thread, você pode criar um recurso que suas threads devem adquirir antes de chamar a DLL. A terceira circunstância na qual você pode querer usar um thread lock é para proteger recursos que são compartilhados entre programas. Memória compartilhada é um exemplo deste recurso.

    Uma thread-safe variable combina um objeto thread-locking com o dado que ele protege. Essa abordagem é mais simples e menos propensa a erro do que usando um thread lock para proteger uma parte do dado. Você pode usar thread-safe variables para proteger todos os tipos de dados, incluindo tipos estruturados. Thread-safe variables são menos propensas a erro do que os thread locks porque você deve chamar a função API da Biblioteca de Utilidades para ter acesso ao dado. Pelo fato de a função API adquirir os objetos thread-locking, você não acessará acidentalmente o dado sem adquirir o objeto thread-locking. As thread-safe variable também são mais simples de utilizar que os thread locks porque você precisa de apenas uma variável (thread-safe variable handle) ao contrário do thread lock onde são necessárias duas variáveis (thread-lock handle e a protected variable).

    Uma thread-safe queue é um mecanismo para a passagem segura de conjuntos de dados entre threads. Você tipicamente usa uma thread-safe queue quando o seu programa contém uma thread que gera um array de dados e outra thread que deve operar cálculos no array de dados. Um exemplo deste tipo de programa é uma thread que adquire dados que outra thread analisou ou mostrou em uma interface de usuário do LabWindows/CVI. Uma thread-safe queue tem as seguintes vantagens sobre uma thread-safe variable de um tipo array.

    • Internamente, a thread-safe queue usa um esquema de travamento no qual uma thread pode ler através da queue (fila) ao mesmo tempo em que outra thread está escrevendo para a queue (por exemplo, threads de leitura e escrita não bloqueiam umas às outras).
    • Você pode configurar uma thread-safe queue para acesso baseado em eventos. Você pode registrar um callback de leitura que é chamado quando certa quantidade de dados está disponível na queue e/ou um callback de escrita que é chamado quando uma quantidade específica de espaço está disponível na queue.

    Você pode configurar uma thread-safe queue para automaticamente aumentar se o dado é adicionado quando ele já está cheio.

Thread Lock

Chame CmtNewLock na inicialização do programa para criar uma trava para cada configuração de dado que você quer proteger. Essa função retorna um handle que você utilizará para identificar a trava nas chamadas subseqüentes da função. Antes de acessar o dado ou o código protegido pela trava, a thread precisa chamar CmtGetLock para adquirir a trava. Depois de acessar o dado, a thread precisa chamar CmtReleaseLock para liberar a trava. Você pode chamar CmtGetLock mais de uma vez a partir da mesma thread (isso não bloqueará nas chamadas subseqüentes), mas você deve chamar CmtReleaseLock exatamente uma vez para cada chamada que você fizer para CmtGetLock . Chame CmtDiscardLock para liberar os recursos da trava antes do término do seu programa. O código a seguir demonstra como usar a trava da Biblioteca de Utilidades do LabWindows/CVI para proteger uma variável global.

int lock;
int count;

int main (int argc, char *argv[])
{
    int functionId;
    CmtNewLock (NULL, 0, &lock);
    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, NULL, &functionId);
    CmtGetLock (lock);
    count++;
    CmtReleaseLock (lock);
    CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
    CmtDiscardLock (lock);
}
int CVICALLBACK ThreadFunction (void *functionData)
{
    CmtGetLock(lock);
    count++;
    CmtReleaseLock(lock);
    return 0;
}

Thread-Safe Variables

As Thread-safe variables combinam seu dado e um objeto thread-locking em uma única entidade. Essa abordagem evita um dos erros comuns em programas multithread: o programador erroneamente se esquece de obter uma trava antes de acessar uma variável. Essa abordagem também facilita a passagem de dados protegidos entre funções porque você tem que passar apenas o thread-safe variable handle, ao invés de passar o thread-lock handle e a variável que ele protege. O API da Biblioteca de Utilidades do LabWindows/CVI contém algumas funções para criar e acessar thread-safe variables. Com essas funções, você pode criar thread-safe variables de qualquer tipo. Portanto, os parâmetros para as funções são genéricos no tipo e não fornecem os tipos seguros. Tipicamente, você não chama diretamente as funções thread-safe variable da Biblioteca de Utilidades do LabWindows/CVI.

O arquivo de cabeçalho da Biblioteca de Utilidades do LabWindows/CVI contém algumas macros que fornecem funções wrapper seguras ao redor das funções API da Biblioteca de Utilidades. Além disso, para fornecer o tipo seguro, esses macros também ajudam você a evitar outros dois erros comuns nas programações multithread. Esses erros são: esquecer de liberar a trava depois de acessar o dado e tentar liberar a trava sem tê-la adquirido previamente. Use as macros DefineThreadSafeScalarVar DefineThreadSafeArrayVar para criar thread-safe variable e funções do tipo segura que você usa para acessá-los. Se você precisa acessar a thread-safe variable a partir de mais de um arquivo fonte, use a macro DeclareThreadSafeScalarVar ouDeclareThreadSafeArrayVar em um arquivo include (.h) para criar declarações para as funções de acesso. A macro DefineThreadSafeScalarVar (datatype, VarName, maxGetPointerNestingLevel) cria as seguintes funções de acesso:

int InitializeVarName (void);
void UninitializeVarName (void);
datatype *GetPointerToVarName (void);
void ReleasePointerToVarName (void);
void SetVarName (datatype val);
datatype GetVarName (void);

Nota: A macro usa o token que você passou como segundo parâmetro para a macro (nesse exemplo, VarName) para criar nomes da função de acesso personalizados para a thread-safe  variable.

Nota: O argumento maxGetPointerNestingLevel é discutido mais adiante.

Chame InitializeVarName uma vez (isso é, a partir de uma única thread) antes de acessar a thread-safe variable pela primeira vez. Você deve chamar UninitializeVarName antes do seu programa terminar. Se você quer mudar o valor da variável baseado no valor atual (por exemplo incrementar de um valor inteiro) chame GetPointerToVarName, mude o valor, e então chame ReleasePointerToVarName. Você pode chamar GetPointerToVarName mais do que uma vez a partir da mesma thread (isso não bloqueia nas chamadas subseqüentes), mas você deve chamar ReleasePointerToVarName exatamente uma vez para cada chamada que você fizer para GetPointerToVarName. Se você chamar ReleasePointerToVarName sem antes combinar uma chamada para GetPointerToVarName na mesma thread, ReleasePointerToVarName informará um erro de execução.

Se você deseja configurar o valor da variável independentemente do seu valor atual, chame SetVarName. Se você quer obter o valor atual da variável, chame GetVarName. Isso é importante para entender que o valor atual da variável poderia mudar depois queGetVarName ler o valor a partir da memória, mas antes GetVarName retornaria o valor para você.

O código a seguir mostra como você usaria um thread-safe variable como uma variável count mencionada no exemplo anterior.

DefineThreadSafeScalarVar (int, Count, 0);
int CVICALLBACK ThreadFunction (void *functionData);

int main (int argc, char *argv[])
{
    int functionId;
    int *countPtr;
   
    InitializeCount();
    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, NULL, &functionId);
    countPtr = GetPointerToCount();
    (*countPtr)++;
    ReleasePointerToCount();
    CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
    UninitializeCount();
    return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
    int *countPtr;

    countPtr = GetPointerToCount();
    (*countPtr)++;
    ReleasePointerToCount();
    return 0;
}

Usando Arrays como Thread-Safe Variables

A macro DefineThreadSafeArrayVar é similar ao DefineThreadSafeScalarVar exceto que ele tem um parâmetro adicional que especifica o número de elementos em um array. Também, diferentemente do DefineThreadSafeScalarVar, e do DefineThreadSafeArrayVar, não define as funções GetVarName e SetVarName. A declaração a seguir define um array thread-safe de 10 números inteiros.

DefineThreadSafeArrayVar (int, Array, 10, 0);

Combinando Múltiplas Variáveis em um Único Thread-Safe Variable

Se você tem duas ou mais variáveis que estão de alguma forma relacionadas, você deve impedir que duas threads modifiquem os valores ao mesmo tempo. Um exemplo disso é um array e um contador do número de valores válidos no array. Se uma thread remove valores de um array, ela deve atualizar antes do array e o contador, permitindo que a outra thread acesse o dado. Embora você possa usar um único thread lock da Biblioteca de Utilidades LabWindows/CVI para proteger o acesso para ambos estes valores, uma abordagem mais segura é definir a estrutura e então usar esta estrutura como uma thread-safe variable. O exemplo a seguir mostra como você pode usar uma thread-safe variable desta maneira para adicionar de forma segura um valor a um array.

typedef struct {
    int data[500];
    int count;
} BufType;

DefineThreadSafeVar(BufType, SafeBuf);

void StoreValue(int val)
{
    BufType *safeBufPtr;
    safeBufPtr = GetPointerToSafeBuf();
    safeBufPtr->data[safeBufPtr->count] = val;
    safeBufPtr->count++;
    ReleasePointerToSafeBuf();
}

Detectando Chamadas Não Combinadas para GetPointerToVarName

Você pode especificar o número máximo de chamadas para GetPointerToVarName através do último parâmetro (maxGetPointerNestingLevel) para DefineThreadSafeScalarVar e DefineThreadSafeArrayVar. Você deve geralmente passar 0 para este parâmetro para que GetPointerToVarName informe o erro de tempo de execução quando ele detectar duas chamadas consecutivas para GetPointerToVarName vindas da mesma thread sem uma chamada de intervalo para ReleasePointerToVarName. Por exemplo, o código a seguir gera um erro de execução de tempo pela segunda vez que é executado porque está faltando uma chamada para ReleasePointerToCount.

int IncrementCount (void)
{
    int *countPtr;

    countPtr = GetPointerToCount(); /* run-time error on second execution */
    (*countPtr)++;
    /* Missing call to ReleasePointerToCount here */
    return 0;

Passe um número inteiro maior do que zero como o parâmetromaxGetPointerNestingLevel se o seu código precisar fazer chamadas para GetPointerToVarName. Por exemplo, o código a seguir configura o parâmetro maxGetPointerNestingLevel para 1 porque esse é um valor válido para fazer chamadas para GetPointerToVarName um nível abaixo.

DefineThreadSafeScalarVar (int, Count, 1);
int Count (void)
{
    int *countPtr;
    countPtr = GetPointerToCount();
    (*countPtr)++;
    DoSomethingElse(); /* calls GetPointerToCount */
    ReleasePointerToCount ();
    return 0;
}
void DoSomethingElse(void)
{
    int *countPtr;
    countPtr = GetPointerToCount(); /* nested call to GetPointerToCount */
    ... /* do something with countPtr */
    ReleasePointerToCount ();
}

Se você não sabe o nível máximo para GetPointerToVarName, passe TSV_ALLOW_UNLIMITED_NESTING para desativar a verificação para chamadas não combinadas GetPointerToVarName.

Thread-Safe Queue

Usando a thread-safe queue da Biblioteca de Utilidades do LabWindows/CVI, você pode assegurar a passagem de dados entre threads. Isso é mais usual quando uma thread adquire o dado e outra thread processa esse dado. A thread-safe queue manuseia todos os dados internamente para você. Geralmente, uma thread secundária na aplicação adquire o dado enquanto a thread principal lê o dado quando ele está disponível e então analisa e/ou mostra o dado. O código a seguir mostra como uma thread usa uma thread-safe queue para passar dados para outra thread. A thread principal usa uma chamada de retorno para ler o dado quando ele está disponível.

int queue;
int panelHandle;

int main (int argc, char *argv[])
{
    if (InitCVIRTE (0, argv, 0) == 0)
        return -1; /* out of memory */
    if ((panelHandle = LoadPanel(0, "DAQDisplay.uir", PANEL)) < 0)
        return -1;
    /* create queue that holds 1000 doubles and grows if needed */
    CmtNewTSQ(1000, sizeof(double), OPT_TSQ_DYNAMIC_SIZE, &queue);
    CmtInstallTSQCallback (queue, EVENT_TSQ_ITEMS_IN_QUEUE, 500, QueueReadCallback, 0, CmtGetCurrentThreadID(), NULL);
    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, DataAcqThreadFunction, NULL, NULL);
    DisplayPanel (panelHandle);
    RunUserInterface();
    . . .
    return 0;
}
void CVICALLBACK QueueReadCallback (int queueHandle, unsigned int event, int value, void *callbackData)
{
    double data[500];
    CmtReadTSQData (queue, data, 500, TSQ_INFINITE_TIMEOUT, 0);
}

Evitando Deadlocks

  • Quando duas threads estão esperando por um obejeto thread-locking que são controlados por essas duas threads, a execução não pode continuar. Essa condição é referida como um deadlock. Se a thread da interface de usuário se torna deadlock, ele não pode responder para a entrada vinda do usuário. O usuário deve sair da aplicação de maneira anormal. O exemplo a seguir ilustra como um deadlock pode ocorrer.

    Thread 1: Chama uma função para adquirir a trava A (Thread 1: sem trava, Thread 2: sem trava)

    Thread 1: Retorna a partir da função da trava adquirida (Thread 1: trava A, Thread 2: sem trava)

    Troca para Thread 2: (Thread 1: trava A, Thread 2: sem trava)

    Thread 2: Chama uma função para adquirir a trava B (Thread 1: trava A, Thread 2: sem trava)

    Thread 2: Retorna a partir da função da trava adquirida (Thread 1: trava A, Thread 2: trava B)

    Thread 2: Chama uma função para adquirir a trava A (Thread 1: trava A, Thread 2: trava B)

    Thread 2: É bloqueado porque Thread 1 está travando A (Thread 1: trava A, Thread 2: trava B)

    Troca para Thread 1: (Thread 1: trava A, Thread 2: trava B)

    Thread 1: Chama uma função para adquirir a trava B (Thread 1: trava A, Thread 2: trava B)

    Thread 1: É bloqueado porque Thread 2 está travando B (Thread 1: trava A, Thread 2: trava B)

     

    Semelhante a erros que ocorrem a partir de dados não protegidos, os erros de deadlock são freqüentemente intermitentes por causa de diferenças no temporização da troca de threads em execuções diferentes do programa. Por exemplo, se a troca para a Thread 2 não ocorrer antes da Thread 1 travar os dois, A e B, a Thread 1 estará apta para completar seu trabalho e liberar as travas para a Thread 2 adquirir depois. Um deadlock como o descrito acima pode ocorrer apenas quando a sua thread adquire mais de uma trava ao mesmo tempo. Você pode empregar uma regra simples para evitar esse tipo de deadlock. Quando adquirindo mais do que um objeto thread-locking, todos as threads no seu programa devem sempre adquirir o objeto thread-locking no mesmo pedido. As funções da Biblioteca de Utilidades do LabWindows/CVI a seguir adquirem objetos thread-locking e retornam sem liberá-los.

    •       CmtGetLock

    •       CmtGetTSQReadPtr

    •       CmtGetTSQWritePtr

    •       CmtGetTSVPtr

    Nota: Tipicamente, você não chama CmtGetTSVPtr diretamente. Ele é chamado pela função GetPtrToVarName que a macro DeclareThreadSafeVariable criou. Desta maneira, você deve tratar todas as funções GetPtrToVarName que você chamar como uma função para adquirir o objeto thread-locking com a respectiva proteção deadlock.

    As funções do Windows SDK a seguir podem adquirir um objeto thread locking sem liberá-los antes de retornar. Nota: Esta não é uma lista detalhada.

    • EnterCriticalSection
    • CreateMutex
    • CreateSemaphore
    • SignalObjectAndWait
    • WaitForSingleObject
    • MsgWaitForMultipleObjectsEx

Monitoring and Controlling Secondary Threads

Quando você marca uma função para ser executada em uma thread separada, você pode controlar o status de execução da função agendada. Para obter o status de execução de uma função agendada, chame CmtGetThreadPoolFunctionAttribute para obter o valor do atributo ATTR_TP_FUNCTION_EXECUTION_STATUS. Você também pode registrar um callback que o thread pool chama imediatamente antes de executar uma função marcada e/ou imediatamente após a execução de uma função agendada. Você deve usar CmtScheduleThreadFunctionAdv para agendar a função se você quiser registrar como um callback.

Tipicamente, as threads secundárias devem terminar a execução antes da thread principal terminar. Se a thread principal sair antes das threads secundárias terminarem a execução, as threads secundárias podem não ter a chance de liberar os recursos que eles alocaram. Qualquer biblioteca usada por essas threads secundárias também podem não ter a oportunidade de realizarem a eliminação adequada dos dados.

Você pode chamar CmtWaitForThreadPoolFunctionCompletion para esperar com segurança o término da execução de suas threads secundárias antes que o sua thread principal termine.

Em alguns casos, a função da sua thread secundária deve manter algumas tarefas executando até que a thread principal sinalize para parar. Neste caso, a thread secundária tipicamente realiza sua tarefa dentre de um loop while. A condição de loop while é uma variável de valor inteiro que a thread principal configura para um valor diferente de zero quando ele quer sinalizar para a thread secundária para finalizar a execução. O código a seguir mostra como usar o loop while para controlar quando uma thread secundária deve terminar sua execução.

 volatile int quit = 0;

int main (int argc, char *argv[])
{
    int functionId;
    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, NULL, &functionId);
    // This would typically be done inside a user interface
    // Quit button callback.
    quit = 1;
    CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
    return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
    while (!quit) {
        . . .
    }
    return 0;
}

Nota: Se você usar a palavra chave volatile esse código funcionará devidamente em um compilador otimizado, tal como o Microsoft Visual C++. Um compilador otimizado determina que nada dentro do loop while pode mudar o valor da variável quit. Desta forma, ao otimizar, o compilador pode usar apenas o valor inicial da variável quit na condição do loop while. Use a palavra chave volatile para notificar ao compilador que outra thread pode mudar o valor da variável quit. Como resultado, o compilador usa a atualização do valor da variável quit cada vez que o loop é executado.

Algumas vezes isso é conveniente para suspender a execução de uma thread secundária enquanto a thread principal está realizando outra tarefa. Se você suspender uma thread que está executando um código do SO, você pode deixar o SO em estado inválido. Desta forma, você deve sempre chamar a função do Windows SDK SuspendThread a partir da thread que você quer suspender. Sendo assim, você sabe que a thread não é suspensa enquanto é executado um código crítico. É seguro chamar a função do Windows SDK ResumeThread a partir de outra thread. O código a seguir demonstra como fazer isso.

volatile int quit = 0;

int main (int argc, char *argv[])
{
    int functionId;
    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, NULL, &functionId);
    // This would typically be done inside a user interface
    // Quit button callback.
    quit = 1;
    CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
    return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
    while (!quit) {
        . . .
    }
    return 0;

 

Processos e Prioridades nas Threads

No Windows, você pode especificar a importância relativa, conhecida como prioridade, do trabalho que está sendo feito em cada processo e thread. Se você der ao seu processo ou a uma thread em sua aplicação maior prioridade, esse processo ou thread terá preferência sobre as outras threads que tem prioridade menor. Isso significa que quando existem duas ou mais threads prontas para serem executadas, a thread com maior prioridade é executada primeiro.

O Windows agrupa as prioridades em classes. Todas as threads em um processo compartilham da mesma classe de prioridade. Cada thread em um processo tem a prioridade que é relativa à classe de prioridade do processo. Chame a função do Windows SDK SetProcessPriorityClass para configurar a prioridade do processo relativa a outros processos que estão sendo executados no seu sistema.

A National Instruments recomenda que você não configure a prioridade do seu processo para prioridade real-time a não ser que você faça isso por um período muito curto de tempo. Quando o seu processo é configurado para prioridade real-time, ele bloqueia as interrupções do sistema enquanto essa prioridade é executada. Isso resulta no mau funcionamento do mouse, teclado, disco rígido e de outras características críticas do sistema e possivelmente causa o travamento do seu sistema.

Se você chama CmtScheduleThreadFunctionAdv para marcar uma função para executar em um thread pool, você também pode especificar a prioridade da thread que executa a função que você agendou. O thread pool muda a prioridade da thread antes que ela execute a função que você agendou. O thread pool restaura a prioridade da thread para o seu valor padrão depois que sua função finaliza a execução. Você pode usar CmtScheduleThreadFunctionAdv para especificar a prioridade da thread no thread pool padrão bem como para thread pools personalizados.

Se você criar um thread pool personalizado na Biblioteca de Utilidades LabWindows/CVI (ao chamar CmtNewThreadPool), você pode configurar a prioridade padrão do thread pool.

Processamento de Mensagens

Todas as thread que criam uma janela precisam processar mensagens do Windows para evitar que ocorra o travamento do seu sistema. A função RunUserInterface da Biblioteca de Interface de Usuário contém um loop que processa os eventos da interface de usuário do LabWindows/CVI e processa mensagens do Windows. As funções GetUserEvent e ProcessSystemEvents da Biblioteca de Interface de Usuário processam mensagens do Windows cada vez que você as chama. Cada thread no seu programa precisa chamar GetUserEvent ouProcessSystemEvents regularmente para processar mensagens do Windows se uma ou outra das seguintes circunstâncias for verdade:

  • A thread cria uma janela e não chama RunUserInterface.
  • A thread cria uma janela e chama RunUserInterface mas executa callbacks que levam uma significante parcela de tempo (mais do que poucas centenas de milisegundos) antes de retornar de volta para o loop RunUserInterface.

Entretanto, podem ter partes do seu código onde ele não é apropriado para processar mensagens do Windows. Quando você chama GetUserEvent, ProcessSystemEvents, ou RunUserInterface  a partir da thread de interface de usuário do LabWindows/CVI, essa thread pode chamar um callback da interface de usuário. Se você chamar uma dessas funções em um callback da interface de usuário, a thread pode chamar outro callback. A não ser que você tenha planejado isso, este evento pode gerar um funcionamento inesperado.

As funções multithread da Biblioteca de Utilidades que podem fazer com que as suas threads tenham que esperar até que um loop permita a você especificar se quer processar mensagens na thread de espera. Por exemplo, CmtWaitForThreadPoolFunctionCompletion tem o parâmetro Option com o qual você pode especificar que a thread de espera processe as mensagens do Windows.

Isso não é sempre tão óbvio quando uma thread cria uma janela. As funções da Biblioteca de Interface de Usuário, tais como LoadPanel, CreatePanel, e FileSelectPopup criam janelas que você mostra e descarta. Essas funções também criam uma janela oculta para cada thread que as chama. Essa janela oculta não é destruída quando você descarta a janela visível. Além disso, para essas funções da Biblioteca de Interface de Usuário, várias outras funções da Biblioteca LabWindows/CVI e do Windows API criam janelas com fundo oculto. Para evitar que o seu sistema trave, você deve processar mensagens do Windows em threads que criam janelas em qualquer uma das duas formas.

Usando Variáveis Thread-Local

As variáveis thread-local são semelhantes às variáveis globais quanto ao acesso de qualquer thread. Enquanto as variáveis globais fixam um único valor para todas as threads, as variáveis thread-local fixam valores separados para cada thread que as acessam. Você tipicamente usa uma variável thread-local quando o seu programa é estruturado de modo que ele realize uma determinada tarefa em mais de um contexto ao mesmo tempo, gerando uma thread separada para cada contexto. Por exemplo, se você escrever um programa de teste paralelo que gera uma thread para fixar cada unidade em análise, você pode usar variáveis thread-local para fixar as informações que são específicas para cada unidade (por exemplo, o número serial).

Embora a API do Windows proporcione um mecanismo para a criação e acesso de variáveis thread-local, esse mecanismo limita o número de variáveis thread-local que você pode ter em cada processo. As funções de variáveis thread-local da Biblioteca de Utilidades do LabWindows/CVI não têm essa limitação. O código a seguir demonstra como criar e acessar variáveis thread-local que fixam valores inteiros.

volatile int quit = 0;
volatile int suspend = 0;
int main (int argc, char *argv[])
{
    int functionId;
    HANDLE threadHandle;
    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, NULL, &functionId);
    . . .
    // This would typically be done in response to user input or a
    // change in program state.
    suspend = 1;
    . . .
    CmtGetThreadPoolFunctionAttribute (DEFAULT_THREAD_POOL_HANDLE, functionId, ATTR_TP_FUNCTION_THREAD_HANDLE, &threadHandle);
    ResumeThread (threadHandle);
    . . .
    return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
    while (!quit) {
        if (suspend) {
            SuspendThread (GetCurrentThread ());
            suspend = 0;
        }
        . . .
    }
    return 0;

 

int CVICALLBACK ThreadFunction (void *functionData);
int tlvHandle;
int gSecondaryThreadTlvVal;

int main (int argc, char *argv[])
{
    int functionId;
    int *tlvPtr;

    if (InitCVIRTE (0, argv, 0) == 0)
        return -1; /* out of memory */
    CmtNewThreadLocalVar (sizeof(int), NULL, NULL, NULL, &tlvHandle);
    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, 0, &functionId);
    CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
    CmtGetThreadLocalVar (tlvHandle, &tlvPtr);
    (*tlvPtr)++;
    // Assert that tlvPtr has been incremented only once in this thread.
    assert (*tlvPtr == gSecondaryThreadTlvVal);
    CmtDiscardThreadLocalVar (tlvHandle);
    return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
    int *tlvPtr;

    CmtGetThreadLocalVar (tlvHandle, &tlvPtr);
    (*tlvPtr)++;
    gSecondaryThreadTlvVal = *tlvPtr;
    return 0;
}

Armazenamento Dinâmico de Dados Alocados em Variáveis Thread-Local

Se você usar uma variável thread-local para armazenar um recurso alocado dinamicamente, você deve liberar cada cópia do recurso que você alocou. Em outras palavras, você deve liberar cada cópia do recurso para cada thread que ele aloca. Com as funções das variáveis thread-local do LabWindows/CVI você pode especificar um callback de descarte para uma variável thread-local. Quando você descarta a variável thread-local, o callback é chamado por cada thread que acessou a variável. O código a seguir demonstra como você cria e acessa uma variável thread-local que fixa um string alocado dinamicamente.

int CVICALLBACK ThreadFunction (void *functionData);
void CVICALLBACK StringCreate (char *strToCreate);
void CVICALLBACK StringDiscard (void *threadLocalPtr, int event, void *callbackData, unsigned int threadID);
int tlvHandle;
volatile int quit = 0;
volatile int secondStrCreated = 0;

int main (int argc, char *argv[])
{
    int functionId;

    if (InitCVIRTE (0, argv, 0) == 0)
        return -1; /* out of memory */
    CmtNewThreadLocalVar (sizeof(char *), NULL, StringDiscard, NULL, &tlvHandle);
    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, "Secondary Thread", &functionId);
    StringCreate ("Main Thread");
    while (!secondStrCreated){
        ProcessSystemEvents ();
        Delay (0.001);
    }
    CmtDiscardThreadLocalVar (tlvHandle);
    quit = 1;
    CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
    return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
   char **sString;

   // Create thread local string variable
   StringCreate ((char *)functionData);

   // Get thread local string and print it
   CmtGetThreadLocalVar (tlvHandle, &sString);
   printf ("Thread local string: %s\n", *sString);

   secondStrCreated = 1;

   while (!quit)
   {
       ProcessSystemEvents ();
       Delay (0.001);
   }

   return 0;
}
void CVICALLBACK StringCreate (char *strToCreate)
{
    char **tlvStringPtr;
    CmtGetThreadLocalVar (tlvHandle, &tlvStringPtr);
    *tlvStringPtr = malloc (strlen (strToCreate) + 1);
    strcpy (*tlvStringPtr, strToCreate);
}
void CVICALLBACK StringDiscard (void *threadLocalPtr, int event, void *callbackData, unsigned int threadID)
{
    char *str = *(char **)threadLocalPtr;
    free (str);

Alguns recursos que você alocou devem ser liberados a partir da mesma thread que os alocou. Tais recursos dizemos que possuem afinidade com as threads. Por exemplo, um painel deve ser descartado a partir da mesma thread que o criou. Quando você chama CmtDiscardThreadLocalVar, a Biblioteca de Utilidades chama o callback de descarte da variável thread-local na thread que chamou CmtDiscardThreadLocalVar. A Biblioteca de Utilidades chama o callback de descarte uma vez para cada thread que acessou a variável. Ela passa, no parâmetro threadID para o callback de descarte, o threadID da thread para o qual ele está chamando o callback de descarte. Você pode usar este threadID para determinar se você quer liberar os seus recursos com afinidade direta com as threads ou se você deve chamar a função Toolslib PostDeferredCallToThreadAndWait para liberar os recursos na thread correta. O código a seguir demonstra como mudar o exemplo anterior para liberar os strings a partir das threads que as alocaram.

void CVICALLBACK StringDiscard (void *threadLocalPtr, int event, void *callbackData, unsigned int threadID)
{
    char *str = *(char **)threadLocalPtr;
   
    if (threadID == CmtGetCurrentThreadID ())
        free (str);
    else
        PostDeferredCallToThreadAndWait (free, str, threadID, POST_CALL_WAIT_TIMEOUT_INFINITE);

Executando Callbacks (Chamadas de Retorno) em Threads Separados

Com algumas Bibliotecas do LabWindows/CVI, você pode receber chamadas de retorno em uma thread criada de sistema. Pelo fato dessas bibliotecas criarem automaticamente threads que executam seus callbacks, você não tem que criar threads ou marcar uma função para ser executada em uma thread separada. Você ainda tem que proteger os dados que são compartilhados entre essas threads e outras threads em seu programa. As implementações dessas chamadas de retorno são tipicamente referidas como eventos assíncronos.

Na Biblioteca do LabWindows/CVI GPIB/GPIB 488.2, você pode chamar ibnotify para registrar um callback que a Biblioteca GPIB/GPIB 488.2 chama quando eventos ocorrem. Você pode especificar uma função de callback por placa ou equipamento. Você pode especificar os eventos para os quais você gostaria que os seus callbacks fossem chamados. A Biblioteca GPIB/GPIB 488.2 cria uma thread que é usada para executar seu callback.

Na Biblioteca do LabWindows/CVI Virtual Instrument Software Architecture (VISA), você pode chamar viInstallHandler para registrar um ou mais eventos manipulados (funções de callback) para os eventos do tipo VISA (I/O completion, serviço de requisição e assim por diante) que você quer receber de um determinado ViSession. A Bilioteca VISA normalmente utiliza uma thread separada que é criada para executar seu callback. O VISA pode usar a mesma thread para todos os callbacks em um processo ou uma thread diferente para cada ViSession. Você deve chamar viEnableEvent para o(s) tipo(s) de evento(s) específico(s) para informar a Bilioteca VISA que você quer chamar o evento manipulado que você registrou.

Na Biblioteca VXI do LabWindows/CVI, cada tipo de interrupção ou callback tem  o registro da sua própria callback e funções habilitadas. Por exemplo, para receber uma interrupção NI-VXI, você deve chamar ambos SetVXIintHandler e EnableVXIint. A Biblioteca VXI usa uma thread separada que é criada para executar seu callback. A Biblioteca VXI usa as mesmas threads para todos os callbacks em um determinado processo.

Selecionando um Processador para uma Thread

Você pode usar a função SetThreadIdealProcessor da Plataforma SDK para especificar o processador no qual você gostaria de executar uma determinada thread. O primeiro parâmetro para essa função é um thread handle. O segundo parâmetro é um processador indexado iniciando em zero. Você pode chamar a função CmtGetThreadPoolFunctionAttribute da Biblioteca de Utilidades do LabWindows/CVI com o atributo ATTR_TP_FUNCTION_THREAD_HANDLE para obter o handle do thread pool. Você pode chamar a função da Biblioteca de Utilidades do LabWindows/CVI CmtGetNumberOfProcessors para através de programação determinar o número de processadores no equipamento que estão executando o seu programa.

Você pode usar a função SetProcessAffinityMask da Plataforma SDK para especificar os processadores no quais as threads no seu programa terão a permissão de ser executados. Você pode usar a função SetThreadAffinityMask da Plataforma SDK para especificar os processadores nos quais uma determinada thread de seu programa terá a permissão de ser executada. A máscara que você passa para SetThreadAffinityMask deve ser uma subconfiguração da máscara que você passou para SetProcessAffinityMask.

Essas funções têm algum efeito apenas quando o seu programa é executado em um equipamento multicore com o Microsoft Windows XP/2000/NT 4.0. O SO Microsoft Windows 9x, não trabalha com equipamentos multicore.

Was this information helpful?

Yes

No