【笔记&代码】 多核编程实战Multicore Application Programming For Windows

【笔记&代码】 多核编程实战Multicore Application Programming For Windows_第1张图片

英文版下载地址

1、创建线程

CreateThread的缺陷:

调用CreateThread()让系统生成一个新线程,但并不设置线程使用开发者环境提供的库进行工作。
Windows只生成线程并返回句柄,但没机会建立起需要的线程本地数据结构。 库将在首次被调用时创建需要的结构,但并非所有的库能做到这一点。
因此,建议使用_beginthreadex()做替代。

_beginthreadex()的参数:

void* 指向线程安全属性的指针 unsigned int
线程栈大小 unsigned int()(void) 函数地址 void*
入参指针 unsigned int 是否以挂起状态创建 unsigned int*
指向用于写入线程ID的变量的可选指针
注意:
1、_beginthreadex()返回的句柄必须调用CloseHandle()关闭;
2、_beginthread()的缺陷是,如果线程终止,句柄将无效或被重用,容易乱。

例-创建线程的三种不同方式:

#include 
#include 
#include 

DWORD WINAPI mywork1(__int LPVOID lpParameter)
{
    printf("CreateThread thread %i\n", GetCurrentThreadId());
    return 0;
}

unsigned int __stdcall mywork2(void* data)
{
    printf("_beginthreadex thread %i\n", GetCurrentThreadId());
    return 0;
}

void mywork3(void* data)
{
    printf("_beginthread thread %i\n", GetCurrentThreadId());
}

int main(int argc, _TCHAR* argv[])
{
    HANDLE h1, h2, h3;
    h1 = CreateThread(0, 0, mywork1, 0, 0, 0);

    //good
    h2 = (HANDLE)_beginthreadex(0, 0, &mywork2, 0, 0, 0);
    WaitForSingleObject(h2, INFINITE);
    CloseHandle(h2);

    h3 = (HANDLE)_beginthread(&mywork3, 0, 0);

    getchar();
    return 0;
}//end main

例-线程句柄可能被重用:

#include 
#include 

void mywork1(void* data)
{//很快终止
}

void mywork2(void* data)
{
    for(volatile int i = 0; i < 100000; i++)
    {}//volatile类型的i,多数编译器不会删除循环
}

int main(int argc, _TCHAR* argv[])
{
    HANDLE h1, h2;
    h1 = (HANDLE)_beginthread(&mywork3, 0, 0);
    h2 = (HANDLE)_beginthread(&mywork3, 0, 0);
    WaitForSingleObject(h1, INFINITE);
    WaitForSingleObject(h2, INFINITE);
    return 0;
}//end main

2、终止线程

可调用ExitThread()和TerminateThread()迫使线程退出,但不建议。
这可能使程序处于不确定状态,且线程在持有互斥量后不会有机会释放互斥量或任何分配给它的其他资源。
此外,也不会给运行库机会来清除已经分配给线程的资源。
确保线程获得的资源被恰当释放,就可用。
如果线程调用_endthreadex()终止,还必须再调用CloseHandle()关闭线程句柄。

例-使用WaitForSingleObject函数【fork-join模式】:

unsigned int __stdcall mywork(void* data)
{
    printf("thread %i\n", GetCurrentThreadId());
    return 0;
}

int main(int argc, _TCHAR* argv[])
{
    HANDLE h[2];
    //fork
    for(int i = 0; i < 2; i++)
    {
        h[i] = (HANDLE)_beginthreadex(0, 0, &mywork, 0, 0, 0);
    }
    //join
    for(int i = 0; i < 2; i++)
    {
        WaitForSingleObject(h[i], INFINITE);
        CloseHandle(h[i]);
    }
    getchar();
    return 0;
}//end main

例-使用WaitForMultipleObjects函数:

HANDLE h[2];
for(int i = 0; i < 2; i++)
{
    h[i] = (HANDLE)_beginthreadex(0, 0, &mywork, 0, 0, 0);
}
WaitForMultipleObjects(2        // 要等待的线程数 
                      ,h        // 句柄数组指针
                      ,true     // ture,等待全部;false,只等一个。
                      ,INFINITE);
for(int i = 0; i < 2; i++)
{
    CloseHandle(h[i]);
}
getchar();
return 0;

3、挂起线程

挂起:是指现在并未运行的线程。可在创建时挂起。
SuspendThread()挂起线程,ResumeThread()恢复挂起。
注意:挂起线程时,若正好线程正在持有互斥量资源,可能会有问题。

例-创建并挂起线程:

#include 
#include 

unsigned int __stdcall mywork(void* data)
{
    printf("Thread %i\n", GetCurrentThreadId());
    return 0;
}

