关闭

多线程/线程同步机制

标签: 多线程同步机制线程
368人阅读 评论(0) 收藏 举报
分类:

//-------------------------------------------

HANDLE     CreateEvent(
 
 LPSECURITY_ATTRIBUTES     lpEventAttributes, // SD  
 BOOL     bManualReset, //reset type  
 BOOL     bInitialState, //initial state  
 LPCTSTR     lpName   //object name  
    );  
    该函数创建一个Event同步对象,如果CreateEvent调用成功的话,会返回新生成的对象的句柄,否则返回NULL。
 
   参数说明:
   lpEventAttributes 一般为NULL   
  
   bManualReset  创建的Event是自动复位还是人工复位.如果true,人工复位,   一旦该Event被设置为有信号,则
   它一直会等到ResetEvent()API被调用时才会恢复 为无信号.
   如果为false,Event被设置为有信号,则当有一个wait到它的Thread时,  该Event就会自动复位,变成无信号.  
   如果想在每次调用WaitForSingleObject 后让WINDOWS为您自动地把事件地状态恢复为”无信号”状态,
   必须把该参数设为FALSE,否则,您必须每次调用ResetEvent函数来清除事件的信号。  
   bInitialState  初始状态,true,有信号,false无信号  
   lpName   事件对象的名称。您在OpenEvent函数中可能使用。

//-------------------------------------------

多线程对于多线程程序,如果线程都需要访问共享资源,就需要进行线程间的同步处理。

两个线程访问同一个全局变量(共享资源),需要做同步处理,保证只有一个线程访问共享资源时,其他线程不能访问该资源;如果在一个线程进入后调用Sleep()函数,该线程就要放弃执行权力,操作系统就会选择另一个线程来执行。

线程同步方法:互斥对象,事件对象,关键代码段

简单说就是:一件事情完成才可能做下一件事情,在IO等待的时候,同步不会切走,浪费时间。

异步:注:简单说就是在等待的时候可切换做别的事情。

1、            利用互斥对象实现线程同步

互斥对象属于内核对象,它能够确保线程拥有对单个资源的互斥访问权,互斥对象包含一个使用数量,一个线程Id和一个计数器,ID用于标识系统中哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。

互斥对象的创建:CreateMutex ()创建或打开一个命名的或匿名的互斥对象,然后程序就可以利用互斥对象完成对线程间的同步。

HANDLE CreateMutex(

  LPSECURITY_ATTRIBUTES lpMutexAttributes,  // 指向SECURITY_ATTRIBUTES结构的指针,可以传NULL,让互斥对象使用默认的安全性。

  BOOL bInitialOwner,                       // 指定互斥对象初始化拥有者,为TRUE则创建这个互斥对象的线程获得该对象的所有权,否则该线程部获得所创建互斥对象的所有权

  LPCTSTR lpName                            // 指定互斥对象名称,为NULL表示创建一个匿名的互斥对象

);

调用成功则返回所创建互斥对象的句柄。

线程对共享资源访问结束后,应该嗲用ReleaseMutex()函数释放该对象的所有权,也就是让该对象处于已通知状态,

线程必须主动请求共享对象的使用权才可能获得该所有权:

DWORD WaitForSingleObject(

  HANDLE hHandle,        // handle to object请求的对象句柄

  DWORD dwMilliseconds   // time-out interval指定等待的时间间隔,毫秒为单位。超过时间间隔,所请求的对象仍处於无信号状态,函数会返回。如果设置为INFINITE则会一直等待,知道等待的对象处于有信号状态才会返回。

);

WaitForSingleObject只在两种情况下才会返回,第一:指定对象变成有信号状态,第二,指定等待时间间隔已过。

 

Code:

#include <windows.h>

#include <iostream.h>

int index = 0;

int tickets = 100;

HANDLE hMutex;

DWORD WINAPI FunProc1(LPVOID lpParameter);

DWORD WINAPI FunProc2(LPVOID lpParameter);

void main()

{

    HANDLE hThread1,hThread2;

    hMutex = CreateMutex(NULL,FALSE,NULL);//create 互斥对象,第二个参数设置为FALSE表明当前没有线程拥有这个互斥对象

    //于是操作系统会将该互斥对象设置为有信号状态。

    hThread1 = CreateThread(NULL,0,FunProc1,NULL,0/*CREATE_SUSPENDED*/,NULL);

    hThread2 = CreateThread(NULL,0,FunProc2,NULL,0,NULL);

    CloseHandle(hThread1);

    CloseHandle(hThread2);

//  cout<<index<<" main thread is running."<<endl;

    Sleep(300);

}

