¿Los programas win32 utilizan _beginthread de la biblioteca de lenguaje C o CreateThread de la API para crear subprocesos? ¿Cuál se usa con más frecuencia?
Usa el programa. Este diseño se adoptó porque el tiempo de ejecución estándar C se inventó alrededor de 1970. Tomará mucho, mucho tiempo.
Después de eso, el concepto de subprocesos aparecerá en el sistema operativo. Los inventores del tiempo de ejecución estándar C simplemente no tenían aplicaciones multiproceso en mente.
Problemas al utilizar c runtime en programas. Usemos un ejemplo para comprender posibles problemas.
Tomemos como ejemplo la variable global errno del tiempo de ejecución estándar de C. Algunas funciones configuran esta variable en caso de error. Supongamos que existe un fragmento de código de este tipo:
BOOL fFailure = (system("NOTEPAD.EXE README.TXT ")= =-1);
if (failure) { p>
cambio (número de error){
caso E2BIG: //La lista de parámetros o el entorno es demasiado grande
Pausa;
Entrada de caso: //intérprete de comandos no encontrado
Breaking;
case ENOEXEC: //El formato del intérprete de comandos es incorrecto
Breaking; >Case e mem://Memoria insuficiente para ejecutar el comando
Roto;
}
}
Supongamos que el hilo que ejecuta el comando El código anterior se interrumpe después de llamar a la función del sistema y antes de ejecutar la instrucción if. Además, esto es falso
Supongamos que después de que se interrumpe este hilo, otro hilo en el mismo proceso comienza a ejecutarse y este nuevo hilo ejecutará otro.
Función de biblioteca en tiempo de ejecución c, esta función establece la variable global errno. Cuando la CPU se vuelve a asignar posteriormente al primer subproceso, para la llamada a la función del sistema del código anterior
err ya no refleja el código de error correcto. Para resolver este problema, cada hilo necesita
su propia variable errno. Además, debe haber algún mecanismo para que el hilo haga referencia a su propia variable errno y, al mismo tiempo,
no puede permitir que toque la variable errno de otro hilo.
Este es sólo uno de los muchos ejemplos que demuestran que el tiempo de ejecución estándar de C/C++ no fue diseñado originalmente para aplicaciones multiproceso.
Las variables y funciones de tiempo de ejecución de C/C++ que pueden causar problemas en entornos multiproceso son errno, _doserrno, strtok, _wcstok,
Strerror, _strerror, tmpnam, tmpfile, asctime, _wasctime , gmtime, _ecvt y _fcvt, etc.
Para garantizar el funcionamiento normal de las aplicaciones multiproceso C y C++, se debe crear una estructura de datos y asociarla con C/C++.
Cada hilo de una función de biblioteca está asociado. Luego, al llamar a funciones de la biblioteca de tiempo de ejecución de C/C++, esas funciones deben saber cómo encontrar la nota clave.
Bloque de datos del hilo para evitar afectar a otros hilos.
Entonces, ¿cómo sabe el sistema asignar este bloque de datos al crear un nuevo hilo? La respuesta es que no lo sabe. Sistema
No tenía idea de que la aplicación estaba escrita en C/C++, ni de que las funciones que estabas llamando no eran inherentemente seguras para subprocesos. Importe de la garantía
La seguridad del programa es responsabilidad del programador. Al crear un nuevo hilo, nunca llame a la función CreateThread del sistema operativo.
Por el contrario,
Se debe llamar a C/C++ para ejecutar la función de biblioteca _beginthreadex:
Unsigned long _beginthreadex(
void *security,
Tamaño de pila sin firmar,
Sin firmar (*dirección_inicio)(void *),
void *arglist,
Initflag sin firmar,
Unsigned *thr addr);
La lista de parámetros de la función _beginthreadex es la misma que la lista de parámetros de la función CreateThread, pero los nombres y tipos de parámetros no son exactamente los mismos.
Muestra. Esto se debe a que el equipo de desarrollo de tiempo de ejecución C/C++ de Microsoft cree que las funciones de la biblioteca de tiempo de ejecución C/C++ no deben usarse para las clases de datos de Windows.
El tipo tiene dependencias. La función _beginthreadex también devuelve un identificador al hilo recién creado, al igual que CreateThread. Entonces,
Si ya ha llamado a la función CreateThread en su código fuente, puede usar _beginthreadex fácilmente para completarla.
Reemplace todos los CreateThreads. Sin embargo, debido a que los tipos de datos no son exactamente iguales, es posible que sea necesario realizar alguna conversión de tipos.
Para poder pasar con éxito la compilación. Para facilitar esto, creé una macro llamada chBEGINTHREADEX.
Y úsalo en tu propio código fuente:
typedef unsigned(_ _ stdcall * PTHREAD _ START)(void *);
#define chBEGINTHREADEX( psa, cbStack, pfnStartAddr, \
pvParam, fdwCreate, pdwThreadID) \
((HANDLE) _beginthreadex( \
(void *) (psa), \
(unsigned)(cbStackSize),\
(PTHREAD_START) (pfnStartAddr),\
(void *) (pvParam),\
(Unsigned)(dwCreateFlags),\
(Unsigned*) (pdwThreadID)))
Según el tiempo de ejecución C/C++ proporcionado por Microsoft Desde el código fuente, es fácil ver que _beginthreadex está bien, pero CreateThread no.
¿Qué hice? De hecho, después de buscar en la carpeta de instalación de Visual Studio, encontré el código fuente de
_beginthreadex en Studio 8\VC\CRT\src\threadex.c, para ahorrar espacio, no todas las fotos fueron tomadas aquí. .
Cópialo de nuevo. En su lugar, he proporcionado aquí una versión en pseudocódigo de la función, destacando los bits más interesantes:
uintptr_t __cdecl _beginthreadex(
void *psa,
Unsigned cbStackSize,
Sin firmar (__stdcall * pfnStartAddr) (void *),
void * pvParam,
dwCreateFlags sin firmar,
Sin firmar * pdwThreadID) {
_ ptiddata ptd//Puntero al bloque de datos del hilo
uintptr _ t thdl//Manejador del hilo
//Asignar bloque de datos para hilo nuevo.
if((ptd =(_ ptid data)_ calloc _ CRT(1, sizeof(struct _tiddata))) == NULL)
ir a error _ return
//Inicializar bloque de datos.
init ptd(ptd);
//Guardar la función y los parámetros del hilo requeridos
//Queremos que entre en el bloque de datos.
ptd->_ init addr = (void *)pfnStartAddr;
ptd->_ initarg = pvParam
ptd-> =(uintptr _ t)(-1);
//Crear un nuevo hilo.
thdl =(uintptr _ t)CreateThread((LP seguridad _ ATTRIBUTES)PSA, cbStackSize,
_threadstartex, (PVOID) ptd, dwCreateFlags, pdwThreadID
<); p>if (thdl == 0) {//No se puede crear el hilo, limpiarlo y devolver el error.
goto error _ return
}
//El hilo se crea correctamente y el identificador se devuelve como un entero largo sin signo.
return(thdl);
Error_return:
//Error: No se puede crear un bloque de datos o un hilo.
//GetLastError() se asigna al valor correspondiente a errno
//Si ocurre un error en CreateThread.
_ free _ CRT(ptd);
return((uintptr _ t)0L
}
Para la función _beginthreadex; , debe prestar atención a los siguientes puntos.
Cada subproceso tiene su propio bloque de memoria _tiddata dedicado, que se asigna desde el montón de tiempo de ejecución de C/C++.
Sí.
La dirección de la función de subproceso pasada a _beginthreadex se almacena en el bloque de memoria _tiddata. (La estructura de datos _tid está en el código fuente C++ del archivo Mtdll.h). Solo para agregar interés, copié esta estructura a continuación. Requisitos
Los parámetros pasados a la función _beginthreadex también se almacenan en este bloque de datos.
_beginthreadex llama a CreateThread internamente porque el sistema operativo solo sabe cómo hacerlo.
Crear un hilo nuevo.
Al llamar a la función CreateThread, la dirección de la función que se le pasa es _threadstartex (no
pfnStartAddr. Además, la dirección del parámetro es la dirección de la estructura _tiddata, no pvParam).
Si todo va bien, se devolverá el identificador del hilo, al igual que CreateThread. Cualquier operación fallida devolverá 0.
struct _tiddata {
Tid largo sin firmar/*ID de subproceso */
Thandle largo sin firmar/*identificador de subproceso*/
int _ terrno/*valor de error*/
unsigned long _tdoserrno/* _doserrno value*/
unsigned int _ fpds/*segmento de datos de coma flotante*/
Unsigned long _ holdrand/* rand() valor semilla */
char * _ token/* ptr to strtok() mark */
wchar _ t * _ wtoken/ *Puntero al token wcstok()*/
Carbón sin firmar * _ mtoken/* ptr al token _mbstok() */
/*El siguiente puntero Error asignado en tiempo de ejecución */
char * _ errmsg/* ptr a strerror()/_ strerror()buff */
wchar _ t * _ werrmsg/* ptr a _ wcs error()/_ _ wcs error()buff */
char * _ namebuf0/* ptr al búfer tmpnam() */
wchar _ t * _ wnamebuf0/* ptr al búfer _wtmpnam() * /
char * _ nombre buf 1; /* ptr al búfer tmpfile() */
wchar _ t * _ wname buf 1 /* ptr al _ wtm archivo(); buffer */
char * _ asctimebuf/* ptr al buffer asctime() */
wchar _ t * _ wasctimebuf/ * ptr al buffer _wasctime() */
void * _ gmtimebuf/* ptr a estructura gmtime() */
char * _ cvtbuf/* ptr a ecvt()/fcvt buffer*/
carácter sin firmar _ con _ ch _ buf[MB _ LEN _ MAX];
/* ptr al buffer putch() */
Corto sin firmar _ ch _ buf _ usado/*If _ se utiliza con _ ch _ buf */
/* El código de inicio del hilo requiere los siguientes campos*/
void * _ initaddr/*Dirección inicial del hilo del usuario*/
void * _ initarg/*Parámetros iniciales del hilo del usuario*/
/*Los siguientes tres campos son necesarios para admitir el manejo de señales y errores de tiempo de ejecución */
void * _ pxcptacttab/*Puntero a la tabla de operaciones de excepción*/
void * _ tpxcptinfoptrs/*Puntero al puntero de información de excepción*/
int _ tfpecode/*Código de excepción de punto flotante*/ p>
/*Puntero a una copia de la información de caracteres multibyte utilizada por el subproceso*/
pthreadmbcinfo ptmbcinfo
/*Puntero a una copia de la información local utilizada por el hilo*/
pthreadlocinfo ptlocinfo
int _ ownlocale/*Si es 1, el hilo posee su propia configuración regional*/
/* Las rutinas NLG requieren los siguientes campos*/
Long_NLG_dwCode sin firmar;
/*
* Se requieren datos por subproceso para manejo de excepciones de c++
*/
void * _ terminate/* rutina terminate() */
void * _Unexpected ;/*Rutina inesperada ()*/
void * _ traductor/*Traducción avanzada*/
void * _purecall/*Llamar cuando ocurre virtual puro*/ p>
void * _ curexception/*Actual excepción*/
void * _ curcontext/*Contexto de excepción actual*/
int _ ProcessingThrow/* para un catch _ excepción */
void * _ curexcspec/*Se usa para manejar excepciones causadas por std::unexpected*/
#Si está definido (_M_IA64) ||Está definido (_M_AMD64) p>
void * _ pExitContext
void * _ pUnwindContext
void * _ pFrameInfoChain
Unsigned_ _ int64 _ ImageBase
#Si está definido (_M_IA64)
unsigned _ _ int64 _ TargetGp
#endif /*Defined (_M_IA64) */
Unsigned_ _ int64 _ ThrowImageBase
void * _ pForeignException
#elif está definido (_M_IX86)
void * _ pFrameInfoChain
#endif / *Definido (_M_IX86) */
_ setloc _ struct _ setloc _ data
void * _ encode _ ptr/* Rutina EncodePointer() */
void * _ decode _ ptr/* Rutina DecodePointer() */ p>
void * _ reservado 1;/*Ninguno*/
void * _ reservado2/*Ninguno* /
void * _ reservado3/*Ninguno*/ p>
int _ cxxReThrow/*Si se trata de una excepción de C++ reiniciada, establecida en True */
Unsigned long _ _ initDomain/*Usado por _beginthread[ex] para el dominio inicial alojado
Función*/
};
typedef struct _ tid data * _ ptid data;
Después de asignar e inicializar la estructura _tiddata para el nuevo hilo, debe saber cómo se asocia esta estructura con el hilo. Echemos un vistazo a la función
_threadstartex (también en el archivo Threadex.c del tiempo de ejecución de C/C++). Esto es lo que hice para esta función y sus ayudantes
Versión de pseudocódigo escrita por la función __callthreadstartex:
WINAPI larga estática sin firmar _ thread startex(void * ptd){
//Nota: ptd es la dirección del bloque tiddata de este hilo.
//Asocia el bloque tiddata con este hilo, así
// _getptd() podrá encontrarlo en _callthreadstartex.
TlsSetValue(__tlsindex, ptd);
//Guarde este ID de hilo en el bloque _tiddata.
((_ptid data)ptd)->_tid = GetCurrentThreadId();
//Inicializar el soporte de punto flotante (código no mostrado).
//Llame a la función auxiliar.
_ callthreadstartex();
//Nunca llegamos aquí; el hilo termina en _callthreadstartex.
return(0L);
}
static void _callthreadstartex(void) {
_ ptiddata ptd/* apunta al hilo _tiddata Puntero de estructura*/
//Obtener el puntero de datos del hilo de TLS
ptd = _ get ptd();
//Obtener todos los datos del hilo en el Marco SEH Las funciones de subproceso requeridas están empaquetadas como
// Manejo de errores de tiempo de ejecución y soporte de señales.
_ _Try {
// Llama a la función de hilo requerida, pasándole los parámetros requeridos.
// Pasa el valor del código de salida del hilo a _endthreadex.
_endthreadex(
((unsigned(WINAPI *)(void *)(((_ ptid data)ptd)->_initaddr))
( (((_ ptid data)ptd)-& gt; _ initarg));
}
_ _ except(_ xcpt filter(obtener código de excepción(), obtener información de excepción ()){
//El controlador de excepciones de tiempo de ejecución de C maneja los errores de tiempo de ejecución
//y señala el soporte; no deberíamos obtenerlo aquí
. exit(GetExceptionCode());}
}
Tenga en cuenta los siguientes puntos sobre la función _threadstartex:
El nuevo hilo primero ejecuta RtlUserThreadStart (en el archivo NTDLL.dll) y luego salta a
_threadstartex.
El único parámetro de _threadstartex es el bloque de memoria _tiddata de la nueva dirección del hilo. p>
TlsSetValue es una función del sistema operativo que asocia un valor con el subproceso que realiza la llamada. Esto se denomina almacenamiento local de subprocesos (TLS); consulte el Capítulo 21 para obtener más detalles. La función _threadstartex asocia el
_tiddata. bloque de memoria con el hilo recién creado.
En la función auxiliar sin parámetros _callthreadstartex, un marco SEH rodea la función de hilo esperada que se ejecutará.
Este marco maneja muchas cosas. relacionado con el tiempo de ejecución, como generar una excepción
C++ no detectada, y la función de señal de tiempo de ejecución C/C++. Esto es muy importante. Si usa la función CreateThread,
crea un nuevo hilo. y luego llama a la función de señal de tiempo de ejecución de C/C++, la función de señal no funcionará correctamente.
Se espera que llame a la función de subproceso que se ejecutará y le pase los parámetros esperados.
Como se mencionó anteriormente, la dirección y los parámetros de la función se guardan en el bloque _tiddata de TLS mediante _beginthreadex y se recuperarán de _callthreadstartex;
Obtenido en TLS.
El valor de retorno de una función de hilo se considera el código de salida del hilo.
Tenga en cuenta que _callthreadstartex no simplemente regresa a _threadstartex y luego a RtlUserThreadStart.
En este caso, el hilo dejará de ejecutarse y su código de salida se configurará correctamente, pero el hilo del hilo dejará de ejecutarse. El bloque de memoria _tiddata
no será destruido. Esto puede provocar pérdidas de memoria en la aplicación. Para evitar este problema, se llama.
_endthreadex (también una función de biblioteca de tiempo de ejecución de C/C++) y le pasa el código de salida.
La última función a tener en cuenta es _endthreadex (también en el archivo Threadex.c del tiempo de ejecución de C). A continuación se muestra lo que escribí.
Versión en pseudocódigo de la función:
void __cdecl _endthreadex(retcode unsigned) {
_ ptiddata ptd//Puntero al bloque de datos del hilo
//Borrar soporte de punto flotante (código no mostrado).
//Obtiene la dirección del bloque tiddata de este hilo.
ptd = _ getptd _ no exit();
//Libera el bloque tiddata.
If (ptd!=null)
_ free ptd(ptd);
//Terminar el hilo.
salir del hilo (retcode);
}
Para la función _endthreadex, preste atención a los siguientes puntos:
c runtime La función _getptd_noexit llama internamente a la función TlsGetValue del sistema operativo, que obtiene el tónico.
La dirección del bloque de memoria tiddata del hilo.
Luego, se libera el bloque de datos y se llama a la función ExitThread del sistema operativo para destruir el hilo. Por supuesto,
el código de salida se pasará y se configurará correctamente.
Al principio de este capítulo, sugerí que deberías evitar el uso de la función ExitThread. Es verdad, estoy aquí.
No hay contradicción en el significado. Como se mencionó anteriormente, esta función mata el hilo principal y no permite el retorno de la función que se está ejecutando actualmente.
Volver. Dado que la función no regresa, ningún objeto C++ construido será destruido. Ahora ya no tenemos teléfono.
Otra razón para la función ExitThread es que evita que se libere el bloque de memoria _tiddata del hilo, lo que provocará pérdidas de memoria en la aplicación.
Fuga (hasta finalizar todo el proceso).
El equipo de desarrollo de C++ de Microsoft también se da cuenta de que siempre hay algunos desarrolladores a los que les gusta llamar a ExitThread. Por lo tanto, deben
hacer esto posible evitando al máximo las pérdidas de memoria en la aplicación. Si realmente quieres suicidarte por la fuerza
Thread, puedes hacer que llame a _endthreadex (en lugar de ExitThread) para liberar el bloque _tiddata del hilo y salir. Sin embargo,
Le desaconsejo que llame a _endthreadex.
Ahora, deberías entender por qué la función de la biblioteca de tiempo de ejecución de C/C++ prepara un bloque de datos separado para cada nuevo hilo, y
Deberías entender cómo _beginthreadex asigna e inicializa este bloque de datos y asociarlo con un nuevo hilo. Además, también
Debes comprender cómo la función _endthreadex libera el bloque de datos cuando el hilo deja de ejecutarse.
Una vez que este bloque de datos se inicializa y se asocia con el subproceso, el subproceso llama a cualquier operación C/C++ que requiera "datos de instancia por subproceso".
Las funciones de la biblioteca de líneas pueden obtener fácilmente la dirección del bloque de datos del hilo que llama (a través de TlsGetValue) y manipular los datos del hilo. No hay nada malo con esta
función. Pero, ¿cómo funciona para variables globales como errno? Errno está dentro
Se define en el archivo de encabezado C estándar de la siguiente manera:
_ Cr TIMP extern int * _ _ cdecl _ errno(void);
#define errno (*_errno())
int* __cdecl _errno(void) {
_ ptid data ptd = _ get ptd _ no exit();
if (!ptd) {
return &ErrnoNoMem
} else {
return (&ptd->_terrno);
p>}
}
Siempre que se hace referencia a errno, en realidad se está llamando a la función interna de la biblioteca de ejecución de C/C++ _errno. Esta función devuelve la dirección al miembro de datos errno en el bloque de datos asociado con el hilo que llama. Tenga en cuenta que la macro errno está definida para obtener el contenido de una dirección. Esta
definición es necesaria porque es posible escribir el siguiente código:
int * p = & amperrno
if (*p = = ENOMEM) {
...
}
Si la función interna _errno solo devuelve el valor de errno, el código anterior no se compilará.
El tiempo de ejecución de C/C++ también coloca primitivas de sincronización alrededor de funciones específicas. Por ejemplo, si dos subprocesos llaman a malloc al mismo tiempo, el montón se dañará. Función de biblioteca de tiempo de ejecución de C/C++ para evitar que dos subprocesos asignen memoria del montón de memoria al mismo tiempo.
El método específico es dejar que el segundo hilo espere hasta que el primer hilo regrese de la función malloc. Luego, permita que entre el segundo hilo.
Entra. (La sincronización de subprocesos se analizará en detalle en los Capítulos 8 y 9. Obviamente, todo este trabajo adicional afectará el tiempo de ejecución de C/C++.
El rendimiento de la versión multiproceso.
La versión vinculada dinámicamente de la función de ejecución de C/C++ está escrita de manera más genérica para que pueda ser utilizada por todas las funciones de ejecución de C/C++.
Por lo tanto, solo hay una versión multiproceso. biblioteca Debido a que el tiempo de ejecución de C/C++ está en la DLL, la aplicación (archivo .exe) y el dll no necesitan contener el código de la función de la biblioteca de tiempo de ejecución de C/C++ y pueden ser más pequeños. Algunos Además, si Microsoft corrige algún error en la DLL del tiempo de ejecución de C/C++, la aplicación se solucionará automáticamente.
Como es de esperar, el código de inicio del tiempo de ejecución de C/C++ es el hilo principal. del programa asigna e inicializa un bloque de datos. De esta manera, el hilo principal puede llamar de forma segura a cualquier función de biblioteca en ejecución de C/C++ cuando el hilo principal regresa de su función de entrada p>Espere, la función de biblioteca de tiempo de ejecución de C/C++ se liberará. el bloque de datos asociado Además, el código de inicio establece el código de manejo de excepciones estructurado correcto para que el hilo principal pueda llamar con éxito a la función de señal de C/C++.
6.7.1 Utilice _beginthreadex en lugar de CreateThread. para crear un hilo.
Puede que sientas curiosidad si llamas a CreateThread en lugar del _ startthreadex del tiempo de ejecución de C/C++ para crear un nuevo hilo. ¿Qué sucede cuando un hilo llama a una función de tiempo de ejecución de C/C++ que lo requiere? una estructura _tiddata, se producen las siguientes condiciones. (La mayoría de las funciones de la biblioteca de tiempo de ejecución de C/C++ son seguras para subprocesos y no requieren esta estructura). Primero, las funciones de la biblioteca de tiempo de ejecución de C/C++.
Intenta obtener la dirección del bloque de datos del hilo (llamando a TlsGetValue). Si se devuelve NULL como dirección del bloque _tiddata, la tabla
el hilo principal no tiene un bloque _tiddata asociado. En este momento, la función de la biblioteca de tiempo de ejecución de C/C++ asignará e inicializará el hilo de depuración principal.
Inicializar un bloque _tiddata. Este bloque luego se asociará con el subproceso (a través de TlsSetValue) y existirá y estará asociado con el subproceso mientras el subproceso esté ejecutándose. C/C++ ahora puede usar el bloque _tiddata del hilo para ejecutar funciones de biblioteca. Posteriormente,
también se puede utilizar cualquier función de biblioteca de tiempo de ejecución C/C++ llamada.
Por supuesto, esto es bastante tentador ya que los hilos pueden ejecutarse (casi) sin problemas. Pero todavía hay un problema. El primer problema es que si el hilo usa la función de señal del tiempo de ejecución de C/C++, todo el proceso terminará debido a una excepción estructural.
El marco de fotos SEH aún no está listo. El segundo problema es que si el hilo no finaliza llamando a _endthreadex, el bloque de datos no se puede destruir, lo que provoca una pérdida de memoria. (¿Quién llamará al hilo creado con la función CreateThread?
_endthreadex?)