int _tmian(int argc, _TCHAR* argv[])
{
    HANDLE h;
    //创建并挂起线程
    h = (HANDLE)_beginthreadex(0, 0, &mywork, 0, CREATE_SUSPENDED, 0);
    getchar();
    //恢复被挂起线程
    ResumeThread(h);
    getchar();
    WaitForSingleObject(h, INFINITE);
    CloseHandle(h);
    return 0;
}

4、内核资源

使用内核资源的句柄:

返回句柄的WinAPI调用是在内核空间创建某个资源,句柄只是此资源的编号。 进程可共享有句柄的资源。
内核资源的句柄仅对有权限访问这一资源的进程有意义。 将句柄的值传递给另一个进程,这并不能使该进程获得对资源的访问权限;
内核必须使新进程获得对资源的访问权限并在新进程中提供现存资源的新句柄。

自Win NT4以来,Windows使用UTF-16格式,称为宽字符编码,每个字符占用两个字节。

wchar_t mystring1 = L"Some text";
WCHAR mystring2 = TEXT("More text");

//根据是否定义UNICODE来判断使用ANSCII还是UTF-16
TCHAR mystring = _T("The Good Way!");   

5、同步和资源共享的方式

同步和资源共享的方式:

1、互斥锁(进程 & 线程),Win Vista中引入;
2、临界区(只用于线程);
3、轻量级读写锁(只用于线程), Win7中引入;
4、信号量;
5、条件变量;
6、事件。


5.1 、临界区:

注意:
1、不能在进程间使用,因此性能开销较低(内核资源性能开销较高);
2、临界区无需超时值,调用非阻塞;
3、无法进入临界区时,可以让线程进入休眠期旋转多次迭代的功能。
性能:
1、线程休眠后再唤醒非常耗时,因为涉及进入内核——临界区要快进快出;
2、使用TryEnterCriticalSection()时,若没拿到锁不再休眠,立即返回;
3、旋转线程会占用更多CPU事件,可能使正在临界区的线程拿不到CPU时间。

例-使用临界区保护对变量的访问:

volatile int counter = 0;
CRITICAL_SETION critical;

unsigned int __stdcall test(void*)
{
    while(counter < 100)
    {
        //进入临界区
        EnterCriticalSection(&critical);
        int number = counter++;
        //退出临界区
        LeaveCriticalSection(&critical);

        printf("Thread %i; value=%i, is prime = %i\n"
            , GetCurrentThreadId(), number, isprime(number));
    }
    return 0;
}//end test

int _tmian(int argc, _TCHAR* argv[])
{
    HANDLE h1, h2;
    //初始化临界区
    InitializeCriticalSection(&critical);
    h1 = (HANDLE)_beginthreadex(0, 0, &test, 0, (void*)0, 0, 0);
    h2 = (HANDLE)_beginthreadex(0, 0, &test, 0, (void*)1, 0, 0);
    WaitForSingleObject(h1, INFINITE);
    WaitForSingleObject(h2, INFINITE);
    CloseHandle(h1);
    CloseHandle(h2);
    getchar();
    //删除临界区
    DeleteCriticalSection(&critical);
    return 0;
}

例-使用TryEnterCriticalSection()避免调用线程休眠:

while(counter < 100)
{
    //一直等待,直到获得锁。
    //注意:旋转线程会占用更多CPU时间,可能使正在临界区的线程更难拿到CPU时间。
    while(!TryEnterCriticalSection(&critical)){}

    int number = counter++;
    //退出临界区
    LeaveCriticalSection(&critical);

    printf("Thread %i; value=%i, is prime = %i\n"
        , GetCurrentThreadId(), number, isprime(number));
}

例-面向临界区设定旋转计数的方法:

//初始化和设置临界区旋转计数次数为1000次
InitializeCriticalSectionAndSpinCount(&critical, 1000);
SetCriticalSectionSpinCount(&critical, 1000);

5.2、 互斥锁:

注意:
1、可指定超时时间,超时后将返回失败;
2、多个线程等待锁时,无法确保等待线程获取互斥量的顺序;
3、可在进程间共享。

CreateMutex函数:

HANDLE CreateMutex(   
    LPSECURITY_ATTRIBUTES   lpMutexAttributes,   //安全属性结构指针
    BOOL                    bInitialOwner,      //是否占有该互斥量  
    LPCTSTR                 lpName              //设置互斥对象的名字   
);

例-获取和释放互斥量:

volatile int counter = 0;
HANDLE mutex;

unsigned int __stdcall test(void*)
{
    while(counter < 100)
    {
        //等待互斥量
        WaitForSingleObject(mutex, INFINITE);
        int number = counter++;
        //释放互斥量
        ReleaseMutex(mutex);

        printf("Thread %i; value=%i, is prime = %i\n"
            , GetCurrentThreadId(), number, isprime(number));
    }
    return 0;
}//end test

