C++线程同步笔记

本文介绍了多线程编程中实现线程同步的三种方法:互斥对象、事件对象和关键代码段。通过示例代码详细解释了每种方法的具体应用,并讨论了线程死锁的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程同步

多线程实现线程同步有三种方式:互斥对象、事件对象和关键代码段。

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

主要函数:

  • CreateMutex:创建互斥对象
  • WaitForSingleObject:请求对象的使用权
  • ReleaseMutex:释放互斥对象的所有权

示例代码如下:

#include<windows.h>
#include<iostream.h>

DWORD WINAPI Fun1Proc(LPVOID lpParameter); //新线程1的入口函数声明,函数的名称Fun1Proc可任意取
DWORD WINAPI Fun2Proc(LPVOID lpParameter); //新线程1的入口函数声明

int tickets = 100;
HANDLE hMutex;

void main()
{
HANDLE hThread1;
HANDLE hThread2;

//参数2:TRUE表示创建互斥对象的线程获取对象的所有权;3:互斥对象的名称,NULL表示匿名
hMutex = CreateMutex(NULL,FALSE,NULL);

/*参数1:NULL表示使用默认的安全性;2:线程初始栈的大小,0表示默认使用与调用该函数的线程相同的栈空间大小;
3:新线程的入口函数;4:可通过这个参数给新线程传递参数;5:0表示线程创建后立即运行,CREATE_SUSPENDED表示线程
创建后处于暂停状态,需要调用ResumeThread运行;6:用来接收线程ID*/
hThread1 = CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
hThread2 = CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);

/*CloseHandle关闭对新创建的线程的引用,并不是终止新线程。还会递减线程内核对象的使用计数,当使用计数为0时,系统就会释放该线程内核对象,
如果不关闭线程句柄,只有等到进程终止时,系统才会清理这些对象*/
CloseHandle(hThread1);
CloseHandle(hThread2);

cout<<"main thread is running"<<endl;
Sleep(4000);
}

DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while(1)
{
    /*请求共享对象的使用权。参数1:请求的对象的句柄,如果互斥对象处于有信号状态,就返回,否则一直等待;
    参数2:等待的时间,如果指定的时间已过,请求的对象仍然无信号,则返回,参数为0表示测试该对象的状态立即返回,参数为INFINITE表示永远等待*/

    WaitForSingleObject(hMutex,INFINITE);
    if(tickets>0){
        Sleep(1);
        cout<<"thread1 sell "<<tickets--<<endl;
    }else{
        break;
    }
    //释放指定对象的所有权
    ReleaseMutex(hMutex);
}

return 0;
}

DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while(1)
{
    WaitForSingleObject(hMutex,INFINITE);
    if(tickets>0){
        Sleep(1);
        cout<<"thread2 sell "<<tickets--<<endl;
    }else{
        break;
    }
    ReleaseMutex(hMutex);
}

return 0;
}

注意点:

  • 获取到互斥对象的使用权后,要保护的代码操作完成后,要调用ReleaseMutex释放当前线程对互斥对象的使用权,这个时候操作系统会将该线程ID设置为0,然后设置互斥对象为有信号状态,其他线程才有机会获得互斥对象的使用权。谁拥有互斥对象,谁就要释放它。
  • 如果当前线程已经获得了互斥对象的使用权,又调用了WaitForSingleObject请求对象的使用权,因为请求的线程的ID和互斥对象当前所有者的线程ID是相同的,所以可以请求到这个互斥对象。如果多次在同一个线程中请求同一个互斥对象,就要相应地多次调用ReleaseMutex释放互斥对象。
  • 如果一个线程获得了互斥对象的使用权,但是没有释放它,那么当线程执行完毕时,操作系统会将互斥对象的线程ID置为0,引用计数置为0,其他线程就可以得到互斥对象的所有权了。

利用事件对象实现线程同步

主要函数:

  • CreateEvent:创建事件对象。其中第2个参数指定是人工重置事件对象还是自动重置事件对象。TRUE表示人工,当线程获取到事件对象的所有权后,需要手动调用ResetEvent将事件对象设置为无信号状态;FALSE表示自动,当线程获取到事件对象的所有权后,系统会自动将该对象设置为无信号状态。
  • SetEvent:设置事件对象为有信号状态
  • ResetEvent:设置事件对象为无信号状态

