Como conseguir a sincronização de thread --- leitura de notas para programação de servidor multithread

Quatro princípios de design

1. Use objetos compartilhados tanto quanto possível para reduzir a necessidade de sincronização. Se um objeto não pode ser exposto a outros threads, não o exponha; se você quiser expô-lo, considere apenas o objeto imutável; se não funcionar, você pode expor o objeto a ser modificado; se não Para funcionar, você pode modificar o objeto exposto e usar medidas de sincronização para protegê-lo.

2. Em segundo lugar está o uso de componentes de programação simultânea avançados, como TaskQueue, Fila de Produtor-Consumidor, Contagem DownLatch;

3. Quando você tiver que usar primitivas de sincronização como último recurso, use apenas mutexes e variáveis ​​de condição, use bloqueios de leitura e gravação com cuidado e não use semáforos.

4. Não escreva código livre de bloqueio sozinho e não use primitivos de sincronização no nível do kernel.

 

2.1 Mutex

Mutex é o primitivo de sincronização mais usado. O principal objetivo de usar mutex sozinho é usar dados compartilhados. Meus princípios pessoais são:

Use a técnica de RAII para criar, destruir, bloquear e desbloquear essas quatro operações. Evite esquecer

Basta usar mutex não recursivo.

As funções de bloqueio e desbloqueio não são chamadas manualmente e tudo é entregue às funções de construção e destruição do objeto Guard na pilha. O ciclo de vida do Guard é exatamente igual à zona crítica. Desta forma, podemos garantir que, no mesmo escopo, adicionar e desbloquear automaticamente.

Cada vez que um objeto Guard é construído, considere os bloqueios que foram mantidos ao longo do caminho para evitar bloqueios causados ​​por diferentes bloqueios

Os princípios secundários são:

Não use mutex não recursivo

1) O bloqueio e o desbloqueio devem estar no mesmo segmento, o segmento a não pode desbloquear o mutex que o segmento b bloqueou

2) Não se esqueça de desbloquear

3) Não desbloqueie repetidamente

4) Use PTHREAD_MUTEX_ERRORCHECK para solucionar o problema quando necessário

2.1.1 Use apenas mutex não recursivo

Fale sobre meus pensamentos pessoais sobre aderir a mutex não recursivo

Mutex é dividido em dois tipos: recursivo e não recursivo. Este é o nome de posix. Os outros nomes são reentrantes e não reentrantes. A diferença entre os dois é que o mesmo encadeamento pode bloquear repetidamente um bloqueio reentrante, mas não pode bloquear um bloqueio não recursivo.

O mutex não recursivo preferido definitivamente não é para desempenho, mas para refletir a intenção do design. A diferença de desempenho entre a recursão e a não recursão não é realmente grande, porque há um contador a menos, o primeiro é um pouco mais rápido e o
uso múltiplo de bloqueios não recursivos no mesmo encadeamento levará a bloqueios. Esta é uma vantagem que permite nos a encontrar deficiências no início.

Não há dúvida de que os bloqueios recursivos são mais convenientes de usar e você não precisa se preocupar em se bloquear no mesmo segmento.

É justamente por sua conveniência que os bloqueios recursivos podem esconder alguns problemas.Você acha que pode modificar o objeto se obtiver um bloqueio, mas o código externo já obteve o bloqueio e está modificando o mesmo objeto.

Vejamos como os bloqueios recursivos e não recursivos são usados.

Primeiro encapsulei um mutex

class MutexLock{
public:
    MutexLock()
    {
        pthread_mutexattr_init(&mutexattr);
        pthread_mutex_init(&mutex, nullptr);
    }

    MutexLock(int type)
    {
        int res;
        pthread_mutexattr_init(&mutexattr);
        res = pthread_mutexattr_settype(&mutexattr,type);
        pthread_mutex_init(&mutex, &mutexattr);
    }

    ~MutexLock()
    {
        pthread_mutex_destroy(&mutex);
    }

    void lock()
    {
        int res = pthread_mutex_lock(&mutex);
        std::cout<<res<<std::endl;
    }

    void unLock()
    {
        pthread_mutexattr_destroy(&mutexattr);
        pthread_mutex_unlock(&mutex);
    }
private:
    pthread_mutex_t mutex;
    pthread_mutexattr_t mutexattr;
};

Passe o tipo no construtor para determinar o tipo de bloqueio