int _tmian(int argc, _TCHAR* argv[])
{
    HANDLE h1, h2;
    //创建互斥量
    mutex = CreateMutex(0,0,0);
    h1 = (HANDLE)_beginthreadex(0, 0, &test, 0, (void*)0, 0, 0);
    h2 = (HANDLE)_beginthreadex(0, 0, &test, 0, (void*)1, 0, 0);
    WaitForSingleObject(h1, INFINITE);
    WaitForSingleObject(h2, INFINITE);
    CloseHandle(h1);
    CloseHandle(h2);
    getchar();
    //删除互斥量
    CloseHandle(mutex);
    return 0;
}

5.3、 事件:

用于向一个或多个线程发出信号,表明某个事件已经发生。
手动重置:事件需要在其他线程再次等待该事件前被重置;
自动重置:让一个线程通过后自动重置。

注意:
1、若事件需要手动重置才能去除信号已发出状态,则调用ResetEvent()来实现;
2、若事件对象会自动重置,则在事件重置前只有一个线程将被释放。

例-用事件对象来强制实现执行顺序:

#include 
#include 
#include 

HANDLE hevent;

unsigned int __stdcall thread1(void* param)
{
    //等待事件
    WaitForSingleObject(hevent, INFINITE);
    printf("Thread 1 done\n");
    return 0;
}//end thread1

unsigned int __stdcall thread2(void* param)
{
    printf("Thread 2 done\n");
    //设置事件
    SetEvent(hevent);
    return 0;
}//end thread2

int _tmian(int argc, _TCHAR* argv[])
{
    HANDLE h1, h2;
    //创建事件对象
    hevent = CreateEvent(0,0,0,0);
    h1 = (HANDLE)_beginthreadex(0, 0, &thread1, 0, 0, 0);
    h2 = (HANDLE)_beginthreadex(0, 0, &thread2, 0, 0, 0);
    WaitForSingleObject(h1, INFINITE);
    WaitForSingleObject(h2, INFINITE);
    CloseHandle(h1);
    CloseHandle(h2);
    //关闭事件对象
    CloseHandle(hevent);
    getchar();
    return 0;
}//end _tmian

5.4、 轻量级读写锁

获取失败后立即返回的接口:
TryAcquireSRWLockExclusive();
TryAcquireSRWLockShared();

例-创建和使用轻量级读写锁:

#include 
#include 

int array[100][100];
SRWLOCK lock;

unsigned int __stdcall update(void* param)
{
    for(int y = 0; y < 100; y++)
        for(int x = 0; x < 100; x++)
        {
            AcquireSRWLockExclusive(&lock);
            array[x][y]++;
            array[y][x]--;
            ReleaseSRWLockExclusive(&lock);
        }//end for-x

    return 0;
}//end update

unsigned int __stdcall read(void* param)
{
    int value = 0;
    for(int y = 0; y < 100; y++)
        for(int x = 0; x < 100; x++)
        {
            AcquireSRWLockExclusive(&lock);
            value = array[x][y] + array[y][x];
            ReleaseSRWLockExclusive(&lock);
        }//end for-x

    printf("Value=%i\n", value);
    return 0;
}//end read

int _tmian(int argc, _TCHAR* argv[])
{
    HANDLE h1, h2;
    //初始化轻量级读写锁,非内核资源,无效对应删除。
    InitializeSRWLock(&lock);
    h1 = (HANDLE)_beginthreadex(0, 0, &test, 0, (void*)0, 0, 0);
    h2 = (HANDLE)_beginthreadex(0, 0, &test, 0, (void*)1, 0, 0);
    WaitForSingleObject(h1, INFINITE);
    WaitForSingleObject(h2, INFINITE);
    CloseHandle(h1);
    CloseHandle(h2);
    getchar();
    return 0;
}

6、创建进程

CreateProcess()的参数:

LPCWSTR                 进程名

LPWSTR                  命令行

LPSECURITY_ATTRIBUTES   指向子进程安全属性的指针

LPSECURITY_ATTRIBUTES   指向子进程第一个线程安全属性的指针

BOOL                    说明被创建进程是否继承调用进程句柄的布尔值

DWORD                   进程创建标志和进程优先级标志集,为可选参数。
                        这些标志控制被创建进程的各种特性,
                        如:进程是否有窗口;
                        主进程是否创建为挂起状态等。
                        优先级决定了给进程多少CPU份额。

LPVOID                  可选参数,指向新的运行时环境字符串集合的指针。
                        为空,则继承调用进程的运行时环境。

LPCWSTR                 可选参数,指向包含进程当前目录的字符串的指针。
                        为空,则继承调用进程的运行时目录。

LPSTARTUPINFOW          指向STARTUPINFO结构的指针

