继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

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

慕田峪9158850
关注TA
已关注
手记 45
粉丝 20
获赞 71

 

一丶简介什么是线程安全

  通过上面几讲.我们知道了线程怎么创建.线程切换的原理(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\r\n", g_Number);
        g_Number--;
        printf("+++当前的Number个数 = %d\r\n", g_Number);
    }    return 0;
}

DWORD WINAPI MyThreadFun2(LPVOID lParame)
{    while (g_Number > 0 )
    {
        printf("***剩下Number个数 = %d\r\n", g_Number);
        g_Number--;                                           //产生线程安全问题
        printf("***当前的Number个数 = %d\r\n", 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 \r\n", g_Number);
    system("pause");    return 0;
}

复制代码

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

https://img4.mukewang.com/5b8bba8100016f7c10430434.jpg

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

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

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

https://img2.mukewang.com/5b8bba890001ee9008250100.jpg

首先获取全局变量的值.

然后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\r\n", g_Number);
        g_Number--;
        printf("+++当前的Number个数 = %d\r\n", g_Number);
    }
    LeaveCriticalSection(&g_cs);    return 0;
}

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

复制代码

官方MSDN例子:

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

 

三丶线程同步之互斥体

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

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

https://img.mukewang.com/5b8bba9300014ba109020268.jpg

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

2.跨进程控制.

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

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

  https://img2.mukewang.com/5b8bba9b00012aed07270297.jpg

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 \r\n", i);
    }

    ReleaseMutex(MutexHandle);    return 0;
}

复制代码

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

https://img1.mukewang.com/5b8bbaa40001602c14140166.jpg

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

官方代码例子:

  

复制代码

#include <windows.h>#include <stdio.h>#define THREADCOUNT 2HANDLE 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\n", 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\n", 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...\n", 
                            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; 
}

复制代码

原文出处:https://www.cnblogs.com/iBinary/p/9574211.html

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP