ARM学习(38)ARM学习(38)多进程多线程之间的通信方式
一、问题背景
笔者在调试模拟器的时候,碰到进程间通信的问题,一个进程在等另外一个进程ready的时候,迟迟等不到,然后通过调试发现,另外一个进程变量已经变化了,但是当前进程变量没变化,需要了解进程间通信的方式。
如下图所示,子进程1 迟迟无法读到子进程2的变量数据?
通过这个问题,我们可以研究一下以下这些问题:
- 进程的创建方式,类比线程的创建方式,一些子进程的使用方式
- 进程间的通信方式,以及类比线程间的通信方式,
- 进程间锁与线程间锁的方式
二、进程与线程创建
线程创建和使用demo基于Windows,创建线程使用CreateThread函数,原型声明如下:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD
SIZE_T dwStackSize,//initialstacksize
LPTHREAD_START_ROUTINE lpStartAddress,//threadfunction
LPVOID lpParameter,//threadargument
DWORD dwCreationFlags,//creationoption
LPDWORD lpThreadId//threadidentifier
)
参数说明:
- lpThreadAttributes:指向SECURITY_ATTRIBUTES型态的结构的指针。在Windows 98中忽略该参数。在Windows NT中,NULL使用默认安全性,不可以被子线程继承,否则需要定义一个结构体将它的bInheritHandle成员初始化为TRUE
dwStackSize,设置初始栈的大小,以字节为单位,如果为0,那么默认将使用与调用该函数的线程相同的栈空间大小。任何情况下,Windows根据需要动态延长堆栈的大小。 - lpStartAddress,指向线程函数的指针,形式:@函数名,函数名称没有限制,
线程有两种声明方式
(1)DWORD WINAPI 函数名 (LPVOID lpParam); //标准格式
DWORD WINAPI 函数名 (LPVOID lpParam)
CreateThread(NULL, 0, 函数名, 0, 0, 0);
(2)void 函数名();
使用void 函数名()此种线程声明方式时,lpStartAddress需要加入LPTHREAD_START_ROUTINE转换,如
void 函数名()
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)函数名, 0, 0, 0); - lpParameter:向线程函数传递的参数,是一个指向结构的指针,不需传递参数时,为NULL。
- dwCreationFlags :线程标志,可取值如下
(1)CREATE_SUSPENDED(0x00000004):创建一个挂起的线程,
(2)0:表示创建后立即激活。
(3)STACK_SIZE_PARAM_IS_A_RESERVATION(0x00010000):dwStackSize参数指定初始的保留堆栈 的大小,否则,dwStackSize指定提交的大小。该标记值在Windows 2000/NT and Windows Me/98/95上不支持。 - lpThreadId:保存新线程的id。
返回值:函数成功,返回线程句柄;函数失败返回false。若不想返回线程ID,设置值为NULL。
基于CreateThread创建线程的例子如下图所示,也比较简单,两个线程申请内存就打印内存的地址。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#define SIZE 512 * 1024 * 1024 // 1 GB
DWORD WINAPI thread_func(LPVOID arg) {
// 申请1GB空间
unsigned char* buffer = (unsigned char*)malloc(SIZE);
if (buffer == NULL) {
perror("malloc failed");
return 1;
}
// 将内存清空
memset(buffer, 0, SIZE);
// 打印该线程的内存地址(可选)
printf("Thread allocated memory at %p\n", (void*)buffer);
// 返回指向内存的指针
free(buffer);
return 0;
}
int thread_vs() {
HANDLE thread1, thread2;
// 创建两个线程
thread1 = CreateThread(NULL, 0, thread_func, NULL, 0, NULL);
thread2 = CreateThread(NULL, 0, thread_func, NULL, 0, NULL);
// 等待两个线程完成
WaitForSingleObject(thread1, INFINITE);
WaitForSingleObject(thread2, INFINITE);
// 打印验证信息
printf("Memory allocation and cleanup complete.\n");
// 关闭线程句柄
CloseHandle(thread1);
CloseHandle(thread2);
return 0;
}
程序结果如下:
进程创建和使用demo
进程创建在windows上面使用CreateProcess,其函数声明如下:
BOOL CreateProcess
(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATIONlpProcessInformation
);
- lpApplicationName
指向一个NULL结尾的、用来指定可执行模块的字符串。
这个字符串可以是可执行模块的绝对路径,也可以是相对路径,在后一种情况下,函数使用当前驱动器和目录建立可执行模块的路径。
这个参数可以被设为NULL,在这种情况下,可执行模块的名字必须处于 lpCommandLine 参数最前面并由空格符与后面的字符分开。 - lpCommandLine
指向一个以NULL结尾的字符串,该字符串指定要执行的命令行。
这个参数可以为空,那么函数将使用lpApplicationName参数指定的字符串当做要运行的程序的命令行。
如果lpApplicationName和lpCommandLine参数都不为空,那么lpApplicationName参数指定将要被运行的模块,lpCommandLine参数指定将被运行的模块的命令行。新运行的进程可以使用GetCommandLine函数获得整个命令行。C语言程序可以使用argc和argv参数。 - lpProcessAttributes
指向一个SECURITY_ATTRIBUTES结构体,这个结构体决定是否返回的句柄可以被子进程继承。如果lpProcessAttributes参数为空(NULL),那么句柄不能被继承。
在Windows NT中:SECURITY_ATTRIBUTES结构的lpSecurityDescriptor成员指定了新进程的安全描述符,如果参数为空,新进程使用默认的安全描述符。 - lpThreadAttributes
同lpProcessAttribute,不过这个参数决定的是线程是否被继承.通常置为NULL. - bInheritHandles
指示新进程是否从调用进程处继承了句柄。
如果参数的值为真,调用进程中的每一个可继承的打开句柄都将被子进程继承。被继承的句柄与原进程拥有完全相同的值和访问权限。 - dwCreationFlags
指定附加的、用来控制优先类和进程的创建的标志。以下的创建标志可以以除下面列出的方式外的任何方式组合后指定。
(1)值:CREATE_DEFAULT_ERROR_MODE
含义:新的进程不继承调用进程的错误模式。CreateProcess函数赋予新进程当前的默认错误模式作为替代。应用程序可以调用SetErrorMode函数设置当前的默认错误模式。
这个标志对于那些运行在没有硬件错误环境下的多线程外壳程序是十分有用的。
对于CreateProcess函数,默认的行为是为新进程继承调用者的错误模式。设置这个标志以改变默认的处理方式。
(2)值:CREATE_NEW_CONSOLE
含义:新的进程将使用一个新的控制台,而不是继承父进程的控制台。这个标志不能与DETACHED_PROCESS标志一起使用。
(3)值:CREATE_NEW_PROCESS_GROUP
含义:新进程将是一个进程树的根进程。进程树中的全部进程都是根进程的子进程。新进程树的用户标识符与这个进程的标识符是相同的,由lpProcessInformation参数返回。进程树经常使用GenerateConsoleCtrlEvent函数允许发送CTRL+C或CTRL+BREAK信号到一组控制台进程。
(4)值:CREATE_SEPARATE_WOW_VDM
如果被设置,新进程将会在一个私有的虚拟DOS机(VDM)中运行。另外,默认情况下所有的16位Windows应用程序都会在同一个共享的VDM中以线程的方式运行。单独运行一个16位程序的优点是一个应用程序的崩溃只会结束这一个VDM的运行;其他那些在不同VDM中运行的程序会继续正常的运行。同样的,在不同VDM中运行的16位Windows应用程序拥有不同的输入队列,这意味着如果一个程序暂时失去响应,在独立的VDM中的应用程序能够继续获得输入。
(5)值:CREATE_SHARED_WOW_VDM
如果WIN.INI中的Windows段的DefaultSeparateVDM选项被设置为真,这个标识使得CreateProcess函数越过这个选项并在共享的虚拟DOS机中运行新进程。
(6)值:CREATE_SUSPENDED
含义:新进程的主线程会以暂停的状态被创建,直到调用ResumeThread函数被调用时才运行。
(7)值:CREATE_UNICODE_ENVIRONMENT
含义:如果被设置,由lpEnvironment参数指定的环境块使用Unicode字符,如果为空,环境块使用ANSI字符。
(8)值:DEBUG_PROCESS
含义:如果这个标志被设置,调用进程将被当做一个调试程序,并且新进程会被当做被调试的进程。系统把被调试程序发生的所有调试事件通知给调试器。
如果你使用这个标志创建进程,只有调用进程(调用CreateProcess函数的进程)可以调用WaitForDebugEvent函数。
(9)值:DEBUG_ONLY_THIS_PROCESS
含义:如果此标志没有被设置且调用进程正在被调试,新进程将成为调试调用进程的调试器的另一个调试对象。如果调用进程没有被调试,有关调试的行为就不会产生。
(10)值:DETACHED_PROCESS
含义:对于控制台进程,新进程没有访问父进程控制台的权限。新进程可以通过AllocConsole函数自己创建一个新的控制台。这个标志不可以与CREATE_NEW_CONSOLE标志一起使用。
(11)值:CREATE_NO_WINDOW
含义:系统不为新进程创建CUI窗口,使用该标志可以创建不含窗口的CUI程序。
dwCreationFlags还用来控制新进程的优先类,优先类用来决定此进程的线程调度的优先级。如果下面的优先级类标志都没有被指定,那么默认的优先类是NORMAL_PRIORITY_CLASS,除非被创建的进程是IDLE_PRIORITY_CLASS。在这种情况下子进程的默认优先类是IDLE_PRIORITY_CLASS。
可以选择下面的标志中的一个:
优先级:HIGH_PRIORITY_CLASS
含义:指示这个进程将执行时间临界的任务,所以它必须被立即运行以保证正确。这个优先级的程序优先于正常优先级或空闲优先级的程序。一个例子是Windows任务列表,为了保证当用户调用时可以立刻响应,放弃了对系统负荷的考虑。确保在使用高优先级时应该足够谨慎,因为一个高优先级的CPU关联应用程序可以占用几乎全部的CPU可用时间。
优先级:IDLE_PRIORITY_CLASS
含义:指示这个进程的线程只有在系统空闲时才会运行并且可以被任何高优先级的任务打断。例如屏幕保护程序。空闲优先级会被子进程继承。
优先级:NORMAL_PRIORITY_CLASS
含义:指示这个进程没有特殊的任务调度要求。
优先级:REALTIME_PRIORITY_CLASS
含义:指示这个进程拥有可用的最高优先级。一个拥有实时优先级的进程的线程可以打断所有其他进程线程的执行,包括正在执行重要任务的系统进程。例如,一个执行时间稍长一点的实时进程可能导致磁盘缓存不足或鼠标反映迟钝。 - lpEnvironment
指向一个新进程的环境块。如果此参数为空,新进程使用调用进程的环境。
一个环境块存在于一个由以NULL结尾的字符串组成的块中,这个块也是以NULL结尾的。每个字符串都是name=value的形式。
因为相等标志被当做分隔符,所以它不能被环境变量当做变量名。
与其使用应用程序提供的环境块,不如直接把这个参数设为空,系统驱动器上的当前目录信息不会被自动传递给新创建的进程。对于这个情况的探讨和如何处理,请参见注释一节。
环境块可以包含Unicode或ANSI字符。如果lpEnvironment指向的环境块包含Unicode字符,那么dwCreationFlags字段的CREATE_UNICODE_ENⅥRONMENT标志将被设置。如果块包含ANSI字符,该标志将被清空。
请注意一个ANSI环境块是由两个零字节结束的:一个是字符串的结尾,另一个用来结束这个块。一个Unicode环境块是由四个零字节结束的:两个代表字符串结束,另两个用来结束块。 - lpCurrentDirectory
指向一个以NULL结尾的字符串,这个字符串用来指定子进程的工作路径。这个字符串必须是一个包含驱动器名的绝对路径。如果这个参数为空,新进程将使用与调用进程相同的驱动器和目录。这个选项是一个需要启动应用程序并指定它们的驱动器和工作目录的外壳程序的主要条件。 - lpStartupInfo
指向一个用于决定新进程的主窗体如何显示的STARTUPINFO结构体。 - lpProcessInformation
指向一个用来接收新进程的识别信息的PROCESS_INFORMATION结构体。
其创建的demo如下所示,
- 创建一个父进程,打印shared_data 初始化为12,之后修改为42,创建子进程,然后修改为99。由于没有使用共享内存变量,所以子进程的初始化值还是12,后续为99。
- 父进程和子进程变量的地址一样,但是都是虚拟地址,所以无法同时共享。
#include <windows.h>
#include <stdio.h>
volatile int sharedData = 12;
void share_func() {
// 输出共享内存中的数据
printf("Shared data in child process before modification: %d addr=%p\n", sharedData, &sharedData );
// 修改共享内存中的数据
sharedData = 99;
printf("Shared data in child process after modification: %d addr=%p\n", sharedData, &sharedData);
}
CONTEXT context_g;
int main() {
// 输出父进程信息
printf("Parent process started. Shared data: %d addr=%p\n", sharedData, &sharedData);
// 初始化共享内存
sharedData = 42; // 给共享内存中的数据赋初值
// 创建子进程
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi;
char file_path[1024];
GetModuleFileName(NULL, file_path, 1024);
// 创建子进程,使用 CREATE_SUSPENDED 来暂停子进程
if (!CreateProcess(
file_path, // 可执行文件名
NULL, // 子进程程序名(此处可以与父进程名称相同)
NULL, // 安全属性
NULL, // 继承属性
FALSE, // 不继承句柄
CREATE_SUSPENDED, // 创建子进程并暂停,方便后续操作
NULL, // 环境变量
NULL, // 当前目录
&si, // 启动信息
&pi // 进程信息
)) {
printf("CreateProcess failed: %d\n", GetLastError());
return 1;
}
// 输出父进程和共享内存中的数据
printf("Shared data in parent process: %d addr=%p\n", sharedData, &sharedData);
// 获取子进程的上下文
context_g.ContextFlags = CONTEXT_FULL;
if (!GetThreadContext(pi.hThread, &context_g)) {
printf("GetThreadContext failed: %d\n", GetLastError());
return 1;
}
typedef void(*ParentFunc)();
ParentFunc parentFunc = share_func;
// 设置子进程的上下文,使其执行父进程的函数
context_g.Eip = (DWORD)parentFunc; // 修改 EIP 寄存器为父进程的函数地址
// 设置子进程上下文
if (!SetThreadContext(pi.hThread, &context_g)) {
printf("SetThreadContext failed: %d\n", GetLastError());
return 1;
}
// 启动子进程
ResumeThread(pi.hThread);
// 等待子进程完成
WaitForSingleObject(pi.hProcess, INFINITE);
// 清理
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
三、进程间通信方式与线程间通信方式
3.1 线程间通信的方式
线程间通信的方式比较多,比如事件,消息队列,互斥锁等等
线程间通信方式demo1,通过事件进行任务间通信。
- 线程1做任务,做完任务后,事件置上,
- 线程2等待事件之后,在继续完成任务,
#include <windows.h>
#include <stdio.h>
HANDLE hEvent; // 事件句柄
// 线程2,等待线程1准备好后继续执行
DWORD WINAPI Thread2(LPVOID lpParam) {
printf("Thread 2: Waiting for thread 1 to be ready...\n");
// 等待线程1设置事件
WaitForSingleObject(hEvent, INFINITE);
printf("Thread 2: Received signal from thread 1, proceeding...\n");
return 0;
}
// 线程1,完成准备工作并通知线程2
DWORD WINAPI Thread1(LPVOID lpParam) {
printf("Thread 1: Starting some work...\n");
// 模拟线程1的一些工作
Sleep(3000); // 假设线程1需要3秒钟来准备
printf("Thread 1: Finished work, setting event...\n");
// 设置事件,通知线程2可以继续
SetEvent(hEvent);
return 0;
}
int main() {
// 创建一个手动重置的事件对象
hEvent = CreateEvent(NULL, // 默认安全性
TRUE, // 手动重置
FALSE, // 初始状态为未信号
NULL); // 无名字
if (hEvent == NULL) {
printf("CreateEvent failed (%d)\n", GetLastError());
return 1;
}
// 创建线程1
HANDLE hThread1 = CreateThread(NULL, 0, Thread1, NULL, 0, NULL);
if (hThread1 == NULL) {
printf("CreateThread failed (%d)\n", GetLastError());
return 1;
}
// 创建线程2
HANDLE hThread2 = CreateThread(NULL, 0, Thread2, NULL, 0, NULL);
if (hThread2 == NULL) {
printf("CreateThread failed (%d)\n", GetLastError());
return 1;
}
// 等待两个线程结束
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
// 清理
CloseHandle(hEvent);
CloseHandle(hThread1);
CloseHandle(hThread2);
return 0;
}
3.2 进程间通信方式
windows VS支持data_seg 定义共享内存段,创建的子进程可以访问到共享段内的数据。
进程间通信方式demo代码如下所示:
- 两种方式定义共享段,一种是通过在data_seg的范围内定义变量,可将其定义到共享段内,另外一种是通过__declspec(allocate(“段名字”))将其放置到已经定义好的段内。
- 父进程定义共享内存变量,初始化为12,并修改为42,
- 子进程如果开始值为12,则无法访问到共享内存,如果开始值为42,则通信正常
#include <windows.h>
#include <stdio.h>
#pragma data_seg("shared") // 定义共享内存段
volatile int sharedData = 12;
#pragma data_seg() // 结束共享内存段
#pragma comment(linker, "/SECTION:shared,rws") // 共享数据段,读写共享
//__declspec(allocate("shared")) volatile int sharedData = 12; // 声明共享内存数据
void share_func() {
// 输出共享内存中的数据
printf("Shared data in child process before modification: %d addr=%p\n", sharedData, &sharedData);
// 修改共享内存中的数据
sharedData = 99;
printf("Shared data in child process after modification: %d addr=%p\n", sharedData, &sharedData);
}
// 父进程暴露的函数
__declspec(dllexport) void parentFunction() {
printf("This is a function from the parent process!\n");
}
CONTEXT context_g;
int main() {
// 输出父进程信息
printf("Parent process started. Shared data: %d addr=%p\n", sharedData, &sharedData);
// 初始化共享内存
sharedData = 42; // 给共享内存中的数据赋初值
// 创建子进程
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi;
char file_path[1024];
GetModuleFileName(NULL, file_path, 1024);
// 创建子进程,使用 CREATE_SUSPENDED 来暂停子进程
if (!CreateProcess(
file_path, // 可执行文件名
NULL, // 子进程程序名(此处可以与父进程名称相同)
NULL, // 安全属性
NULL, // 继承属性
FALSE, // 不继承句柄
CREATE_SUSPENDED, // 创建子进程并暂停,方便后续操作
NULL, // 环境变量
NULL, // 当前目录
&si, // 启动信息
&pi // 进程信息
)) {
printf("CreateProcess failed: %d\n", GetLastError());
return 1;
}
// 输出父进程和共享内存中的数据
printf("Shared data in parent process: %d addr=%p\n", sharedData, &sharedData);
// 获取子进程的上下文
context_g.ContextFlags = CONTEXT_FULL;
if (!GetThreadContext(pi.hThread, &context_g)) {
printf("GetThreadContext failed: %d\n", GetLastError());
return 1;
}
typedef void(*ParentFunc)();
ParentFunc parentFunc = share_func;
// 设置子进程的上下文,使其执行父进程的函数
context_g.Eip = (DWORD)parentFunc; // 修改 EIP 寄存器为父进程的函数地址
// 设置子进程上下文
if (!SetThreadContext(pi.hThread, &context_g)) {
printf("SetThreadContext failed: %d\n", GetLastError());
return 1;
}
// 启动子进程
ResumeThread(pi.hThread);
// 等待子进程完成
WaitForSingleObject(pi.hProcess, INFINITE);
// 清理
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
通过这种方式只有VC编译器支持,MingW32 GCC不支持这种方式,这也解释了开头笔者的问题,两个进程无法通信,如果要进程间通信,还得如下这种方式,使用CreateFileMapping放松。
通过CreateFileMapping创建的共享内存可以进行进程间通信。CreateFileMapping相当于创建一块内存,这块内存是多个进程共享数据。
#include <windows.h>
#include <stdio.h>
#define SHARED_MEMORY_NAME "Global\\MySharedMemory"
int* sharedData = NULL;
void share_func() {
// 输出共享内存中的数据
printf("Shared data in child process before modification: %d addr=%p\n", *sharedData, sharedData);
// 修改共享内存中的数据
*sharedData = 99;
printf("Shared data in child process after modification: %d addr=%p\n", *sharedData, sharedData);
}
CONTEXT context_g;
int main() {
// 创建共享内存
SECURITY_ATTRIBUTES sa = { 0 };
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL;
HANDLE hMapFile = CreateFileMapping(
INVALID_HANDLE_VALUE, // 使用系统虚拟内存
&sa, // 默认安全性
PAGE_READWRITE, // 可读写
0, // 高 32 位
sizeof(int), // 映射的大小
SHARED_MEMORY_NAME // 共享内存的名字
);
if (hMapFile == NULL) {
printf("CreateFileMapping failed (%d)\n", GetLastError());
return 1;
}
// 映射共享内存
int* pSharedMemory = (int*)MapViewOfFile(
hMapFile, // 共享内存的句柄
FILE_MAP_ALL_ACCESS, // 可读写权限
0, // 文件偏移
0, // 文件偏移
sizeof(int) // 映射的大小
);
if (pSharedMemory == NULL) {
printf("MapViewOfFile failed (%d)\n", GetLastError());
CloseHandle(hMapFile);
return 1;
}
*sharedData = 12; // 给共享内存中的数据赋初值
// 输出父进程信息
printf("Parent process started. Shared data: %d addr=%p\n", *sharedData, sharedData);
sharedData = pSharedMemory;
// 初始化共享内存
*sharedData = 42; // 给共享内存中的数据赋初值
// 创建子进程
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi;
char file_path[1024];
GetModuleFileName(NULL, (LPSTR)file_path, 1024);
// 创建子进程,使用 CREATE_SUSPENDED 来暂停子进程
if (!CreateProcess(
(LPSTR)file_path, // 可执行文件名
NULL, // 子进程程序名(此处可以与父进程名称相同)
NULL, // 安全属性
NULL, // 继承属性
FALSE, // 不继承句柄
CREATE_SUSPENDED, // 创建子进程并暂停,方便后续操作
NULL, // 环境变量
NULL, // 当前目录
&si, // 启动信息
&pi // 进程信息
)) {
printf("CreateProcess failed: %d\n", GetLastError());
return 1;
}
// 输出父进程和共享内存中的数据
printf("Shared data in parent process: %d addr=%p\n", *sharedData, sharedData);
// 获取子进程的上下文
context_g.ContextFlags = CONTEXT_FULL;
if (!GetThreadContext(pi.hThread, &context_g)) {
printf("GetThreadContext failed: %d\n", GetLastError());
return 1;
}
typedef void(*ParentFunc)();
ParentFunc parentFunc = share_func;
// 设置子进程的上下文,使其执行父进程的函数
context_g.Eip = (DWORD)parentFunc; // 修改 EIP 寄存器为父进程的函数地址
// 设置子进程上下文
if (!SetThreadContext(pi.hThread, &context_g)) {
printf("SetThreadContext failed: %d\n", GetLastError());
return 1;
}
// 启动子进程
ResumeThread(pi.hThread);
// 等待子进程完成
WaitForSingleObject(pi.hProcess, INFINITE);
// 清理
UnmapViewOfFile(pSharedMemory);
CloseHandle(hMapFile);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}