Cómo lograr la sincronización de subprocesos --- lectura de notas para la programación de servidores multiproceso

Cuatro principios de diseño

1. Utilice objetos compartidos tanto como sea posible para reducir la necesidad de sincronización. Si un objeto no puede exponerse a otros hilos, no lo exponga; si desea exponerlo, considere solo el objeto inmutable; si no funciona, puede exponer el objeto para ser modificado; si no lo hace ' Si funciona, puede modificar el objeto expuesto y utilizar medidas de sincronización para protegerlo.

2. El segundo es el uso de componentes avanzados de programación simultánea, como TaskQueue, Cola de productor-consumidor, Count DownLatch;

3. Cuando tenga que usar primitivas de sincronización como último recurso, solo use mutex y variables de condición, use bloqueos de lectura y escritura con precaución y no use semáforos.

4. No escriba usted mismo código libre de bloqueo y no utilice primitivas de sincronización a nivel de kernel.

 

2.1 Mutex

Mutex es la primitiva de sincronización más utilizada. El propósito principal de usar mutex solo es usar datos compartidos. Mis principios personales son:

Utilice la técnica de RAII para crear, destruir, bloquear y desbloquear estas cuatro operaciones. Evita olvidar

Simplemente use mutex no recursivo.

Las funciones de bloqueo y desbloqueo no se llaman manualmente, y todo se entrega a las funciones de construcción y destrucción del objeto Guard en la pila. El ciclo de vida de Guard es exactamente igual a la zona crítica. De esta forma, podemos asegurarnos de que en el mismo ámbito, agregar y desbloquear automáticamente.

Cada vez que se construye un objeto Guard, considere los bloqueos que se han mantenido a lo largo del camino para evitar interbloqueos causados ​​por diferentes bloqueos.

Los principios secundarios son:

No use mutex no recursivo

1) El bloqueo y el desbloqueo deben estar en el mismo hilo, el hilo a no puede desbloquear el mutex que el hilo b ha bloqueado

2) No olvide desbloquear

3) No desbloquear repetidamente

4) Utilice PTHREAD_MUTEX_ERRORCHECK para solucionar problemas cuando sea necesario

2.1.1 Solo use mutex no recursivo

Hablar de mis pensamientos personales acerca de ceñirme al mutex no recursivo

Mutex se divide en dos tipos: recursivo y no recursivo. Este es el nombre de posix. Los otros nombres son reentrantes y no reentrantes. La diferencia entre los dos es que el mismo hilo puede bloquear repetidamente un bloqueo reentrante, pero no puede bloquear un bloqueo no recursivo.

El mutex no recursivo preferido definitivamente no es para el rendimiento, sino para reflejar la intención del diseño. La brecha de rendimiento entre la recursividad y la no recursividad en realidad no es grande, porque hay un contador menos, el primero es un poco más rápido y el
uso múltiple de bloqueos no recursivos en el mismo hilo dará lugar a puntos muertos. Esta es una ventaja que permite nosotros para encontrar deficiencias temprano.

No hay duda de que los bloqueos recursivos son más convenientes de usar y no necesita pensar en encerrarse en el mismo hilo.

Es precisamente por su conveniencia que los bloqueos recursivos pueden ocultar algunos problemas.Crees que puedes modificar el objeto si obtienes un bloqueo, pero el código externo ya tiene el bloqueo y está modificando el mismo objeto.

Veamos cómo se utilizan los bloqueos recursivos y no recursivos.

Primero encapsulé un 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;
};

Pase el tipo en el constructor para determinar el tipo de bloqueo

Luego vemos una demostración

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);
}

En este fragmento de código, encontramos que el programa no se bloqueó después de foo y mutex.lock en el hilo principal, sino que continuó la ejecución, lo que significa que el bloqueo recursivo en el mismo hilo es reentrante y no causa un punto muerto.

Probemos el bloqueo predeterminado y ajustemos el método de aplicación a:

MutexLock mutex(PTHREAD_MUTEX_DEFAULT);

¡Encontraremos que el programa anterior está bloqueado! !

Estoy de acuerdo con Chen Shuo aquí. La ventaja de usar bloqueos no recursivos es muy obvia. Es muy fácil encontrar errores. Incluso si hay un punto muerto, podemos usar gdb para ir al hilo correspondiente bt.

Por supuesto, también podemos usar el atributo PTHREAD_MUTEX_ERRORCHECK_NP en c para verificar el error, solo necesitamos declarar el bloqueo de la siguiente manera

MutexLock mutex(PTHREAD_MUTEX_ERRORCHECK_NP);

Entonces usamos el siguiente 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");
}

Entonces, cuando tengamos un punto muerto, volveremos a EDEADLK

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

