环境:
VISTA+VC6
双核
这个环境对于下面的有些效果来说,十分关键。
在我下面的练习中,如果是单核,那么两个线程无法真正的同时执行,
而单个操作的耗时也并不长,可能看不到互斥访问中的一些问题。
在VISTA之前的一些系统,时间片比较大,也不容易看到。。
设计目标:
模拟一个售票系统,有两个线程可以出售,总共100张票。
中间打印出出售的信息。
这里的票是一个临界资源,
同时,控制台也是个临界资源。(如果同时输出会造成屏幕的混乱)
原始程序:
#include <stdio.h>
#include <iostream>
#include <windows.h>
using namespace std;
int total=100;
DWORD WINAPI proc1(
LPVOID lpParameter // thread data
){
while (1){
if ( total == 0 ) break ;
Sleep(rand()%30) ;
cout<<"thread1 sold:"<<total--<<endl;
}
return 0;
};
DWORD WINAPI proc2(
LPVOID lpParameter // thread data
){
while (1){
if ( total == 0 ) break ;
Sleep(rand()%30) ;
cout<<"thread2 sold:"<<total--<<endl;
}
return 0;
};
int main(){
HANDLE thread1,thread2;
thread1=CreateThread(NULL,0,proc1,NULL,0,NULL);
thread2=CreateThread(NULL,0,proc2,NULL,0,NULL);
Sleep(4000);
CloseHandle(thread1);
CloseHandle(thread2);
return 0;
}
程序的意思很直观,就是开了两个线程。
在里面分别判断票数是否到0,
如果不是的话,那么模拟售出了一张票,并且打印出售出的票号。
中间标红的随机延迟是一个关键点。
把他去掉的话,一般就看不到效果了。
因为电脑实在太快了,if的判断和下面的输出,
几乎是在同一时间完成的。
从时间片的意义上来说,大部分时候可以看做原子操作。
于是减到0之后,线程正常结束就停下了。
所以给个随机延迟,强迫if的判断和total--的分离,
这样就可以看到由于没有做好同步造成的问题了。
这个程序的输出,有的地方会有字符交叉,很混乱。
最明显的是,减到0之后,还会不断地向下面减。
同步的框架:
下面几个方法,大同小异,
基本上的过程就是:
1.定义相关的变量
2.创建相关的变量
3.进去临界区前等待相关的信号
4.退出的时候清除相关的信号
(信号有的时候可以进入临界区,还是信号无的时候可以进入,
在几个实现手段里面有不同的叙述,所以清除是个泛化的说法)
互斥量:
#include <stdio.h>
#include <iostream>
#include <windows.h>
using namespace std;
int total=100;
HANDLE mutex;
DWORD WINAPI proc1(
LPVOID lpParameter // thread data
){
while (1){
WaitForSingleObject(mutex,INFINITE);
if ( total == 0 ) break ;
Sleep(rand()%30) ;
cout<<"thread1 sold:"<<total--<<endl;
ReleaseMutex(mutex);
}
return 0;
};
DWORD WINAPI proc2(
LPVOID lpParameter // thread data
){
while (1){
WaitForSingleObject(mutex,INFINITE);
if ( total == 0 ) break ;
Sleep(rand()%30) ;
cout<<"thread2 sold:"<<total--<<endl;
ReleaseMutex(mutex);
}
return 0;
};
int main(){
HANDLE thread1,thread2;
mutex=CreateMutex(NULL,false,NULL);
thread1=CreateThread(NULL,0,proc1,NULL,0,NULL);
thread2=CreateThread(NULL,0,proc2,NULL,0,NULL);
Sleep(4000);
CloseHandle(thread1);
CloseHandle(thread2);
CloseHandle(mutex);
return 0;
}
这是最基本的,和框架非常吻合,
知道标红的几个函数就按照这种方式写就行了。
信号量:
#include <stdio.h>
#include <iostream>
#include <windows.h>
using namespace std;
int total=100;
HANDLE semaphore ;
DWORD WINAPI proc1(
LPVOID lpParameter // thread data
){
while (1){
WaitForSingleObject(semaphore,INFINITE);
if ( total == 0 ) break ;
Sleep(rand()%30) ;
cout<<"thread1 sold:"<<total--<<endl;
ReleaseSemaphore(semaphore , 1 , NULL) ;
}
return 0;
};
DWORD WINAPI proc2(
LPVOID lpParameter // thread data
){
while (total>0){
WaitForSingleObject(semaphore,INFINITE);
if ( total == 0 ) break ;
Sleep(rand()%30) ;
cout<<"thread2 sold:"<<total--<<endl;
ReleaseSemaphore(semaphore , 1 , NULL) ;
}
return 0;
};
int main(){
HANDLE thread1,thread2;
semaphore = CreateSemaphore(NULL , 1 , 1 , NULL) ;
thread1=CreateThread(NULL,0,proc1,NULL,0,NULL);
thread2=CreateThread(NULL,0,proc2,NULL,0,NULL);
Sleep(4000);
CloseHandle(thread1);
CloseHandle(thread2);
CloseHandle(semaphore) ;
return 0;
}
和互斥量不同的地方在于,信号量可以允许多个线程同时访问。
比如writer/reader模型中,多个reader同时访问是允许的。
在创建的时候,可以指定最大的数目和初始化时候的数目。
如果指定为1,也就是这里用的情况,相当于就和前面的互斥量方式一样了。
事件:
#include <stdio.h>
#include <iostream>
#include <windows.h>
using namespace std;
int total=100;
HANDLE event;
DWORD WINAPI proc1(
LPVOID lpParameter // thread data
){
while (1){
WaitForSingleObject(event,INFINITE);
if ( total == 0 ) break ;
Sleep(rand()%30) ;
cout<<"thread1 sold:"<<total--<<endl;
SetEvent(event) ;
}
return 0;
};
DWORD WINAPI proc2(
LPVOID lpParameter // thread data
){
while (total>0){
WaitForSingleObject(event,INFINITE);
if ( total == 0 ) break ;
Sleep(rand()%30) ;
cout<<"thread2 sold:"<<total--<<endl;
SetEvent(event) ;
}
return 0;
};
int main(){
HANDLE thread1,thread2;
event = CreateEvent(NULL , FALSE , TRUE , NULL) ;
thread1=CreateThread(NULL,0,proc1,NULL,0,NULL);
thread2=CreateThread(NULL,0,proc2,NULL,0,NULL);
Sleep(4000);
CloseHandle(thread1);
CloseHandle(thread2);
CloseHandle(event);
return 0;
}
CreateEvent的第二个参数是设置是否为手动事件。
如果是手动的话,当用WaitForSingleObject等到事件的时候,
系统并不清除掉该事件已发生的信号,
于是要自己调用ResetEvent来清除。
这两个函数之间的空隙将造成潜在的同步问题。
于是设置生FALSE,表示自动事件。
在等到该事件的时候,同时把该事件置为无效,防止其他地方进入临界段。
临界区:
#include <stdio.h>
#include <iostream>
#include <windows.h>
using namespace std;
int total=100;
CRITICAL_SECTION _cs;
DWORD WINAPI proc1(
LPVOID lpParameter // thread data
){
while (1){
EnterCriticalSection(&_cs);
if ( total == 0 ) break ;
// Sleep(rand()%30) ;
cout<<"thread1 sold:"<<total--<<endl;
LeaveCriticalSection(&_cs);
}
return 0;
};
DWORD WINAPI proc2(
LPVOID lpParameter // thread data
){
while (1){
EnterCriticalSection(&_cs);
if ( total == 0 ) break ;
// Sleep(rand()%30) ;
cout<<"thread2 sold:"<<total--<<endl;
LeaveCriticalSection(&_cs);
}
return 0;
};
int main(){
HANDLE thread1,thread2;
InitializeCriticalSection(&_cs);
thread1=CreateThread(NULL,0,proc1,NULL,0,NULL);
thread2=CreateThread(NULL,0,proc2,NULL,0,NULL);
Sleep(4000);
CloseHandle(thread1);
CloseHandle(thread2);
return 0;
}
与前面的相比,这种方式在最后不用类似CloseHandle之类的操作。
还有注意到我把上面的Sleep注释掉了。
因为使用临界区来同步,速度非常快,消耗资源比前几种都小
加上随机延迟后,可能一个线程直接就把票给售完了。。
即使在现在这种写法下,可能运行好几次,
能够找到一下若干thread1信息之内夹杂几个thread2的信息,或者反之。
但观察前三种,基本上的效果是一个线程输出一下,交织频繁。
总结:
前三种方式,依赖一个句柄,
他们都可以指定一个名字,成为全局的对象,
可以完成进程间的同步。
在不用的时候要,销毁相关的句柄。
消耗资源比较大。
最后一种临界区,消耗资源非常少,速度快。
但是只能解决线程间的同步。
几种同步手段(互斥量,信号量,事件,临界区)
最新推荐文章于 2022-11-22 18:08:17 发布