Então assistimos a uma demonstração

MutexLock mutex(PTHREAD_MUTEX_RECURSIVE);


void foo()
{
    mutex.lock();
    // do something
    mutex.unLock();
}

void* func(void* arg)
{
    mutex.lock();
    printf("3333\n");
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr,func, nullptr);
    foo();
    int res;
    mutex.lock();
    sleep(5);
    mutex.unLock();
    sleep(3);
}

Neste trecho de código, descobrimos que o programa não bloqueou após foo e mutex.lock na thread principal, mas continuou a execução, o que significa que o bloqueio recursivo na mesma thread é reentrante e não causa deadlock.

Vamos testar o bloqueio padrão e ajustar o método do aplicativo para:

MutexLock mutex(PTHREAD_MUTEX_DEFAULT);

Veremos que o programa acima está em um impasse! !

Eu concordo com Chen Shuo aqui. A vantagem de usar bloqueios não recursivos é muito óbvia. Ele é muito fácil de encontrar erros. Mesmo se houver um deadlock, podemos usar o gdb para ir para o thread correspondente bt.

Claro, também podemos usar o atributo PTHREAD_MUTEX_ERRORCHECK_NP em c para verificar o erro, só precisamos declarar o bloqueio da seguinte forma

MutexLock mutex(PTHREAD_MUTEX_ERRORCHECK_NP);

Então usamos o seguinte programa

int main()
{
    pthread_t tid;
    int res;
    res = mutex.lock();
    printf("%d\n",res);
    res = mutex.lock();
    printf("%d\n",res);
    mutex.unLock();
    printf("end\n");
}

Portanto, quando houver um impasse, retornaremos ao EDEADLK

#define EDEADLK     35  /* Resource deadlock would occur */

Portanto, devido à perspectiva de que os problemas são fáceis de solucionar, concordo com a prática de usar apenas bloqueios não recursivos no livro, e o impasse não é explicado.O método de localização do problema também é mencionado acima.

2.2 Variáveis ​​de condição

O mutex é para evitar contenção por recursos da calculadora e tem características exclusivas, mas se quisermos esperar que uma determinada condição seja estabelecida, desbloqueie

Devemos ter aprendido a função correspondente na programação do ambiente de rede unix

pthread_cond_wait
pthread_cond_signal

endereço csdn: https://blog.csdn.net / shichao1470 / article / details / 89856443
Aqui eu ainda tenho que mencionar um ponto de pthread_code_wait:
"O chamador passa o mutex bloqueado para a função, e a função automaticamente Coloque o thread de chamada na lista de threads aguardando a condição e desbloquear o mutex. Isso fecha o
canal de tempo entre a verificação da condição e a thread entrando em hibernação aguardando a mudança da condição, de modo que a thread não perca a condição Quaisquer mudanças. Quando
pthread_cond_wait retorna, o mutex é bloqueado novamente. " pthread_cond_wait irá desbloquear o byte !!

A quantidade de informações nesta passagem é muito grande, e o funcionamento do mutex pode ser entendido como os três pontos a seguir:

1. Antes de chamar pthread_cond_wait, você precisa bloquear o mutex mutex antes de passar o & mutex para a função pthread_cond_wait
. 2. Dentro da função pthread_cond_wait, o mutex de entrada será desbloqueado primeiro
. 3. Quando a condição de espera chegar, a função pthread_cond_wait está dentro Irá bloquear o mutex de entrada antes de retornar

Se precisarmos esperar que uma condição seja estabelecida, precisamos usar variáveis ​​de condição.As variáveis ​​de condição são threads múltiplas ou uma thread esperando que uma determinada condição seja despertada. O nome científico da variável de condição também é chamado de Guan Cheng!

Existe apenas uma maneira de usar variáveis ​​de condição, e é quase impossível usá-las incorretamente. Para o lado da espera:

1. Deve ser usado com mutex, este valor booleano deve ser protegido por mutex

2. A espera pode ser chamada apenas quando o mutex está bloqueado

3 - Coloque a condição Booleana de julgamento e espere no loop while

Vamos escrever um exemplo para o código acima:

Podemos escrever uma classe de condição simples nós mesmos

class Condition:noncopyable
{
public:
    //explicit用于修饰只有一个参数的构造函数,表明结构体是显示是的,不是隐式的,与他相对的另一个是implicit,意思是隐式的
    //explicit关键字只需用于类内的单参数构造函数前面。由于无参数的构造函数和多参数的构造函数总是显示调用,这种情况在构造函数前加explicit无意义。
    Condition(MutexLock& mutex) : mutex_(mutex)
    {
        pthread_cond_init(&pcond_, nullptr);
    }

    ~Condition()
    {
        pthread_cond_destroy(&pcond_);
    }

    void wait()
    {
        pthread_cond_wait(&pcond_,mutex_.getMutex());
    }

    void notify()
    {
        (pthread_cond_signal(&pcond_));
    }

    void notifyAll()
    {
        (pthread_cond_broadcast(&pcond_));
    }

private:
    MutexLock& mutex_;
    pthread_cond_t pcond_;
};

Aqui, falar sobre não copiável é principalmente proibir a cópia, o principal é privatizar o construtor de cópia

class noncopyable{
protected:
    noncopyable() = default;
    ~noncopyable() = default;

private:
    noncopyable(const noncopyable&) = delete;
    const noncopyable& operator=( const noncopyable& ) = delete;
};

O código acima deve usar um loop while para esperar pela variável de condição em vez de usar a instrução if. A razão é que o despertar espúrio (despertar falso)
também é o ponto de teste da entrevista.
Para o sinal e a transmissão termina:
1. Ele não é necessário que o mutex tenha sido bloqueado, neste caso, sinal de chamada (em teoria).
2. Geralmente modifique a expressão booleana antes do sinal
3. Modifique a expressão booleana geralmente para ser protegida por mutex
4. Preste atenção para distinguir entre sinal e transmissão: transmissão geralmente indica uma mudança de estado, sinal indica que o recurso está disponível

Aqui para falar sobre o que é falso despertar, se usarmos se para julgar

if(条件满足)
{
    pthread_cond_wait();
}

pthread_cond_wait pode ser interrompido quando o sinal ou transmissão não é chamado (pode ser interrompido ou despertado por um sinal), então devemos usar enquanto aqui

De acordo com o exemplo do livro, podemos escrever uma demonstração de fila de maneira muito simples

Observe as duas funções de queue.cc

int dequeue()
{
    mutex.lock();
    while(queue.empty())
    {
        cond.wait();
    }
    int top = queue.front();
    queue.pop_front();
    mutex.unLock();
    return top;
}

void enqueue(int x)
{
    mutex.lock();
    queue.push_back(x);
    cond.notify();
}

Aqui podemos pensar sobre um ponto juntos. O Cond. Notify deve apenas despertar um tópico?

Cond.notify pode despertar mais de um thread, mas se usarmos while para evitar o falso despertar, cond_wait múltiplo é ativado, o kernel irá essencialmente bloquear o mutex, então apenas
um thread continuará a ser executado. Faça o while (queue.empty ()) julgamento, por isso ainda é thread-safe

Variáveis ​​de condição são primitivas de nível muito baixo e raramente são usadas diretamente. Geralmente são usadas para medidas de sincronização de alto nível. Agora, nosso exemplo disse BlockingQueue. Vamos continuar a aprender CountDownLatch.

CountDownLatch também é uma medida comum de sincronização e tem dois usos principais:

1. O encadeamento principal inicia vários encadeamentos filhos e espera que os encadeamentos filhos concluam certas tarefas antes que o encadeamento principal continue a ser executado. Normalmente usado para o thread principal para aguardar a inicialização de vários sub-threads para concluir.

2. O encadeamento principal inicia vários sub-encadeamentos e os sub-encadeamentos aguardam o encadeamento principal. Depois que o encadeamento principal conclui algumas outras tarefas, os sub-encadeamentos começam a ser executados. Normalmente usado para vários threads filhos para aguardar o comando de início do thread principal

Vamos analisar a implementação de countDownLatch in muduo e analisar um pouco o significado dessas funções

Vamos olhar para __attribute__ novamente, o código em Muze é a realidade

#define THREAD_ANNOTATION_ATTRIBUTE__(x)   __attribute__((x))