DWORD WINAPI FunProc1(LPVOID lpParameter)

{

    while (TRUE)

    {

        WaitForSingleObject(hMutex,INFINITE);

        if (tickets > 0)

        {

            Sleep(1);

            cout<<"thread1 sell tickets:"<<tickets--<<endl;

        }

        else

            break;

        ReleaseMutex(hMutex);

    }

    return 0;

}

DWORD WINAPI FunProc2(LPVOID lpParameter)

{

    while (TRUE)

    {

        WaitForSingleObject(hMutex,INFINITE);

        if(tickets > 0)

        {

            Sleep(1);

            cout<<"thread2  sell tickets:"<<tickets--<<endl;

        }

        else

            break;

        ReleaseMutex(hMutex);

    }

    return 0;

}

在创建互斥对象时,第二个参数传递的是FALSE,表面当前没有线程拥有这个互斥对象,于是操作系统将该互斥对象设置为有信号状态,当第一个线程开始运行时,进入while循环后,调用WaitForSingleObject函数,因为这个时候互斥对象处于有信号状态,所以该线程就请求到了这个互斥对象,操作系统会将该互斥对象的ID设置为线程1的ID,接着操作系统会将这个互斥对象设置为未通知状态,线程1继续往下运行,调用Sleep函数,于是暂停执行,操作系统就会选择线程2开始执行,该线程的执行过程也是一样的,进入到while循环后,调用WaitForSingleObject函数,这时该互斥对象已经被线程1所拥有,处于未通知状态,线程2没有获得互斥对象的所有权,因此WaitForSingleObject函数就会处于等待状态,线程2处于暂停执行状态,当线程1睡眠时间到了之后,继续往下执行,销售出一张火车票,这时线程1调用ReleaseMutex释放互斥对象的所有权,也就是让该对象处于已通知状态。如果轮到线程2执行了,那么线程2 的WaitForSingleObject函数就可以得到互斥对象的所有权,线程2继续执行下面的代码,同样,线程2销售完一张票之后,释放互斥对象的所有权。循环执行。

 

如果将互斥对象设置为主线程拥有,对于互斥对象来说,它是唯一与线程相关的内核对象,当主线程拥有互斥对象时,操作系统会将互斥对象的线程ID设置为主线程的ID,在调用ReleaseMutex函数释放互斥对象的所有权时,操作系统会判断线程的ID与互斥对象内部所维护的线程ID是否相等,只有相等才能完成释放操作。

注意:互斥对象谁拥有谁释放。

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
  SIZE_T dwStackSize,                       // initial stack size
  LPTHREAD_START_ROUTINE lpStartAddress,    //新创建的线程的入口函数
  LPVOID lpParameter, //该参数给创建的新线程传递参数(是主线程向数据接收线程传递的参数)
  DWORD dwCreationFlags,                    // creation option
  LPDWORD lpThreadId                        // thread identifier
);

 

对类的静态函数而言,它不属于该类的任一个对象,它只属于类的本身。

 

2、事件对象:

事件对象也属于内核对象,包含三个成员:

1.  使用计数

2.  用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值

3.  用于指明该事件处于已通知状态还是未通知状态的布尔值。

事件对象有两种不同的类型:

1、人工重置的事件对象:当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程。人工重置事件对象在一个线程得到该事件对象后,操作系统并不会将该事件对象设置为无信号状态,除非显示地调用ResetEvent()函数将其设置为无信号状态,否则该对象会一直是有信号状态。

2、自动重置的事件对象:当一个自动重置的事件对象得到通知时,等待该事件对象的线程中有一个线程变为可调用线程,同时操作系统会将该事件对象设置为无信号状态。

创建事件对象CreateEvent()函数创建或打开一个命名的或匿名的事件对象。