示例代码如下:

#include<windows.h>
#include<iostream.h>

DWORD WINAPI Fun1Proc(LPVOID lpParameter); 
DWORD WINAPI Fun2Proc(LPVOID lpParameter);

int tickets = 100;
HANDLE hEvent;

void main()
{
HANDLE hThread1;
HANDLE hThread2;

//创建事件对象。参数2:TRUE表示是人工重置事件对象,FALSE表示自动重置事件对象;3:指定事件对象的初始状态,TRUE表示有信号,反之;4:事件对象的名字,NULL表示匿名
hEvent = CreateEvent(NULL,FALSE,TRUE,NULL);

hThread1 = CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
hThread2 = CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);

CloseHandle(hThread1);
CloseHandle(hThread2);

cout<<"main thread is running"<<endl;
Sleep(4000);
}

DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while(1)
{
    //申请事件对象的所有权
    WaitForSingleObject(hEvent,INFINITE);
    if(tickets>0){
        Sleep(1);
        cout<<"thread1 sell "<<tickets--<<endl;
    }else{
        break;
    }
    //设置事件对象为有信号状态
    SetEvent(hEvent);
}

return 0;
}

DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while(1)
{
    WaitForSingleObject(hEvent,INFINITE);
    if(tickets>0){
        Sleep(1);
        cout<<"thread2 sell "<<tickets--<<endl;
    }else{
        break;
    }
    SetEvent(hEvent);
}

return 0;
}

注意点:

  • 如果使用人工重置事件对象,创建事件对象和设置事件对象为无信号状态,是分两步进行的,所以很可能当线程一刚获取到事件对象时,线程一的时间片终止了,由于此时线程一还没来得及将事件对象设置为无信号状态,所以线程二又能获取到事件对象的所有权。因此线程同步不应该使用人工重置事件对象。

利用关键代码段实现线程同步

主要函数:

  • InitializeCriticalSection:初始化关键代码段–(建造一个电话亭)
  • EnterCriticalSection:获取临界区对象的所有权–(进入电话亭使用,其他人不允许再进入)
  • LeaveCriticalSection:释放临界区对象的所有权–(离开电话亭,其他人可以进入)
  • DeleteCriticalSection:释放一个没有被任何线程所拥有的临界区对象的所有资源(拆除电话亭)

示例代码如下:

#include<windows.h>
#include<iostream.h>

DWORD WINAPI Fun1Proc(LPVOID lpParameter); 
DWORD WINAPI Fun2Proc(LPVOID lpParameter);

int tickets = 100;
CRITICAL_SECTION g_cs;

void main()
{
HANDLE hThread1;
HANDLE hThread2;

hThread1 = CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
hThread2 = CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);

CloseHandle(hThread1);
CloseHandle(hThread2);

//初始化关键代码段--相当于建造一个电话亭
InitializeCriticalSection(&g_cs);

cout<<"main thread is running"<<endl;
Sleep(4000);

DeleteCriticalSection(&g_cs);
}

DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while(1)
{
    EnterCriticalSection(&g_cs);
    if(tickets>0){
        Sleep(1);
        cout<<"thread1 sell "<<tickets--<<endl;
    }else{
        break;
    }
    LeaveCriticalSection(&g_cs);
}

return 0;
}

DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while(1)
{
     EnterCriticalSection(&g_cs);
    if(tickets>0){
        Sleep(1);
        cout<<"thread2 sell "<<tickets--<<endl;
    }else{
        break;
    }
    LeaveCriticalSection(&g_cs);
}

return 0;
}

注意点:

  • 如果一个线程获取了临界区对象的使用权限,并且忘记释放了,尽管该线程执行完毕了,其他线程也没有机会获取临界区对象的使用权限了,只能一直等待。直到进程退出,线程也退出了。

线程死锁

在编写多线程同步时,很容易发生线程死锁。哲学家进餐问题可以形象的描述线程死锁现象:有多位哲学家在一起吃饭,每个人只有一根筷子,无法吃饭。大家都希望其他哲学家可以交出筷子让自己先吃饭,最后只能干瞪眼盯着桌上的美食挨饿。

在多线程中,如果线程1拥有临界区对象A,并且等待获取临界区对象B,而线程2拥有临界对象B,并且还在等待获取临界对象A,就造成了死锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值