LPPROCESS_INFORMATION   指向PROCESS_INFORMATION结构的指针

例-启动一个新进程:

#include 

int _tmian(int argc, _TCHAR* argv[])
{
    STARTUPINFO startup_info;
    PROCESS_INFORMATION process_info;

    if(argc > 1)
    {
        _tprintf(_T("Argument %s\n"), argv[1]);
        _tprintf(_T("Starting child process\n"));

        //创建进程前必须如此
        //思考:为何多数API都如此设计,是否为了动态创建存储空间?
        ZeroMemory(&process_info, sizeof(process_info));
        ZeroMemory(&startup_info, sizeof(startup_info));
        startup_info.cb = sizeof(startup_info);

        //创建进程
        if(CreateProcess(argv[0], 0, 0, 0, 0, 0, 0, 0,
                &startup_info, &process_info) == 0)
        {
            _tprintf(_T("Error %i\n"), GetLastError());
        }
        WaitForSingleObject(process_info.hProcess, INFINITE);
    }else
    {
        _tprintf(_T("No arguments\n"));
    }

    getchar();
    return 0;
}

例-传参数给新进程:

#include 

int _tmian(int argc, _TCHAR* argv[])
{
    STARTUPINFO startup_info;
    PROCESS_INFORMATION process_info;

    if(argc == 1)
    {
        _tprintf(_T("No arguments given starting child process\n"));

        wchar_t argument[256];
        wsprintf(argument, L"\"%s\" Hello", argv[0]);

        //创建进程前必须如此
        ZeroMemory(&process_info, sizeof(process_info));
        ZeroMemory(&startup_info, sizeof(startup_info));
        startup_info.cb = sizeof(startup_info);

        //创建进程
        if(CreateProcess(argv[0], argument, 0, 0, 0, 0, 0, 0,
                &startup_info, &process_info) == 0)
        {
            _tprintf(_T("Error %i\n"), GetLastError());
        }
        WaitForSingleObject(process_info.hProcess, INFINITE);
    }else
    {
        _tprintf(_T("Argument %s\n"), argv[1]);
    }

    getchar();
    return 0;
}

6.1、进程间共享内存

CreateFileMapping()的参数:

HANDLE                  INVALID_HANDLE_VALUE,表明该调用应当创建共享内存。

LPSECURITY_ATTRIBUTES   可选参数,为指向子进程安全属性的指针,
                        表明此句柄是否可被子进程继承。

DWORD                   被创建的内存页保护属性。
                        对于共享内存,该参数很可能是读写访问的组合属性。

DWORD                   该区域大小的高位DWORD(无符号4字节整数)

DWORD                   该区域大小的低位DWORD

LPCTSTR                 映射对象的名字,为可选参数

MapViewOfFile()的参数:

HANDLE      来自CreateFileMapping()或OpenFileMapping()的句柄

DWORD       对内存的访问权限。
            对于需要读写共享内存的进程,此参数很可能是FILE_MAP_ALL_ACCESS

DWORD       共享内存偏移量的高位DWORD

DWORD       共享内存偏移量的低位DWORD

SIZE_T      共享内存区域的大小。此参数取0时共享内存大小与映射对象分配的大小相同。
注意:

对象可通过共享对象的句柄或公用名在进程间共享。
公用名可包含除反斜杠以外的任何字符,必须以标志符Global\或Local\开头。
全局命名空间由所有用户共享,而本地命名空间由每个用户私有。

CreateFileMapping()在内核中创建映射对象,但并不会实际映射对象到用户空间。
调用MapViewOfFile()使共享对象映射到内存,返回值是指向内存基址的指针。

例-创建和使用共享内存:

#include 
#include 

int _tmian(int argc, _TCHAR* argv[])
{
    STARTUPINFO startup_info;
    PROCESS_INFOMATION process_info;

    HANDLE filehandle;
    TCHAR ID[] = _T("Local\\SharedMemory");
    char* memory;

    if(argc == 1)
    {//父进程
        //创建映射对象
        filehandle = CreateFileMapping(INVALLD_HANDLE_VALUE,
                                        NULL, PAGE_READWRITE, 0, 1024, ID);
        //把共享对象映射到内存
        memory = (char*)MapViewOfFile(filehandle, FILE_MAP_ALL_ACCESS, 0, 0, 0);

        sprintf_s(memory, 1024, "%s", "Data from first process");
        printf("First process: %s\n", memory);

        ZeroMemory(&process_info, sizeof(process_info));
        ZeroMemory(&startup_info, sizeof(startup_info));
        startup_info.cb = sizeof(startup_info);

        wchar_t commandline[256];
        wsprintf(commandline, L"\"%s\" Child\n", argv[0]);

        CreateProcessW( argv[0], commandline, 0, 0, 0, 0, 0, 0,
                        &startup_info, &process_info );
        //子进程退出后,父进程就可以取消映射了
        WaitForSingleObject(process_info.hProcess, INFINITE);

        //取消映射并关闭句柄
        UnmapViewOfFile(memory);
        CloseHandle(filehandle);
    }else
    {//子进程
        //连接到父进程的共享内存区域
        filehandle = OpenFileMapping(FILE_MAP_ALL_ACCESS, 0, ID);
        //把共享对象映射到内存
        memory = (char*)MapViewOfFile(filehandle, FILE_MAP_ALL_ACCESS, 0, 0, 0);

        printf("Second process:%s\n", memory);

        //取消映射并关闭句柄
        UnmapViewOfFile(memory);
        CloseHandle(filehandle);
    }
    getchar();
    return 0;
}