Por lo tanto, debido a la perspectiva de que los problemas son fáciles de solucionar, estoy de acuerdo con la práctica de usar solo bloqueos no recursivos en el libro y no se explica el punto muerto. El método para localizar el problema también se menciona anteriormente.

2.2 Variables de condición

El mutex es para evitar la contención por los recursos de la calculadora y tiene características exclusivas, pero si queremos esperar a que se establezca una determinada condición, entonces desbloqueamos

Debemos haber aprendido la función correspondiente en la programación del entorno de red unix

pthread_cond_wait
pthread_cond_signal

Dirección csdn: https://blog.csdn.net / shichao1470 / article / details / 89856443
Aquí todavía tengo que mencionar un punto de pthread_code_wait:
"La persona que llama pasa el mutex bloqueado a la función, y la función automáticamente pone el hilo de llamada en la lista de subprocesos que esperan la condición y desbloquear el mutex. Esto cierra el
canal de tiempo entre la verificación de la condición y el subproceso que se pone a dormir esperando que la condición cambie, de modo que el subproceso no perderá la condición Cualquier cambio. Cuando
pthread_cond_wait regresa, el mutex se bloquea de nuevo. "¡¡ pthread_cond_wait desbloqueará el byte !!

La cantidad de información en este pasaje es muy grande, y el funcionamiento del mutex puede entenderse como los siguientes tres puntos:

1. Antes de llamar a pthread_cond_wait, debe bloquear el mutex mutex antes de pasar el & mutex a la función pthread_cond_wait
. 2. Dentro de la función pthread_cond_wait, primero se desbloqueará el mutex entrante
. 3. Cuando llegue la condición de espera, la función pthread_cond_wait está adentro. Bloqueará el mutex entrante antes de regresar

Si necesitamos esperar a que se establezca una condición, necesitamos usar variables de condición, que son múltiples subprocesos o un subproceso que espera que se despierte una determinada condición. El nombre científico de la variable de condición también se llama Guan Cheng.

Solo hay una forma de utilizar las variables de condición y es casi imposible utilizarlas incorrectamente. Para el lado de la espera:

1. Debe usarse con mutex, este valor booleano debe estar protegido por mutex

2. Sólo se puede llamar a Wait cuando el mutex está bloqueado

3. Ponga la condición booleana de juicio y espere en el ciclo while

Escribiremos un ejemplo para el código anterior:

Podemos escribir una clase de condición simple nosotros mismos

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_;
};

Aquí hablar de no copiable es principalmente para prohibir la copia, el núcleo es privatizar el constructor de copias

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

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

El código anterior debe usar un bucle while para esperar la variable de condición en lugar de usar la instrucción if. La razón es que el despertar espurio (despertar falso)
también es el punto de prueba de la entrevista.
Para la señal y la transmisión finaliza:
1. Es No es necesario que el mutex esté bloqueado, en este caso llamar a la señal (en teoría).
2. Modifique generalmente la expresión booleana antes de la señal
3. Modifique la expresión booleana generalmente para que esté protegida por mutex
4. Preste atención para distinguir entre señal y transmisión: la transmisión generalmente indica un cambio de estado, la señal indica que el recurso está disponible

Aquí para hablar de lo que es falso despertar, si usamos if para juzgar

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

pthread_cond_wait puede interrumpirse cuando no se llama a la señal o transmisión (puede ser interrumpido o despertado por una señal), por lo que debemos usarlo mientras estamos aquí

Según el ejemplo del libro, podemos escribir una demostración en cola de forma muy sencilla

Mira las dos funciones 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();
}

Aquí podemos pensar juntos en un punto. Cond. Notify ¿debe simplemente despertar un hilo?

Cond.notify puede despertar más de un subproceso, pero si usamos while para evitar un falso despertar, se despiertan varios cond_wait, el kernel básicamente bloqueará el mutex, por lo que solo
un subproceso continuará ejecutándose. Haga el while (queue.empty ()) juicio, por lo que todavía es seguro para subprocesos

Las variables de condición son primitivas de muy bajo nivel y rara vez se usan directamente. Por lo general, se usan para medidas de sincronización de alto nivel. Hace un momento, nuestro ejemplo decía BlockingQueue. Sigamos aprendiendo CountDownLatch.

CountDownLatch también es una medida común de sincronización, tiene dos usos principales:

1. El subproceso principal inicia varios subprocesos secundarios y espera a que los subprocesos secundarios completen ciertas tareas antes de que el subproceso principal continúe ejecutándose. Por lo general, se usa para que el subproceso principal espere a que se complete la inicialización de varios subprocesos.

2. El subproceso principal inicia varios subprocesos y los subprocesos esperan al subproceso principal. Después de que el subproceso principal completa algunas otras tareas, los subprocesos comienzan a ejecutarse. Normalmente se usa para que varios subprocesos secundarios esperen el comando de inicio del subproceso principal