HANDLE CreateEvent(
  LPSECURITY_ATTRIBUTES lpEventAttributes, // SD默认安全属性
  BOOL bManualReset, //为FALSE创建一个自动重建事件对象,为TRUE则为创建人工重置事件对象。
  BOOL bInitialState, //初始化状态,TRUE则为有信号状态,FALSE为无信号状态。
  LPCTSTR lpName//NULL则创建一个匿名的事件对象。);
设置事件对象状态:SetEvent()函数
BOOL SetEvent(
  HANDLE hEvent//指定的事件对象设置为有信号状态。
);
重置时间对象状态:ResetEvent()函数
BOOL ResetEvent(
  HANDLE hEvent   //指定的事件对象设置为无信号状态。
);
 

Code:

#include <windows.h>

#include <iostream.h>

int index = 0;

int tickets = 100;

HANDLE g_hEvent;

DWORD WINAPI FunProc1(LPVOID lpParameter);

DWORD WINAPI FunProc2(LPVOID lpParameter);

void main()

{

    HANDLE hThread1,hThread2;

//  hMutex = CreateMutex(NULL,TRUE,NULL);//create 互斥对象,第二个参数设置为FALSE表明当前没有线程拥有这个互斥对象

    //于是操作系统会将该互斥对象设置为有信号状态。

    g_hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);//创建自动重置事件对象,初始化状态为无信号状态。

    SetEvent(g_hEvent);

    hThread1 = CreateThread(NULL,0,FunProc1,NULL,0,NULL);

    hThread2 = CreateThread(NULL,0,FunProc2,NULL,0,NULL);

    CloseHandle(hThread1);

    CloseHandle(hThread2);

    Sleep(4000);

    CloseHandle(g_hEvent);

}

DWORD WINAPI FunProc1(LPVOID lpParameter)

{

    while (TRUE)

    {

        WaitForSingleObject(g_hEvent,INFINITE);

        if (tickets > 0)

        {

            Sleep(1);

            cout<<"thread1 sell tickets:"<<tickets--<<endl;

            SetEvent(g_hEvent);

        }

        else

        {

            SetEvent(g_hEvent);

            break;

        }

    }

    return 0;

}

DWORD WINAPI FunProc2(LPVOID lpParameter)

{

    while (TRUE)

    {

        WaitForSingleObject(g_hEvent,INFINITE);

        if(tickets > 0)

        {

            Sleep(1);

            cout<<"thread2  sell tickets:"<<tickets--<<endl;

            SetEvent(g_hEvent);

        }

        else

        {

            SetEvent(g_hEvent);

            break;

        }

    }

    return 0;

}

人工重置的时间对象变成有信号状态时,所有等待该对象的线程都变为可调度线程。也就是说所有等待的线程可以同时运行,这样线程间的同步就失败了。

为了实现线程间的同步,不应该使用人工重置的事件对象,应该使用自动重置的时间对象。

 

关键代码段:

关键代码段:也称为临界区,工作在用户方式下。它指的是一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。通常把多线程中访问同一种资源的那部分代码当做关键代码段。关键代码段类似使用公用电话亭,进入公用电话亭使用这种资源时,首先要判断电话亭里是否有人,有人就只能等待,当前面那人使用完电话,并离开电话亭后,才能进入电话亭使用电话资源。同样地,使用完电话后,也要离开电话亭。

临界资源:是指每次仅允许一个进程访问的资源。属于临界资源的硬件有打印机、磁带机等,软件有消息缓冲队列、变量数组、缓冲区等。 诸进程间采取互斥方式,实现对这种资源的共享。

 

临界区:每个进程中访问临界资源的那段代码,每次只准许一个进程进入临界区,进入后不允许其他进程进入。

 

 

初始化关键代码段:InitializeCriticalSection()函数初始化一个关键代码段(临界资源对象)

VOID InitializeCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection  // critical section
);
进入关键代码段:EnterCriticalSection()函数
VOID EnterCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection //获得指定临界区对象的所有权。所有权被赋予了调用线程,函数才会返回。否则一直等待。
);
释放指定的临界区对象的所有权:LeaveCriticalSection函数
VOID LeaveCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection //释放指定临界区对象的所有权。
);
DeleteCriticalSection函数()释放一个没有被任何线程所拥有的临界区对象的所有资源。
 

Code:

#include <windows.h>

#include <iostream.h>

 

int tickets = 100;//临界资源

CRITICAL_SECTION g_cs;//临界区结构对象

DWORD WINAPI FunProc1(LPVOID lpParameter);

DWORD WINAPI FunProc2(LPVOID lpParameter);

DWORD WINAPI FunProc3(LPVOID lpParameter);

void main()

{

    HANDLE hThread1,hThread2,hThread3;

    InitializeCriticalSection(&g_cs);//临界区对象初始化

    hThread1 = CreateThread(NULL,0,FunProc1,NULL,0,NULL);

    hThread2 = CreateThread(NULL,0,FunProc2,NULL,0,NULL);

    hThread3 = CreateThread(NULL,0,FunProc3,NULL,0,NULL);

    CloseHandle(hThread1);

    CloseHandle(hThread2);

    CloseHandle(hThread3);

    Sleep(4000);

    DeleteCriticalSection(&g_cs);

}

DWORD WINAPI FunProc1(LPVOID lpParameter)