6.2、在子进程中继承句柄

子进程可继承为父进程所有的资源的句柄。
这种情况下,这些句柄为相同值,但父进程需要将这些值传递给子进程,最简单的办法是通过命令行传递。
而且,句柄必须创建为可被子进程继承。

例-把共享内存的句柄传递给子进程:

#include 
#include 

int _tmian(int argc, _TCHAR* argv[])
{
    STARTUPINFO startup_info;
    PROCESS_INFOMATION process_info;

    SECURITY_ATTRIBUTES secat;

    HANDLE filehandle;
    TCHAR ID[] = _T("Local\\foo");
    wchar_t* memory;

    if (argc == 1)
    {//父进程
        secat.nLength = sizeof(secat);  //设置安全属性
        secat.bInheritHandle = true;    //使句柄可继承
        secat.lpSecurityDescriptor = NULL;

        filehandle = CreateFileMapping()
        //创建映射对象
        filehandle = CreateFileMapping(INVALLD_HANDLE_VALUE, &secat,
                                        PAGE_READWRITE, 0, 1024, ID);
        memory = (char*)MapViewOfFile(filehandle, FILE_MAP_ALL_ACCESS, 0, 0, 0);

        //用共享内存设置命令行
        wsprintf(memory, 1024, L"\"%s\" %i", argv[0], filehandle);
        printf("First process memory:%s handle:%i\n", memory, filehandle);

        ZeroMemory(&process_info, sizeof(process_info));
        ZeroMemory(&startup_info, sizeof(startup_info));
        startup_info.cb = sizeof(startup_info);

        //启动子进程
        CreateProcessW(NULL, memory, 0, 0, true, 0, 0, 0, &startup_info, &process_info);
        WaitForSingleObject(process_info.hProcess, INFINITE);

        UnmapViewOfFile(memory);
        CloseHandle(filehandle);
    }else
    {//子进程
        filehandle = (HANDLE)_wtoi(argv[1]);//从argv[1]获得句柄
        memory = (wchar_t*)MapViewOfFile(filehandle, FILE_MAP_ALL_ACCESS, 0, 0, 0);

        printf("Second process memory:%s handle:%i\n", memory, filehandle);

        UnmapViewOfFile(memory);
        CloseHandle(filehandle);
    }
    getchar();
    return 0;
}

6.3、互斥量命令及其在进程间的共享

注意:
1、只有一个进程可以创建互斥量;
2、互斥量的名称必须是唯一的,且必须传递给其他进程。

例-进程间共享互斥量:

#include 
#include 

int _tmian(int argc, _TCHAR* argv[])
{
    STARTUPINFO startup_info;
    PROCESS_INFOMATION process_info;

    HANDLE sharedmutex;

    ZeroMemory(&process_info, sizeof(process_info));
    ZeroMemory(&startup_info, sizeof(startup_info));
    startup_info.cb = sizeof(startup_info);

    sharedmutex = CreateMutex(0, 0, L"mymutex1234");
    if (GetLastError() != ERROR_ALREADY_EXISTS)
    {//互斥量已经存在
        if (CreateProcess( argv[0], 0, 0, 0, 0, 0, 0, 0,
                           &startup_info, &process_info) == 0)
        {
            printf("ERROR %i\n", GetLastError());
        }

        //等待新进程完成初始化,并等待用户输入
        WaitForInputIdle(process_info.hProcess, INFINITE);
    }

    //等待互斥量,并确保一次只有一个进程进入。但顺序不能控制。
    WaitForSingleObject(sharedmutex, INFINITE);

    for (int i = 0; i < 1000; i++)
    {
        printf("Process %i Count %i\n", GetCurrentProcessId(), i);
    }

    //释放互斥量
    ReleaseMutex(sharedmutex);
    CloseHandle(sharedmutex);

    getchar();
    return 0;
}

6.4、管道通信

定义 & 注意:

管道是流式通信的一种方式,一个线程写入管道的数据可由另一个线程读取。
Windows管道能实现进程内不同线程间、不同进程的线程间,甚至是不同系统上线程间的通信。

