进程和线程是操作系统里面经常遇到的两个概念,还有一个概念,是应用程序。应用程序包括指令和数据,在开始运行之前,只是分布在磁盘上的指令和数据。正在执行的应用程序称为进程,进程不仅仅是指令和数据,它还有状态。状态是保存在处理器寄存器中的一些值,记录一些信息,比如说当前执行指令的地址,保存在内存中的值等。进程是应用程序的基本构件块,同时运行的多个应用程序就是多个进程。每个进程可以运行多个线程。线程也有一些状态,但线程的状态基本上只是保存在其寄存器中的值以及其栈上的数据。线程与同一个应用程序中的其他线程共享很多状态。进程的优点是每个进程是独立的,一个进程的死掉对其他正在运行的进程没有任何影响,多进程的缺点是每个进程都需要自己的TLB(Translation
Look-aside Buffer,转换旁视缓冲器)条目,从而增加了TLB条目和缓存的未命中率,多进程还有一个缺点,就是进程之间共享数据需要显式控制,这种操作的开销比较大。多线程的有点事多线程之间共享数据的成本低,因为某个线程可以将数据项存储到内存,且该数据立刻对此进程中的所有其他线程可见,另外一个优点是所有线程共享相同的TLB和缓存条目,所以多线程应用程序的缓存未命中率较低。缺点是一个线程失败就很有可能导致整个应用程序终止。比如说,浏览器是多进程的,可以使用浏览器打开多个标签页,每个标签页是一个单独的进程,一个标签页的失败不会导致整个浏览器的崩溃。如果浏览器做成多线程的,如果一个线程执行一些bad
code,整个浏览器很可能会崩掉。
要使多线程应用程序有效地工作,必须在线程之间共享某些共有状态。当多线程以不安全得方式更新同一数据就会产生数据争用,避免数据争用的一个方法就是正确使用线程同步。同步原语包括互斥量和临界区、自旋锁、信号量、读写锁、屏障。对于线程和进程间通信,有很多机制,比如说内存、共享内存和内存映射文件、条件变量、信号和事件、消息队列、命名管道、网络栈等。
创建线程
Windows操作系统对多线程的支持大体上与POSIX线程提供的支持类似。创建Windows的本机线程可以调用CreateThread函数,该函数的返回值为所创建的线程的句柄,如果返回值为0,则说明调用不成功。
#include <Windows.h> #include <stdio.h> #include <tchar.h> DWORD WINAPI mythread(__in LPVOID lpParameter) { printf("Thread %i\n",GetCurrentThread()); return 0; } int _tmain(int argc,_TCHAR* argv[]){ HANDLE handle; handle=CreateThread(0,0,mythread,0,0,0); getchar(); return 0; }
在上面的一段代码中,调用CreateThread会令操作系统生成一个新线程,然后返回该线程的句柄,但是运行库并没有建立其所需要的线程本地数据结构。运行库也提供了两个线程创建函数_beginthread()和_beginthreadex()。这两者的区别是_beginthread()创建的线程在线程退出时会关闭线程句柄,_beginthreadex()调用返回的线程句柄则需要显式调用CloseHandle才能释放。
#include <Windows.h> #include <process.h> #include <stdio.h> #include <tchar.h> DWORD WINAPI mythread1(__in LPVOID lpParameter) { printf("CreateThread创建的线程,ID为:%i\n",GetCurrentThreadId()); return 0; } unsigned int __stdcall mythread2(void *data) { printf("__beginthreadex创建的线程,ID为:%i\n",GetCurrentThreadId()); return 0; } void mythread3(void *data) { printf("__beginthread创建的线程,ID为:%i\n",GetCurrentThreadId()); } int _tmain(int argc,_TCHAR* argv[]) { HANDLE h1,h2,h3; h1=CreateThread(0,0,mythread1,0,0,0); h2=(HANDLE)_beginthreadex(0,0,&mythread2,0,0,0); WaitForSingleObject(h2,INFINITE); CloseHandle(h2); h3=(HANDLE)_beginthread(&mythread3,0,0); getchar(); }
在多线程下面,有时希望等待某一线程完成了再继续做其他事情,要实现这个目的,可以使用Windows API函数WaitForSingleObject,或者WaitForMultipleObjects。这两个函数都会等待Object被标为有信号(signaled)时才返回。
在Windows操作系统中,经常提及一个概念,句柄,而且许多Windows API的返回值都是句柄,其实句柄说白了就是一个无符号整数。返回句柄的Windows API调用实际上是在内核空间创建某个资源,句柄只是这个资源的索引。当应用程序使用完该资源后,就可以调用CloseHandle函数让内核释放相关的内核空间的资源。
终止线程可以调用ExitThread或者TerminateThread,也可以调用库函数endthread或者endthreadex来终止线程。
如果线程处于挂起状态,则启动线程可以调用ResumeThread函数,该函数以线程的句柄为参数。SuspendThread函数可以迫使运行中的线程挂起,这个函数最好不要轻易调用,因为如果挂起线程时,线程正好持有互斥变量等这些资源,就很容易出问题,呵呵。
#include <Windows.h> #include <process.h> #include <stdio.h> #include <tchar.h> unsigned int __stdcall mythread(void *data) { printf("创建的线程ID为:%i\n",GetCurrentThreadId()); return 0; } int _tmain(int argc,_TCHAR* argv[]) { HANDLE h; h=(HANDLE)_beginthreadex(0,0,&mythread,0,CREATE_SUSPENDED,0); getchar(); ResumeThread(h); getchar(); WaitForSingleObject(h,INFINITE); CloseHandle(h); return 0; }
线程同步和资源共享
Windows提供的同步对象与POSIX规定的很相似。线程的同步还是那种方式,比如说互斥锁、临界区、读写锁、信号量、条件变量、事件等。
给个例子。
#include <Windows.h> #include <process.h> #include <stdio.h> #include <tchar.h> #include <math.h> int isPrime(int num) { int i; for (i=2;i<(int)(sqrt((float)num)+1.0);i++) { if (num%i==0) return 0; } return 1; } volatile int counter=2; unsigned int __stdcall test(void *) { while (counter<20) { int num=counter++; printf("Thread ID : %i; value = %i, is prime = %i\n",GetCurrentThreadId(),num,isPrime(num)); } return 0; } int _tmain(int argc,_TCHAR* argv[]) { HANDLE h1,h2; h1=(HANDLE)_beginthreadex(0,0,&test,(void *)0,0,0); h2=(HANDLE)_beginthreadex(0,0,&test,(void *)1,0,0); WaitForSingleObject(h1,INFINITE); WaitForSingleObject(h2,INFINITE); CloseHandle(h1); CloseHandle(h2); getchar(); return 0; }
用两个线程计算某个给定范围内的所有素数。创建两个线程,这两个线程会一直测试数字,直到2-20以内的所有数字计算完毕为止。从执行结果可以看出,系统共产生两个线程,ID分别为13788和13792,这两个线程会同时访问共享变量counter,肯定会导致数据争用这个问题,如果希望每个线程测试不同的数字,有必要采取一定的措施对共享变量counter的操作进行保护。显示顺序的不同是因为线程完成对一个数字的判断所用的时间与函数调用printf输出显示的时间有差距。
解决的方案有很多,无非就是解决线程同步的经典措施。
第一种方法,可以添加对临界区代码的访问保护,确保仅有单个线程执行。临界区的声明可以调用InitializeCriticalSection(),调用DeleteCriticalSection()删除临界区,如果线程希望进入临界区,可以调用EnterCriticalSection()函数,如果此时临界区中午其他线程,调用线程就能进入临界区并执行相关的代码;如果临界区有线程,则调用线程将休眠,直到正在执行临界区的线程调用LeaveCriticalSection()离开临界区。调用EnterCriticalSection()的线程会直到获得临界区的访问权才会离开,没有什么超时不超时的概念,呵呵。
#include <Windows.h> #include <process.h> #include <stdio.h> #include <tchar.h> #include <math.h> int isPrime(int num) { int i; for (i=2;i<(int)(sqrt((float)num)+1.0);i++) { if (num%i==0) return 0; } return 1; } volatile int counter=2; CRITICAL_SECTION critical; unsigned int __stdcall test(void *) { while (counter<20) { EnterCriticalSection(&critical); int num=counter++; LeaveCriticalSection(&critical); printf("Thread ID : %i; value = %i, is prime = %i\n",GetCurrentThreadId(),num,isPrime(num)); } return 0; } int _tmain(int argc,_TCHAR* argv[]) { HANDLE h1,h2; InitializeCriticalSection(&critical); h1=(HANDLE)_beginthreadex(0,0,&test,(void *)0,0,0); h2=(HANDLE)_beginthreadex(0,0,&test,(void *)1,0,0); WaitForSingleObject(h1,INFINITE); WaitForSingleObject(h2,INFINITE); CloseHandle(h1); CloseHandle(h2); getchar(); DeleteCriticalSection(&critical); return 0; }
实际上,使线程休眠后再唤醒线程比较耗时,因为这涉及到进入内核。因为很可能线程进入休眠时,原来已经处于临界区的线程已经离开了,此时让等待的线程休眠后再唤醒就有点扯淡了。。。可以调用TryEnterCriticalSection()立即返回,返回值为真时表示该线程获得对临界区的访问权。此时可以将test函数修改一下,
unsigned int __stdcall test(void *) { while (counter<20) { while (!TryEnterCriticalSection(&critical)){} int num=counter++; LeaveCriticalSection(&critical); printf("Thread ID : %i; value = %i, is prime = %i\n",GetCurrentThreadId(),num,isPrime(num)); } return 0; }
这样做也有问题,会保持进程持续Try,直到获得临界区的访问权,这样会剥夺其他线程的处理器时间。解决的方法是让想进入临界区的线程短暂等待,类似添加了一个等待时间,超时就会离开。两种方法,一种设置调用EnterCriticalSection的线程进入休眠前旋转的次数,一种是通过初始化调用InitializeCriticalSectionAndSpinCount()初始化临界区,参数为指向临界区的指针和旋转的次数,也可以通过调用SetCriticalSectionSpinCount()设置已创建临界区的旋转次数。
第二种方案是用互斥量保护代码段。互斥量是内核对象,所以能在进程之间共享。通过调用CreateMutex或者CreateMutexEx创建互斥量。为了获取互斥变量,调用WaitForSingleObject,要么已经获得互斥量要么在指定的超时后返回。线程完成后,调用ReleaseMutex释放互斥量所保护的代码段。
#include <Windows.h> #include <process.h> #include <stdio.h> #include <tchar.h> #include <math.h> int isPrime(int num) { int i; for (i=2;i<(int)(sqrt((float)num)+1.0);i++) { if (num%i==0) return 0; } return 1; } volatile int counter=2; HANDLE mutex; unsigned int __stdcall test(void *) { while (counter<20) { WaitForSingleObject(mutex,INFINITE); int num=counter++; ReleaseMutex(mutex); printf("Thread ID : %i; value = %i, is prime = %i\n",GetCurrentThreadId(),num,isPrime(num)); } return 0; } int _tmain(int argc,_TCHAR* argv[]) { HANDLE h1,h2; mutex=CreateMutex(0,0,0); h1=(HANDLE)_beginthreadex(0,0,&test,(void *)0,0,0); h2=(HANDLE)_beginthreadex(0,0,&test,(void *)1,0,0); WaitForSingleObject(h1,INFINITE); WaitForSingleObject(h2,INFINITE); CloseHandle(h1); CloseHandle(h2); getchar(); CloseHandle(mutex); return 0; }
注:volatile与const一样,volatile是一个类型修饰符(type
specifier)。它是被设计用来修饰被不同线程访问和修改的变量。如果不加入volatile,基本上会导致这样的结果:要么无法编写多线程程序,要么编译器失去大量优化的机会。
第三种方案是使用轻量级读写锁。锁在数据库里面是一个经常出现的概念,锁的性质就是允许多个线程对数据具有读访问权限,或者单个线程对数据具有写访问权限。可以调用InitializeSRWLock()对锁进行初始化,锁本质上是用户变量,不使用内核资源。作为读者获取锁调用AcquireSRWLockShared(),作为读者释放锁调用ReleaseSRWLockShared(),写者获取锁调用AcquireSRWLockExclusive(),写者释放锁调用ReleaseSRWLockExclusive()。
#include <Windows.h> #include <process.h> #include <stdio.h> #include <tchar.h> int array[100][100]; SRWLOCK lock; unsigned int __stdcall write(void *param) { for (int y=0;y<100;y++) { for (int x=0;x<100;x++) { AcquireSRWLockExclusive(&lock); array[x][y]++; array[y][x]--; ReleaseSRWLockExclusive(&lock); } } return 0; } unsigned int __stdcall read(void *param) { int value=0; for (int y=0;y<100;y++) { for (int x=0;x<100;x++) { AcquireSRWLockShared(&lock); value=array[x][y]+array[y][x]; ReleaseSRWLockShared(&lock); } printf("Value = %i\n",value); return value; } } int _tmain(int argc,_TCHAR* argv[]){ HANDLE h1,h2; InitializeSRWLock(&lock); h1=(HANDLE)_beginthreadex(0,0,&write,(void *)0,0,0); h2=(HANDLE)_beginthreadex(0,0,&read,(void *)0,0,0); WaitForSingleObject(h1,INFINITE); WaitForSingleObject(h2,INFINITE); CloseHandle(h1); CloseHandle(h2); getchar(); return 0; }
第四种方案是使用信号量。调用CreateSemaphore和CreateSemaphoreEx可以创建信号量,调用OpenSemaphore可以获得某个信号量的句柄。信号量是内核对象,创建函数会返回其句柄,调用CloseHandle释放。信号量通过调用等待函数WaitForSingleObject,其参数为信号量句柄和超时,返回递减后的信号量,否则在达到超时后返回。调用ReleaseSemaphore()递增信号量,其参数为信号量句柄、信号地增量以及一个指向LONG型变量的可选指针,信号量之前的值写入该LONG型变量。
给个例子,信号量创建为最大值为1、初始值为1,创建了两个线程,同时执行相同的代码,将变量value的值增加200,最终应用程序终止时,变量value的值变为400.
#include <Windows.h> #include <process.h> #include <stdio.h> #include <tchar.h> HANDLE semaphore; int value; void add(int num) { WaitForSingleObject(semaphore,INFINITE); value+=num; ReleaseSemaphore(semaphore,1,0); } unsigned int __stdcall test(void *) { for (int counter=0;counter<100;counter++) { add(2); } return 0; } int _tmain(int argc,_TCHAR* argv[]){ HANDLE h1,h2; value=0; semaphore=CreateSemaphore(0,1,1,0); h1=(HANDLE)_beginthreadex(0,0,&test,(void *)0,0,0); h2=(HANDLE)_beginthreadex(0,0,&test,(void *)0,0,0); WaitForSingleObject(h1,INFINITE); WaitForSingleObject(h2,INFINITE); CloseHandle(h1); CloseHandle(h2); CloseHandle(semaphore); printf("Value = %i\n",value); getchar(); return 0; }
第五种解决方案是条件变量,需要与临界区或者轻量级读写锁共同使用,使线程可以进入休眠,直到为真。比如说,对于生产者——消费者问题,生产者线程负责将数据添加到队列,消费者线程进入临界区,从队列中删除一个数据。这个问题用条件变量就可以解决。。。
第六种方案就是向其他线程或者进程发出事件完成信号。事件用于向一个或者多个线程发出信号,表明某个事件已经发生。等待某个事件发生的线程将等待该事件对象。完成任务的线程将设置事件为信号已发送状态,然后等待线程将被释放。事件有两种类型:手动重置和自动重置。事件是内核对象,调用CreateEvent将返回一个句柄,OpenEvent打开现有的事件,SetEvent()将事件设置为信号已经发出的状态。自动重置: SetEvent之后,事件自动重置为未触发状态,手动重置: SetEvent之后,
需要调用ResetEvent事件才置为未触发状态。当一个手动重置事件被触发的时候,正在等待该事件的所有线程都变为可调度状态;当一个自动重置事件被触发的时候,只有一个正在等待该事件的线程会变为可调度状态。
给个例子,调用CreateEvent创建事件对象,该对象需要手动重置,且创建为未发信号状态。然后创建两个线程,第一个等待该事件,第二个执行输出一条消息,然后发送给事件对象。信号使第一个线程继续执行,并输出第二条消息。
#include <Windows.h> #include <process.h> #include <stdio.h> #include <tchar.h> HANDLE event; unsigned int __stdcall thread1(void *param) { WaitForSingleObject(event,INFINITE); printf("Thread 1 done \n"); return 0; } unsigned int __stdcall thread2(void *param) { printf("Thread 2 done \n"); SetEvent(event); return 0; } int _tmain(int argc,_TCHAR* argv[]){ HANDLE h1,h2; event=CreateEvent(0,0,0,0); h1=(HANDLE)_beginthreadex(0,0,&thread1,0,0,0); h2=(HANDLE)_beginthreadex(0,0,&thread2,0,0,0); WaitForSingleObject(h1,INFINITE); WaitForSingleObject(h2,INFINITE); CloseHandle(h2); CloseHandle(h1); CloseHandle(event); getchar(); return 0; }
版权声明:本文为博主原创文章,未经博主允许不得转载。