创建一个线程,自然有一个相应的系统API来完成。CreateThread这个函数就用来创建线程的。
各种参数的用途我就不多说了,这里直接贴一个我自己练习的例子
1、下面是一个创建一个线程的例子,当然,仅仅是创建;
#include<windows.h> #include<iostream> using namespace std; DWORD WINAPI Fun1Pro(LPVOID laparameter); int main() { HANDLE hthread1; hthread1 = CreateThread(NULL, 0, Fun1Pro, NULL, 0, NULL); CloseHandle(hthread1); cout << "main thread is running \n"; return 0; } DWORD WINAPI Fun1Pro(LPVOID Laparameter) { printf("The New Thread is running\n"); return 0; }在C++中,对于对象的引用,也是通过引用计数的方式来处理的,所以上述代码中的closehandle这个函数,只是在主线程中关闭了线程的一个句柄,但是实际上创建的这个新线程还是存在的,只不过这个新线程的内核对象中对这个线程的引用计数-1.否则如果不执行这样的一个函数,即使这个线程执行完了,也会因为主线程中仍然有引用这个线程的痕迹从而这个线程的引用计数不为0,则不能彻底删除,只有当整个进程计数的时候,才会进行清理。
运行这个程序之后可以发现,并没有看到新线程输出的那句话。
原因是这样的,进程启动之后先执行了主线程,因为操作系统是通过分配给线程时间片的方式来让线程运行的,所以,在主线程的时间片内,没有特殊情况是没人打断他的,一直在执行,到了创建新的子线程的时候,因为主线程的时间片没到,所以,不能够执行子线程,之后马上又关闭了子线程的句柄,主线程执行完了,就说明整个进程也就结束了,回收了所有的资源,子线程基本上是没用的。如果要想看见子线程运行的话,那么就需要让主线程暂停执行,这时候,操作系统中的分派器就会在队列里面找到一个新的线程来执行。
让一个线程停止运行的的办法就是利用sleep()函数,使其能够暂停一下。
可以在main函数中return 0;这条语句的前面加上sleep他就 会在主线程结束之前暂停,从而执行新的子线程。
那么主线程和子线程他们直接的运行顺序到底是什么样的呢?由于线程和进程的运行时间都是又操作系统的时间片来决定的,我对代码做了如下修改:
#include<windows.h> #include<iostream> using namespace std; DWORD WINAPI Fun1Pro(LPVOID laparameter); int index = 0; int main() { index = 0; HANDLE hthread1; hthread1 = CreateThread(NULL, 0, Fun1Pro, NULL, 0, NULL); CloseHandle(hthread1); while(index++<100) cout << "main thread is running \n"; //Sleep(10); return 0; } DWORD WINAPI Fun1Pro(LPVOID Laparameter) { while(index++<100) printf("The New Thread is running\n"); return 0; }
从图中我们就可以印证上面所说的了,线程的运行时间是靠时间片来决定的,当主线程的时间片运行完,但是整个主线程还没运行完的时候,操作系统的分派器也会将主线程加入线程的就绪队列,从队列中找出新的子进程来执行。
2、利用互斥对象实现同步:
互斥对象属于内核对象,他能够保证线程对单个资源拥有互斥的访问权,也就是说,只能由一个线程在同一时间访问该资源。一个互斥对象包括:使用数量,线程ID,计数器。ID表明现在是哪个线程拥有这个互斥对象,计数器用于指明该线程拥有互斥对象的次数。
需要调用CreateMutex函数来创建互斥对象。通过WaitForSingleObject函数来请求互斥对象,通过ReleaseMutex函数来释放互斥对象。
可能看到这里大家还是不能明白为什么互斥对象可以实现进程间的同步作用。
首先,我们要创建一个互斥对象,此时这个互斥对象处于一个有信号状态,当我们用wait函数来请求一个互斥对象的时候, 那么我们此时的这个线程是可以获得互斥对象的访问权,从而开始执行wait函数后面的共享代码区,同时,当调用一次wait的时候,互斥对象就从有信号变为无信号状态,因为互斥对象就是保证对单个资源的互斥访问,如果此时还有其他的新的线程来请求互斥对象的访问权从而执行共享代码的话,那么此时他得到的互斥对象是无信号状态,是不能够继续执行的,此时的线程会处于等待状态。当第一个线程执行完共享代码区的时候,我们要释放现在所使用的这个互斥对象,即用releasemutex来进行释放,他会将互斥对象从无信号状态设置为有信号状态,此时根据请求的先后顺序,之前在wait函数那里等待互斥对象信号的两个线程中的一个,就会被允许获得互斥的对象的访问权,从而执行共享存储区的代码。
其实上面的互斥对象和操作系统中的PV操作是同理的。
共享代码就是在wait函数和release函数中间的代码,每次只能允许一个线程获得互斥对象的访问权从而进入到临界区里面。wait是请求操作,release是恢复操作。
互斥对象还有一个重要的机制,就是他包括的线程的ID.如果是线程1请求了一个互斥对象,那么如果线程二想用这个互斥对象之前,必须由线程1进行释放,在释放的过程中,互斥对象会拿出他维护的线程ID和释放它的线程进行比较,只有两者相同的情况下才能够释放这个互斥对象,否则如果线程ID和释放者不一样 ,则不能够释放互斥对象。
CreateMutex函数中的第二个参数有两个取值,一个是false,一个是true.false代表当前的线程不拥有这个互斥对象,互斥对象处于有信号状态,true呢表示当前的线程拥有这个互斥对象,即当前的线程和该互斥对象绑定,互斥对象处于无信号状态,相当于默认的执行了wait函数。如果此时在这个线程里面再调用wait函数,虽然此时的互斥对象是无信号状态,但是因为调用他的线程和互斥对象内部维护的线程ID是相等的,所以依然会获得该互斥对象的控制权。但是此时有一个非常重要的一点,就是这个互斥对象的计数器+1了。一开始就说过,互斥对象的计数器代表了拥有这个互斥对象的线程拥有它的次数,所以每当调用一次wait函数的时候,计数器都会+1.所以,如果说当前线程不需要这个互斥对象的时候,需要调用两次释放函数,计数器才会减为0,这也就告诉了我们,释放函数的功能实际上就是让计数器-1.
3、关于互斥对象还有一个很重要的功能:
当一个线程里面利用wait函数请求互斥对象执行完毕之后,在线程中并没有调用release函数来释放这个对象,所以在线程结束之后操作系统如果发现线程已经终止的话,他会自动帮我们释放掉这个互斥对象,把它变为有信号状态。
4、如何保证只有一个实例运行:
我们都知道程序只是代码,一个程序运行起来可以有多个实例,那么如果只让这个程序只有一个实例在运行呢。解决办法就是利用命名的互斥对象来实现。原因就是,如果在调用创建互斥对象的函数的时候,如果之前已经有该命名的互斥对象存在,那么就返回已经创建的互斥对象,不再创建 新的,这时getlasterror将返回error_alreday_exists。
所以,如果createmutex返回的是一个有效句柄,接下来就要判断getlasterror的返回值是什么,从而断定我们 是不是在先前已经创建过了一个互斥对象,也就相当于已经有一个实例正在运行。