管道是一个字节流,所以多条消息可以连接起来。
处理传入消息的线程必须进行一个额外处理,
确保缓冲区中的所有信息都能在线程尝试读取下一组消息前获得处理。

CreateNamedPipe()的参数:

LPCWSTR                 管道名。需要符合"\\.\pipe\name"格式,其中name需替换为所需管道名。
                        本地机上的管道使用"\\."指定,名称的下一部分为"pipe\"声明。

DWORD                   管道访问模式。
                        创建可读写的管道:PIPE_ACCESS_DUPLEX;
                        创建只读的管道:PIPE_ACCESS_INBOUND;
                        创建只写的管道:PIPE_ACCESS_OUTBOUND;
                        成功的连接,只读和只写必须成对使用。

DWORD                   管道操作模式。可使管道传输数据流或信息流

DWORD                   管道允许的实例数。
                        PIPE_UNLIMITED_INSTANCES表明最大实例数仅受系统资源的限制

DWORD                   输出缓冲区的字节数  

DWORD                   输入缓冲区的字节数                

DWORD                   读或写的超时值。0值表示使用默认超时50ms

LPSECURITY_ATTRIBUTES   可选参数,指向安全属性的指针,在子进程应继承管道句柄时设定。

ReadFile()和WriteFile()的参数:

HANDLE          管道的句柄

LPVOID          调用ReadFile()时存储数据的缓冲区的地址;
                调用WriteFile()时发送数据的缓冲区的地址;

DWORD           ReadFile()对应的缓冲区大小或WriteFile()时所发送的数据量

LPDWORD         指向变量的指针,从该变量读取数据量或将写入数据量记录到该变量

LPOVERLAPPED    指向OVERLAPPED结构的指针,用此结构使函数调用立即返回,并允许读写处理在稍后完成。

例-使用匿名管道在两个线程间通信:

#include 
#include 

HANDLE readpipe, writepipe;

unsigned int __stdcall stage1(void* param)
{
    char buffer[200];//现实中,缓冲区不一定够用,有分批的可能性。
    DWORD length;
    for (int i = 0; i < 10; i++)
    {
        sprintf(buffer, "Text %i", i);
        WriteFile(writepipe, buffer, strlen(buffer)+1, &length, 0);
    }
    CloseHandle(writepipe);
    return 0;
}//end stage1

unsigned int __stdcall stage2(void* param)
{
    char buffer[200];//现实中,缓冲区不一定够用,有分批的可能性。
    DWORD length;
    while(ReadFile(readpipe, buffer, 200, &length, 0))
    {
        DWORD offset = 0;
        while(offset < length)
        {
            printf("%s\n", &buffer[offset]);
            offset += strlen(&buffer[offset]) + 1;
        }
    }
    CloseHandle(writepipe);
    return 0;
}//end stage2

int _tmian(int argc, _TCHAR* argv[])
{
    HANDLE thread1, thread2;
    CreatePipe(&readpipe, &writepipe, 0, 0);
    thread1 = (HANDLE)_beginthreadex(0, 0, &stage1, 0, 0, 0);
    thread2 = (HANDLE)_beginthreadex(0, 0, &stage2, 0, 0, 0);
    WaitForSingleObject(thread1, INFINITE);
    WaitForSingleObject(thread2, INFINITE);
    getchar();
    return 0;
}

6.5、套接字通信

例-客户端和服务端的套接字:

//包含Windows Sockets的头文件
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include 
#include 

#include 
#include 

#pragma comment(lib, "win2_32.lib")
HANDLE hEvent;

//主线程负责启动客户端线程和服务端线程
int _tmain(int argc, _TCHAR* argv[])
{
    HANDLE serverthread, clientthread;
    WSADATA wsaData;

    WSAStartup(MAKEWORD(2, 2), &wsaData);

    hEvent = CreateEvent(0, true, 0, 0);

    //创建线程
    serverthread = (HANDLE)_beginthreadex(0, 0, &server, 0, 0, 0);
    clientthread = (HANDLE)_beginthreadex(0, 0, &client, 0, 0, 0);

    //等待客户端退出
    WaitForSingleObject(clientthread, INFINITE);
    CloseHandle(clientthread);

    //关闭事件
    CloseHandle(hEvent);
    getchar();
    WSACleanup();
    return 0;
}//end _tmain