Analicemos la implementación de countDownLatch en muduo, y analicemos un poco el significado de estas funciones

Veamos __attribute__ nuevamente, el código en Muze es la realidad

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

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

atributo__ puede establecer atributos de función, atributos de variable y atributos de clase. El método de usar __attribute__ es __attribute ((x))

__attribute__ puede establecer la unión de la estructura, hay aproximadamente 6 parámetros que se pueden configurar: alineado, empaquetado, transparent_union, no utilizado, obsoleto, may_alias

Al usar __attribute__, también puede agregar __ guiones bajos antes y después de los parámetros. Por ejemplo, use __aligned__ en lugar de alineado, para que pueda usarlo en el archivo de encabezado correspondiente
sin preocuparse por el archivo de encabezado ¿Existe una definición de macro con el mismo nombre en

1 、 alineado

Especificar el formato de alineación del objeto

struct S {

short b[3];

} __attribute__ ((aligned (8)));


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

Esta declaración obliga al compilador a asegurarse de que el tipo de variable sea struct S o int32_t al asignar espacio con 8 bytes alineados. Echemos un vistazo a los resultados de la demostración.

struct S {

    short b[3];

};

El resultado después de sizeof es 6

struct S {

    short b[3];

}__attribute__((__aligned__(8)));

Después de escribir de esta manera, el resultado es 8

2) Packed
usa este atributo para definir el tipo de estructura o unión y establecer las restricciones de memoria de cada variable de su tipo. Le dice al compilador que cancele la estructura está alineada en la optimización del proceso de compilación en el número real de
líneas ocupadas que están alineadas, es una sintaxis específica de gcc, esta función no tiene nada que ver con el sistema operativo, con el compilador sobre, el compilador de gcc no es compacto , bajo windwos Vc no es compacto, la programación tc es compacta

Veamos este ejemplo nuevamente

struct S {

    int a;
    char b;
    short c;


}__attribute__((__packed__));

Esto debería ser de 8 bytes si se elimina el paquete, pero el compilador no realizará la alineación de bytes después de que __packed__ tenga 7 bytes

3) .at

Posicionamiento absoluto, variables o funciones pueden posicionarse absolutamente en Flash o RAM. Este software básicamente no se utiliza, después de todo, la capa inferior de la placa ram es

Bueno, aquí quiero seguir viendo algunos usos muy detallados en el código muduo

Vi muchos códigos interesantes en 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)

La propiedad es la primera vez que se ve, en el documento hay una explicación específica ver URL: HTTP: //clang.llvm.org /docs/ThreadSafetyAnalysis.html

Aquí miro principalmente el guarded_by usado en este código, echemos un vistazo a la definición de esta macro por separado

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

Esta macro significa que el atributo debe estar bloqueado primero, y luego se puede leer. Volviendo al libro, comencé a escribir código para practicar. En el código, vi otro detalle, que es sobre este mutable.

Echemos un vistazo a la implementación 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_;
};

El significado literal mutable es variable, fácil de cambiar, mutable pero también para romper las limitaciones constantes, tengo una pregunta de cuándo debe usar mutable, c ++ a menudo funciona para operar un género
de miembros tiempo para hacer cambios si es necesario Establecer el miembro de atributo sea variable.

Características de función constante:

1. Solo se pueden usar los miembros de datos no se pueden modificar

2. Los objetos constantes solo pueden llamar funciones constantes, no funciones ordinarias

3. El este puntero de una función constante es constante *

getCount es una función constante, por lo que mutex_ debe ser una variable

Echemos un vistazo al uso de esta clase.

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);
    }

}

Una vez que el subproceso secundario se despierta, disminuirá el contador. Si se inicializan todos los subprocesos secundarios, dígale al subproceso principal que continúe hacia abajo

dormir no es una sincronización primitiva

La serie de funciones de suspensión solo se puede utilizar para realizar pruebas. Existen principalmente dos tipos de espera en los subprocesos, esperar a que los recursos estén disponibles y esperar para ingresar a la sección crítica.

Si en un programa normal, si necesita esperar un período de tiempo conocido, debe inyectar un temporizador en el bucle de eventos y luego continuar trabajando en la devolución de llamada del temporizador. Los hilos son un recurso valioso que no se puede
desperdiciar fácilmente y no ser encuestado con el sueño.

Realización de singleton en multiproceso

El singleton en mi código se ha implementado así:

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

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

    return _instance;
}

Esta también es una realización muy clásica. He estado haciendo esto para singleton desde mi graduación.

En la programación de servidor multiproceso, use pthread_once para lograr

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_;
};

De hecho, es una muy buena nota usar la API del kernel para lograrlo.

Supongo que te gusta

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