Win32线程安全问题.同步函数

Posted ibinary

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Win32线程安全问题.同步函数相关的知识,希望对你有一定的参考价值。

 

              线程安全问题.同步函数

一丶简介什么是线程安全

  通过上面几讲.我们知道了线程怎么创建.线程切换的原理(CONTEXT结构) 每个线程在切换的时候都有自己的堆栈.

但是这样会有安全问题. 为什么?  我们每个线程都使用自己的局部变量这个是没有安全问题的. 但是线程可能会使用全局变量.这样很有可能会产生安全问题.为什么是很有可能.

1.有全局变量的情况下.有可能会有安全问题.

2.对全局变量进行写操作.则一定有安全问题. 

上面两个条件都具备,线程才是不安全的.

为什么是不安全的.

 

试想一下. 如果这个全局变量在更改.另一个线程也更改了.最后则会出现两个线程同时更改这个全局变量. 问题就会出现在这.

 

例如以下代码:

// 临界区同步函数.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <Windows.h>
DWORD g_Number = 10;
DWORD WINAPI MyThreadFun1(LPVOID lParame)
{
    while (g_Number > 0)
    {
        printf("+++剩下Number个数 = %d
", g_Number);
        g_Number--;
        printf("+++当前的Number个数 = %d
", g_Number);
    }
    return 0;
}

DWORD WINAPI MyThreadFun2(LPVOID lParame)
{
    while (g_Number > 0 )
    {
        printf("***剩下Number个数 = %d
", g_Number);
        g_Number--;                                           //产生线程安全问题
        printf("***当前的Number个数 = %d
", g_Number);
    }
    return 0;
}

int main(int argc,char *argv[])
{
    HANDLE hThreadHand[2] = { NULL };
    hThreadHand[0] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThreadFun1, NULL, 0, NULL);
    hThreadHand[1] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThreadFun2, NULL, 0, NULL); //创建两个线程
    WaitForMultipleObjects(2, hThreadHand, TRUE, INFINITE);
    


    printf("Number个数 = %d 
", g_Number);
    system("pause");
    return 0;
}

上面的代码很简单. 看下运行结果

技术分享图片

为什么会产生这个问题.原因是.在线程中我们有个地方

while(全局变量 > 0) 则会执行下边代码. 但是很有可能执行完这一句. 线程发生了切换. 去执行另一个线程去了. 最终会产生这样的结果.

如果看反汇编.则会发现 全局变量--的地方.汇编代码 并不是一局. 如果发生线程切换则会出现错误.

技术分享图片

首先获取全局变量的值.

然后sub -1

最后重新赋值.

很有可能在sun eax 1的时候就发生了切换. 这样就有安全问题了.为了解决这些问题.我们必须想办法. 所以Windows提供了一组线程同步的函数.

二丶线程同步函数之临界区

什么时候临界区. 临界区的意思就是 这一个区域我给你锁定.当前有且只能有一个线程来执行我们临界区的代码.

而临界资源是一个全局变量

临界区的使用步骤.

1.创建全局原子变量. 

2.初始化全原子变量

3.进入临界区

4.释放临界区.

5.删除临界区.

具体API:

  1.全局原子变量

 

CRITICAL_SECTION g_cs;  //直接创建即可.不用关心内部实现.

  2.初始化全局原子变量.InitializeCriticalSection

    _Maybe_raises_SEH_exception_ VOID InitializeCriticalSection(
        LPCRITICAL_SECTION lpCriticalSection                       //传入全局原子变量的地址
    );

      3.使用的API 进入临界区.

void EnterCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection      //全局原子变量
);

下面还有一个. 是尝试无阻塞模式进入临界区. 意思就是内部加了一个判断.是否死锁了.

BOOL TryEnterCriticalSection(                  返回吃持有的临界区对象.如果成功的情况下.
  LPCRITICAL_SECTION lpCriticalSection
);

  4.使用API 释放临界区.

  

void LeaveCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection         //全局原子对象
);

  5.删除临界区对象.

void DeleteCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
);

代码例子:

// 临界区同步函数.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <Windows.h>
//创建临界区结构
CRITICAL_SECTION g_cs;