//客户端线程代码
unsigned int __stdcall client(void* data)
{
    SOCKET ConnectSocket = socket(AF_INET, SOCK_STREAM, 0);

    //等待服务器初始化完成
    WaitForSingleObject(hEvent, INFINITE);

    struct sockaddr_in server;
    ZeroMemory(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr("127.0.0.1");
    server.sin_port = 7780;

    //连接服务器
    connect(ConnectSocket, (struct sockaddr*)&server, sizeof(server));

    printf("Sending 'abcd' to server.\n");
    char buffer[1024];
    ZeroMemory(&buffer, sizeof(buffer));
    strncpy_s(buffer, 1024, "abcd", 5);

    //发送消息给服务器
    send(ConnectSocket, buffer, strlen(buffer)+1, 0);

    ZeroMemory(&buffer, sizeof(buffer));

    //从服务器接收消息
    recv(ConnectSocket, buffer, 1024, 0);

    printf("Got '%s' from server.\n", buffer);

    printf("Close client.\n");
    shutdown(ConnectSocket, SD_BOTH);
    closesocket(ConnectSocket);
    return 0;
}//end client

//服务端线程代码
unsigned int __stdcall server(void* data)
{
    SOCKET newsocket;
    SOCKET ServerSocket = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server;
    ZeroMemory(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = 7780;

    //绑定和监听
    bind(ServerSocket, (struct sockaddr*)&server, sizeof(server));
    listen(ServerSocket, SOMAXCONN);

    //通知客户端可以发送连接请求了
    SetEvent(hEvent);

    //接收连接请求
    //直到收到标志为INVALID_SOCKET的连接,才会在其他线程退出后清理退出
    while((newsocket = accept(ServerSocket, 0, 0)) != INVALID_SOCKET)
    {
        //收到请求,创建响应线程
        //服务器线程每次接受一个连接时会创建一个新线程,且新连接的标志会传递给新创建的线程。
        //即创建实际处理工作的线程的调用时_beginthread()。
        //_beginthread()将创建一个新线程,但不会占据在退出时需要CloseHandle()来清除的资源。
        //与之相反,客户端线程和服务端线程是由主线程创建的。
        //因此,客户端线程和服务端线程将一直占用分配给他们的资源直到调用CloseHandle()为止。
        HANDLE newthread;
        newthread = (HANDLE)_beginthread(&handleecho, 0, (void*)newsocket);
    }

    printf("Close server.\n");
    shutdown(ServerSocket, SD_BOTH);
    closesocket(ServerSocket);
    return 0;
}//end server

//响应线程代码
void handleecho(void* data)
{
    char buffer[1024];
    int count;
    ZeroMemory(&buffer, sizeof(buffer));
    int socket = (int)data;

    //从客户端读取消息
    while((count = recv(socket, buffer, 1023, 0)) > 0)
    {
        printf("Received '%s' from client.\n", buffer);
        //发送消息给客户端
        int ret = send(socket, buffer, count, 0);
    }

    printf("Close echo thread.\n");
    shutdown(socket, SD_BOTH);
    closesocket(socket);
}

7、变量的原子更新

注意:

Windows API提供了大量原子操作,用Windows术语来说就是互锁函数。

InterlockedExchangeAdd()就是一个互锁函数,能以原子操作方式添加一个值到一个LONG型变量。

在Windows中,LONG为32位,LONGLONG为64位。变量大小不会因为应用程序是32位还是64位而改变。

InterlockedCompareExchange() 在变量值与预期值相匹配时执行比较和交换操作,将变量的值与新值交换。
InterlockedBitTestAndSet() 返回变量中指定的值,并将该位设置为1。
InterlockedBitTestAndReSet() 返回变量中指定的值,并将该位设置为0。

这些函数大多数都强制实现完全内存排序,所以在调用之前的所有内存操作在原子操作完成前对其他处理器可见,而原子操作之后的任何内存操作在原子操作之后即时对其他处理器可见。

这些函数在原子操作前后对其他处理器都即时可见。

例-使用InterlockedExchangeAdd():

#include 

void update(LONG* value)
{
    InterlockedExchangeAdd(value, 10);
}

例-使用原子操作保护共享变量:

#include 
#include 
#include 
#include 

int isprime(int number)
{
    int i;
    for(i = 2; i < (int)( sqrt( (float)number ) + 1.0); i++)
    {
        if(number % i == 0) {return 0;}
    }
    return 1;
}//end isprime

volatile long counter = 0;

unsigned int __stdcall test(void*)
{
    while(counter < 100)
    {
        //原子操作-递加
        int number = InterlockedIncrement(&counter);
        printf("R=ThreadID %i; value = %i, is prime = %i\n",
            GetCurrentThreadId(), number, isprime(number));
    }
    return 0;
}//end test

int _tmian(int argc, _TCHAR* argv[])
{
    HANDLE h1, h2;
    h1 = (HANDLE)_beginthreadex(0, 0, &test, (void*)0, 0, 0);
    h2 = (HANDLE)_beginthreadex(0, 0, &test, (void*)0, 0, 0);
    WaitForSingleObject(h1, INFINITE);
    WaitForSingleObject(h2, INFINITE);
    CloseHandle(h1);
    CloseHandle(h2);
    getchar();
    return 0;
}

8、分配线程本地存储

线程本地存储的本质是“全局”数据的作用域受到了执行线程的限制。

两种实现方式:
1、 最简单的方式是使用__declspec(thread)说明符分配线程局部变量;
2、 使用线程本地API。全局索引需要通过TlsAlloc()来分配,但每个线程存储在索引中的数据为调用线程所私有。

例-使用__declspec(thread)分配线程局部变量:

#include 
#include 
#include 

//分配线程局部变量
__declspec(thread) int number = 0;

unsigned int __stdcall work(void * param)
{
    //操作线程局部变量
    number = (int)param;
    printf("Number = %i\n", number);
    return 0;
}

int _tmian(int argc, _TCHAR* argv[])
{
    HANDLE hthreads[8];
    for(int i = 0; i < 8; i++)
    {
        hthreads[i] = (HANDLE)_beginthreadex(0, 0, &work, (void*)i, 0, 0);
    }
    WaitForMultipleObjects(8, hthreads, 1, INFINITE);

    for(int i = 0; i < 8; i++)
    {
        CloseHandle(hthreads[i]);
    }
    getchar();
    return 0;
}

例-使用线程本地变量:

#include 
#include 
#include 

DWORD TLSIndex;

void setdata(int value)
{
    printf("Thread %i:Set Value = %i\n", GetCurrentThreadId(), value);
    TlsSetValue(TLSIndex, (void*)value);
}//end setdata

void getdata()
{
    int value;
    value = (int)TlsGetValue(TLSIndex);
    printf("Thread %i:Has Value = %i\n", GetCurrentThreadId(), value);
}//end getdata

unsigned int __stdcall workerthread(void* data)
{
    int value = (int)data;
    printf("Thread %i:Get Value = %i\n", GetCurrentThreadId(), value);
    setdata(value);
    Sleep(1000);
    getdata();
    return 0;
}//end workerthread

int _tmian(int argc, _TCHAR* argv[])
{
    HANDLE hthreads[8];
    //分配-线程本地存储-全局索引
    TLSIndex = TlsAlloc();
    for(int i = 0; i < 8; i++)
    {
        hthreads[i] = (HANDLE)_beginthreadex(0, 0, &workerthread, (void*)i, 0, 0);
    }
    WaitForMultipleObjects(8, hthreads, 1, INFINITE);

    for(int i = 0; i < 8; i++)
    {
        CloseHandle(hthreads[i]);
    }
    //释放-线程本地存储-全局索引
    TlsFree(TLSIndex);
    getchar();
    return 0;
}

9、设置线程优先级

Windows使用线程优先级来确定哪个线程获取下一片CPU资源。
优先级越高获取CPU时间片越多;反之,则越少。

比如,应用程序执行一个长时间运行的后台任务时,后台任务最好以低优先级运行,以保持机器的高响应性,
而高优先级的后台任务可能会消耗所有的CPU资源,在很长一段时间内导致机器无法执行其他用时短的计算密集型任务。

所有线程创建时的优先级均为THREAD_PRIORITY_NORMAL。

优先级反转:对于线程或进程,高优先级等待低优先级释放临界区,而低优先级因很少分得CPU资源而更加拖慢效率。

例-设置线程优先级:

#include 
#include 

unsigned int __stdcall FastThread(void * data)
{
    double d = 0.0;
    printf("Fast thread started\n");
    //设置高优先级
    SetThreadPriority(GetCurrentThreadId(), THREAD_PRIORITY_ABOVE_NORMAL);
    for(int i = 0; i < 100000000; i++)
    {
        d += d;
    }
    printf("Fast thread finished\n");
    return 0;
}//end FastThread

unsigned int __stdcall SlowThread(void * data)
{
    double d = 0.0;
    printf("Slow thread started\n");
    //设置低优先级
    SetThreadPriority(GetCurrentThreadId(), THREAD_PRIORITY_BELOW_NORMAL);
    for(int i = 0; i < 100000000; i++)
    {
        d += d;
    }
    printf("Slow thread finished\n");
    return 0;
}//end SlowThread

int _tmian(int argc, _TCHAR* argv[])
{
    HANDLE hFast, hSlow;
    hFast = (HANDLE)_beginthreadex(0, 0, &FastThread, (void*)0, 0, 0);
    hSlow = (HANDLE)_beginthreadex(0, 0, &SlowThread, (void*)0, 0, 0);
    WaitForSingleObject(hFast, INFINITE);
    WaitForSingleObject(hSlow, INFINITE);
    CloseHandle(hFast);
    CloseHandle(hSlow);
    getchar();
    return 0;
}

你可能感兴趣的:({,并发编程,},{,C/C++从入门到精通,})