Vamos revisar __attribute__ (https://blog.csdn.net / qlexcel / article / details / 92656797)

attribute__ pode definir atributos de função, atributos de variáveis ​​e atributos de classe. O método de usar __attribute__ é __attribute ((x))

__attribute__ pode definir a união da estrutura, existem cerca de 6 parâmetros que podem ser definidos: alinhados, compactados, transparent_union, não utilizados, obsoletos, may_alias

Ao usar __attribute__, você também pode adicionar __ sublinhados antes e depois dos parâmetros. Por exemplo, use __aligned__ em vez de alinhado, para que você possa usá-lo no arquivo de cabeçalho correspondente
sem se preocupar com o arquivo de cabeçalho Existe uma definição de macro com o mesmo nome dentro

1 、 alinhado

Especifique o formato de alinhamento do objeto

struct S {

short b[3];

} __attribute__ ((aligned (8)));


typedef int int32_t __attribute__ ((aligned (8)));

Esta declaração força o compilador a garantir que o tipo de variável seja struct S ou int32_t ao alocar espaço com 8 bytes alinhados. Vamos dar uma olhada nos resultados da demonstração

struct S {

    short b[3];

};

O resultado após sizeof é 6

struct S {

    short b[3];

}__attribute__((__aligned__(8)));

Depois de escrever desta forma, o resultado é 8

2) Packed
usa este atributo para definir o tipo de estrutura ou união e definir as restrições de memória de cada variável de seu tipo. Diz ao compilador para cancelar a estrutura está alinhada na otimização do processo de compilação para o número real de
linhas ocupadas estão alinhadas, é a sintaxe específica do gcc, esta função não tem nada a ver com o sistema operacional, com o compilador sobre, o compilador gcc não é compacto , sob windwos Vc não é compacto, a programação tc é compacta

Vejamos este exemplo novamente

struct S {

    int a;
    char b;
    short c;


}__attribute__((__packed__));

Deve ter 8 bytes se o pacote for removido, mas o compilador não executará o alinhamento de bytes depois que __packed__ tiver 7 bytes

3) .at

Posicionamento absoluto, variáveis ​​ou funções podem ser posicionadas absolutamente em Flash ou RAM. Este software basicamente não é usado, afinal, a camada inferior da placa ram é

Bem, aqui eu quero continuar a olhar para alguns usos muito detalhados no código muduo

Eu vi muitos códigos interessantes em muduo,

#ifndef MUDUO_BASE_MUTEX_H
#define MUDUO_BASE_MUTEX_H

#include "muduo/base/CurrentThread.h"
#include "muduo/base/noncopyable.h"
#include <assert.h>
#include <pthread.h>

// Thread safety annotations {
// https://clang.llvm.org/docs/ThreadSafetyAnalysis.html

// Enable thread safety attributes only with clang.
// The attributes can be safely erased when compiling with other compilers.
#if defined(__clang__) && (!defined(SWIG))
#define THREAD_ANNOTATION_ATTRIBUTE__(x)   __attribute__((x))
#else
#define THREAD_ANNOTATION_ATTRIBUTE__(x)   // no-op
#endif

#define CAPABILITY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(capability(x))

#define SCOPED_CAPABILITY \
  THREAD_ANNOTATION_ATTRIBUTE__(scoped_lockable)

#define GUARDED_BY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x))

#define PT_GUARDED_BY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(pt_guarded_by(x))

#define ACQUIRED_BEFORE(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(acquired_before(__VA_ARGS__))

#define ACQUIRED_AFTER(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(acquired_after(__VA_ARGS__))

#define REQUIRES(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(requires_capability(__VA_ARGS__))

#define REQUIRES_SHARED(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(requires_shared_capability(__VA_ARGS__))

#define ACQUIRE(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(acquire_capability(__VA_ARGS__))

#define ACQUIRE_SHARED(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(acquire_shared_capability(__VA_ARGS__))

#define RELEASE(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(release_capability(__VA_ARGS__))

#define RELEASE_SHARED(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(release_shared_capability(__VA_ARGS__))

#define TRY_ACQUIRE(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_capability(__VA_ARGS__))

#define TRY_ACQUIRE_SHARED(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_shared_capability(__VA_ARGS__))

#define EXCLUDES(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(locks_excluded(__VA_ARGS__))

#define ASSERT_CAPABILITY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(assert_capability(x))

#define ASSERT_SHARED_CAPABILITY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(assert_shared_capability(x))

#define RETURN_CAPABILITY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(lock_returned(x))

#define NO_THREAD_SAFETY_ANALYSIS \
  THREAD_ANNOTATION_ATTRIBUTE__(no_thread_safety_analysis)