{

    while (TRUE)

    {

        EnterCriticalSection(&g_cs);

        Sleep(1);

        if (tickets > 0)

        {

            Sleep(1);

            cout<<"thread1 sell tickets:"<<tickets--<<endl;

            LeaveCriticalSection(&g_cs);

        }

        else

        {

            LeaveCriticalSection(&g_cs);

            break;

        }

    }

    return 0;

}

DWORD WINAPI FunProc2(LPVOID lpParameter)

{

    while (TRUE)

    {

        EnterCriticalSection(&g_cs);

        Sleep(1);

        if (tickets > 0)

        {

            Sleep(1);

            cout<<"thread2 sell tickets:"<<tickets--<<endl;

            LeaveCriticalSection(&g_cs);

        }

        else

        {

            LeaveCriticalSection(&g_cs);

            break;

        }

    }

    return 0;

}

DWORD WINAPI FunProc3(LPVOID lpParameter)

{

    while (TRUE)

    {

        EnterCriticalSection(&g_cs);

        Sleep(1);

        if (tickets > 0)

        {

            Sleep(1);

            cout<<"thread3 sell tickets:"<<tickets--<<endl;

            LeaveCriticalSection(&g_cs);

        }

        else

        {

            LeaveCriticalSection(&g_cs);

            break;

        }

    }

    return 0;

}

描述线程死锁 ——哲学家进餐问题:多位哲学家一起用餐,但每个人只有一只筷子,用一只筷子无法吃食物,这时,如果有位哲学家能将他的那只筷子叫出来,让其他哲学家先吃,之后在将一双筷子交回来,这样就能吃到食物了。但哲学家都担心交出筷子给别人先吃,别人吃完后不把筷子叫回来的话,自己就吃不到食物。所以都希望其他人把筷子叫出来,让自己先吃。这样就是一个线程死锁的实例。

对多线程来说,如果线程1拥有了临界区对象A,等待临界区对象B的所有权,线程2拥有临界区对象B,等待临界区对象A的所有权,这样就造成了死锁。

 

Code:

#include <windows.h>

#include <iostream.h>

 

int tickets = 100;//临界区

CRITICAL_SECTION g_csA;//// 临界区结构对象

CRITICAL_SECTION g_csB;

DWORD WINAPI FunProc1(LPVOID lpParameter);

DWORD WINAPI FunProc2(LPVOID lpParameter);

void main()

{

    HANDLE hThread1,hThread2;

    InitializeCriticalSection(&g_csA);

    InitializeCriticalSection(&g_csB);

    hThread1 = CreateThread(NULL,0,FunProc1,NULL,0,NULL);

    hThread2 = CreateThread(NULL,0,FunProc2,NULL,0,NULL);

    CloseHandle(hThread1);

    CloseHandle(hThread2);

/*  InitializeCriticalSection(&g_cs);*/

    Sleep(4000);

    DeleteCriticalSection(&g_csA);

    DeleteCriticalSection(&g_csB);

}

DWORD WINAPI FunProc1(LPVOID lpParameter)

{

    while (TRUE)

    {

        EnterCriticalSection(&g_csA);

        Sleep(1);

        EnterCriticalSection(&g_csB);

        if (tickets > 0)

        {

            Sleep(1);

            cout<<"thread1 sell tickets:"<<tickets--<<endl;

            LeaveCriticalSection(&g_csB);

            LeaveCriticalSection(&g_csA);

        }

        else

        {

            LeaveCriticalSection(&g_csB);

            LeaveCriticalSection(&g_csA);

            break;

        }

    }

    return 0;

}

DWORD WINAPI FunProc2(LPVOID lpParameter)

{

    while (TRUE)

    {

        EnterCriticalSection(&g_csB);

        Sleep(1);

        EnterCriticalSection(&g_csA);

        if (tickets > 0)

        {

            Sleep(1);

            cout<<"thread2 sell tickets:"<<tickets--<<endl;

            LeaveCriticalSection(&g_csA);

            LeaveCriticalSection(&g_csB);

        }

        else

        {

            LeaveCriticalSection(&g_csA);

            LeaveCriticalSection(&g_csB);

            break;

        }

    }

    cout<<"thread2 is running."<<endl;

    return 0;

}

互斥对象、事件对象与关键代码段比较:

1.  互斥对象和时间对象都属于内核对象,利用内核对象进行线程同步时,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。

2.  关键代码段工作在用户方式下,同步速度较快,但在使用关键代码时,很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值。

 

 

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:6247次
    • 积分:135
    • 等级:
    • 排名:千里之外
    • 原创:5篇
    • 转载:12篇
    • 译文:1篇
    • 评论:0条
    文章分类