DWORD g_Number = 10;
DWORD WINAPI MyThreadFun1(LPVOID lParame)
{
    EnterCriticalSection(&g_cs); //进入临界区
    while (g_Number > 0)
    {
        printf("+++剩下Number个数 = %d
", g_Number);
        g_Number--;
        printf("+++当前的Number个数 = %d
", g_Number);
    }
    LeaveCriticalSection(&g_cs);
    return 0;
}

DWORD WINAPI MyThreadFun2(LPVOID lParame)
{
    EnterCriticalSection(&g_cs); //进入临界区
    while (g_Number > 0 )
    {
        printf("***剩下Number个数 = %d
", g_Number);
        g_Number--;                                                                 //while语句内就是临界区了.有且只能一个线程访问.
        printf("***当前的Number个数 = %d
", g_Number);
    }
    LeaveCriticalSection(&g_cs);
    return 0;
}

int main(int argc,char *argv[])
{
    //初始化临界区全局原子变量
    InitializeCriticalSectionAndSpinCount(&g_cs, 0x00000400);
    //InitializeCriticalSection(&g_cs);                      //初始化临界区.两个API都可以.

    HANDLE hThreadHand[2] = { NULL };
    hThreadHand[0] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThreadFun1, NULL, 0, NULL);
    hThreadHand[1] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThreadFun2, NULL, 0, NULL); //创建两个线程
    WaitForMultipleObjects(2, hThreadHand, TRUE, INFINITE);
    
    DeleteCriticalSection(&g_cs); //删除临界区.

    printf("+Number个数 = %d 
", g_Number);
    system("pause");
    return 0;
}

官方MSDN例子:

链接:  https://docs.microsoft.com/zh-cn/windows/desktop/Sync/using-critical-section-objects

 

三丶线程同步之互斥体

1.临界区缺点.以及衍生出来的跨进程保护

 上面讲了临界区. 但是我们的临界资源是一个全局变量.例如下图:

技术分享图片

如果我们的临界资源是一个文件. 需要两个进程都要访问怎么办? 此时临界区已经不可以跨进程使用了.

2.跨进程控制.

  跨进程控制就是指 不同进程中的多线程控制安全..比如A进程访问临界资源的时候. B进程不能访问. 因为临界区的 令牌.也就是我们说的全局原子变量.只能在应用层.

但是如果放到内核中就好办了. 如下图所示

  技术分享图片

A进程的线程从内核中获取互斥体. 为0 还是为1. B进程一样. 如果为 0 则可以进行访问临界资源.  访问的时候.互斥体则设置为1(也就是令牌设置为1)这样B进程就获取不到了.自然不能访问

临界区资源了.

3.互斥体操作API

  既然明白了互斥体是一个内核层的原子操作.那么我们就可以使用API 进行操作了.

操作步骤.

    1.创建互斥体. 信号量设置为有信号的状态    例如全局的原子变量现在是有信号.是可以进行访问的.

    2.获取信号状态. 如果有信号则进入互斥体临界区执行代码.此时互斥体信号为无信号. 也就是说别的进程访问的时候.因为没有信号.执行不了代码.

    3.释放互斥体. 信号状态为有信号. 此时别的进程信号已经有了.所以可以进行访问了.

具体API:

1.创建互斥体

HANDLE CreateMutexA(
  LPSECURITY_ATTRIBUTES lpMutexAttributes,          SD安全属性.句柄是否可以继承.每个内核对象API都拥有.
  BOOL                  bInitialOwner,              初始的信号量状态. false为有信号. 获取令牌的时候可以获取到. True为无信号. 且如果为True互斥体对象为线程拥有者.
  LPCSTR                lpName                      全局名字. 根据名字寻找互斥体. 
);

2.获取令牌.

  

DWORD WaitForSingleObject(
  HANDLE hHandle,                          等待的内核对象
  DWORD  dwMilliseconds                 等待的时间
);

调用此函数之后.信号为无信号.别的进程是进入不了互斥体临界区的.

 

3.释放互斥体

   

BOOL ReleaseMutex(
  HANDLE hMutex
);

调用完比之后.互斥体为有信号.可以使用了.

 代码例子:

  两个工程代码是一样的.贴一份出来.

#include "stdafx.h"
#include <Windows.h>
//创建临界区结构

int main(int argc,char *argv[])
{
    //初始化临界区全局原子变量
    HANDLE MutexHandle = CreateMutex(NULL, FALSE, TEXT("AAA"));  //创建互斥体. 信号量为0. 有信号的状态.wait可以等待

    WaitForSingleObject(MutexHandle,INFINITE);
    

    for (size_t i = 0; i < 10; i++)
    {
        Sleep(1000);
        printf("A进程访问临街资源中临街资源ID = %d 
", i);
    }

    ReleaseMutex(MutexHandle);
    return 0;
}

先运行A进程在运行B进程. 则B进程处于卡死状态.

技术分享图片

实现了同步. 除非A进程释放互斥体句柄使信号变为有信号.此时才可以访问B

官方代码例子:

  

#include <windows.h>
#include <stdio.h>

#define THREADCOUNT 2

HANDLE ghMutex; 

DWORD WINAPI WriteToDatabase( LPVOID );

int main( void )
{
    HANDLE aThread[THREADCOUNT];
    DWORD ThreadID;
    int i;

    // Create a mutex with no initial owner

    ghMutex = CreateMutex( 
        NULL,              // default security attributes
        FALSE,             // initially not owned               有信号
        NULL);             // unnamed mutex                     不需要跨进程使用.所以不用名字

    if (ghMutex == NULL) 
    {
        printf("CreateMutex error: %d
", GetLastError());
        return 1;
    }

    // Create worker threads

    for( i=0; i < THREADCOUNT; i++ )
    {
        aThread[i] = CreateThread(                                       //创建  THREADCOUNT个线程
                     NULL,       // default security attributes
                     0,          // default stack size
                     (LPTHREAD_START_ROUTINE) WriteToDatabase, 
                     NULL,       // no thread function arguments
                     0,          // default creation flags
                     &ThreadID); // receive thread identifier

        if( aThread[i] == NULL )
        {
            printf("CreateThread error: %d
", GetLastError());
            return 1;
        }
    }

    // Wait for all threads to terminate

    WaitForMultipleObjects(THREADCOUNT, aThread, TRUE, INFINITE);                   //等待线程执行完毕

    // Close thread and mutex handles

    for( i=0; i < THREADCOUNT; i++ )
        CloseHandle(aThread[i]);

    CloseHandle(ghMutex);

    return 0;
}

DWORD WINAPI WriteToDatabase( LPVOID lpParam )
{ 
    // lpParam not used in this example
    UNREFERENCED_PARAMETER(lpParam);

    DWORD dwCount=0, dwWaitResult; 

    // Request ownership of mutex.

    while( dwCount < 20 )
    { 
        dwWaitResult = WaitForSingleObject(                                //线程内部等待互斥体.因为一开始为FALSE所以有信号.第一次执行线程的时候则会执行. 
            ghMutex,    // handle to mutex
            INFINITE);  // no time-out interval
 
        switch (dwWaitResult) 
        {
            // The thread got ownership of the mutex
            case WAIT_OBJECT_0: 
                __try { 
                    // TODO: Write to the database
                    printf("Thread %d writing to database...
", 
                            GetCurrentThreadId());
                    dwCount++;
                } 

                __finally { 
                    // Release ownership of the mutex object
                    if (! ReleaseMutex(ghMutex))                                         //执行完毕.释放互斥体.信号量变为有信号. 其余线程等待的时候可以等到则可以继续执行线程代码
                    { 
                        // Handle error.
                    } 
                } 
                break; 

            // The thread got ownership of an abandoned mutex
            // The database is in an indeterminate state
            case WAIT_ABANDONED: 
                return FALSE; 
        }
    }
    return TRUE; 
}

 

以上是关于Win32线程安全问题.同步函数的主要内容,如果未能解决你的问题,请参考以下文章

第三章--Win32程序的执行单元(部分概念及代码讲解)(中-线程同步

JAVA之旅(十三)——线程的安全性,synchronized关键字,多线程同步代码块,同步函数,同步函数的锁是this

win32中线程的正确同步方法是啥

证明同步函数使用的this锁

win32多线程设计总结

转载使用Win32API实现Windows下异步串口通讯