A propriedade é a primeira vez que se vê, no documento há uma explicação específica, consulte a URL: HTTP: //clang.llvm.org /docs/ThreadSafetyAnalysis.html

Aqui, eu olho principalmente para guarded_by usado neste código, vamos dar uma olhada na definição desta macro separadamente

#define GUARDED_BY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x))

Essa macro significa que o atributo deve ser bloqueado primeiro, para depois poder ser lido. Voltando ao livro, comecei a escrever código para praticar. No código, vi outro detalhe, que é sobre isso mutável.

Vamos dar uma olhada na implementação de CountDownLatch

class CountDownLatch:noncopyable{
public:
    explicit CountDownLatch(int count) : mutex_(),cond_(mutex_),count_(count)
    {
    }
    void wait()
    {
        MutexLockGuard guard(mutex_);
        while(count_ > 0)
        {
            cond_.wait();
        }
    }

    void countDOwn()
    {
        MutexLockGuard guard(mutex_);
        --count_;
        if(count_ == 0)
        {
            cond_.notifyAll();
        }
    }

    int getCount() const
    {
        MutexLockGuard guard(mutex_);
        return count_;
    }

private:
    mutable MutexLock mutex_;
    Condition cond_ __attribute__((guarded_by(mutex_)));
    int count_;
};

O significado literal mutável é variável, fácil de mudar, mutável, mas também para quebrar as limitações de const, eu tenho uma pergunta que é quando você deve usar mutável, c ++ frequentemente funciona para operar um gênero
de membros, tempo para fazer alterações se necessário o membro do atributo a ser variável.

Características de função constante:

1. Só pode usar membros de dados não podem ser modificados

2. Objetos constantes só podem chamar funções constantes, não funções comuns

3. O ponteiro this de uma função constante é const *

getCount é uma função constante, então mutex_ deve ser uma variável

Vamos dar uma olhada no uso desta classe

CountDownLatch syncTool(10);

class CThread{
public:
    //线程进程
    static void* threadProc(void* args)
    {
        sleep(4);
        syncTool.countDOwn();
        sleep(3);
    }
};

int main()
{


    int count = 10;
    int i;
    pthread_t tid;
    pthread_t pthread_pool[count];
    CThread threadStack;
    shared_ptr<CThread> threadObj = make_shared<CThread>();
    for(i=0;i<count;i++)
    {
        pthread_create(&tid, nullptr,&CThread::threadProc, nullptr);
    }

    syncTool.wait();

    for(i=0;i<count;i++)
    {
        pthread_join(tid, nullptr);
    }

}

Depois que o thread filho for ativado, ele diminuirá o contador. Se todos os threads filhos forem inicializados, diga ao thread principal para continuar descendo

o sono não é um primitivo de sincronização

A série de funções sleep só pode ser usada para teste. Existem basicamente dois tipos de espera em threads, aguardando que os recursos estejam disponíveis e esperando para entrar na seção crítica

Se estiver em um programa normal, se você precisar esperar por um período de tempo conhecido, deve injetar um cronômetro no loop de eventos e, em seguida, continuar a trabalhar no retorno de chamada do cronômetro. Threads são um recurso precioso que não pode ser facilmente
desperdiçado e não pode ser pesquisado com o sono.

Realização de singleton em multithreading

O singleton em meu código foi implementado assim:

T* CSingleton<T, CreationPolicy>::Instance (void)
{
    if (0 == _instance)
    {
        CnetlibCriticalSectionHelper guard(_mutex);

        if (0 == _instance)
        {
            _instance = CreationPolicy<T>::Create ();
        }
    }

    return _instance;
}

Esta também é uma realização muito clássica. Tenho feito isso desde a minha formatura.

Na programação de servidor multithread, use pthread_once para alcançar

template <typename T>
class Singleton :noncopyable{
public:
    static T& instance()
    {
        pthread_once(&ponce_,&Singleton::init);
        return *value_;
    }

private:
    Singleton() = default;
    ~Singleton() = default;

    static void init()
    {
        value_ = new T();
    }

    static T* value_;
    static pthread_once_t ponce_;
};

É realmente uma nota muito boa usar a API do kernel para conseguir.

Acho que você gosta

Origin blog.csdn.net/qq_32783703/article/details/105896010
Recomendado
Clasificación