Windows多线程问题

进程和线程是操作系统里面经常遇到的两个概念,还有一个概念,是应用程序。应用程序包括指令和数据,在开始运行之前,只是分布在磁盘上的指令和数据。正在执行的应用程序称为进程,进程不仅仅是指令和数据,它还有状态状态是保存在处理器寄存器中的一些值,记录一些信息,比如说当前执行指令的地址,保存在内存中的值等。进程是应用程序的基本构件块,同时运行的多个应用程序就是多个进程。每个进程可以运行多个线程。线程也有一些状态,但线程的状态基本上只是保存在其寄存器中的值以及其栈上的数据。线程与同一个应用程序中的其他线程共享很多状态。进程的优点是每个进程是独立的,一个进程的死掉对其他正在运行的进程没有任何影响,多进程的缺点是每个进程都需要自己的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;
}

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-10 07:34:24

Windows多线程问题的相关文章

Windows多线程编程总结

Windows 多线程编程总结 keyword:多线程 线程同步 线程池 内核对象 1 内核对象 1 .1 内核对象的概念 内核对象是内核分配的一个内存块,这样的内存块是一个数据结构,表示内核对象的各种特征.而且仅仅能由内核来訪问.应用程序若须要訪问内核对象,须要通过操作系统提供的函数来进行,不能直接訪问内核对象( Windows 从安全性方面来考虑的). 内核对象通过 Create* 来创建,返回一个用于标识内核对象的句柄,这些句柄 (而不是内核对象)可在创建进程范围内使用,不可以被传递到其它

windows多线程同步

概述 任何单个应用程序都不能完全使该处理器达到满负荷.当一个线程遇到较长等待时间事件时,同步多线程还允许另一线程中的指令使用所有执行单元.例如,当一个线程发生高速缓存不命中,另一个线程可以继续执行.同步多线程是 POWER5? 和 POWER6? 处理器的功能,可与共享处理器配合使用. SMT 对于商业事务处理负载的性能优化可达30%.在更加注重系统的整体吞吐量而非单独线程的吞吐量时,SMT 是一个很好地选择. 但是并非所有的应用都能通过SMT 取得性能优化.那些性能受到执行单元限制的应用,或者

windows多线程接口介绍和使用

一windows多线程接口: 1 创建线程 CreateThread 与 _beginthreadex都可以实现创建线程,两个函数的参数 相同, HANDLEWINAPICreateThread( LPSECURITY_ATTRIBUTESlpThreadAttributes, SIZE_TdwStackSize, LPTHREAD_START_ROUTINElpStartAddress, LPVOIDlpParameter, DWORDdwCreationFlags, LPDWORDlpThr

Windows多线程编程及常见问题

提要: Windows 多线程Helloworld 以Windows代码为例,分析多线程编程中易出现的问题 Windows多线程的Helloworld: 笔者写过Java多线程的程序(实现Runnable接口,利用Thread类执行),也写过Linux多线程程序(利用pthread).最近由于另有需要使用Windows多线程,由于Windows API历来难用,特此记录,以作备忘. Helloworld源代码如下: 1 #include <stdio.h> 2 #include <win

Windows多线程多任务设计初步(转)

Windows多线程多任务设计初步 [前言:]当前流行的Windows操作系统,它能同时运行几个程序(独立运行的程序又称之为进程),对于同一个程序,它又可以分成若干个独立的执行流,我们称之为线程,线程提供了多任务处理的能力.用进程和线程的观点来研究软件是当今普遍采用的方法,进程和线程的概念的出现,对提高软件的并行性有着重要的意义.现在的应用软件无一不是多线程多任务处理,单线城的软件是不可想象的.因此掌握多线程多任务设计方法对每个程序员都是必需要掌握的.本文针对多线程技术在应用中经常遇到的问题,如

【APUE】关于windows多线程编程的学习笔记

保证在某一时刻只有一个线程对数据进行操作的基本方法: (1)关中断:通过关闭时钟中断来停止线程调度(不现实) (2)数学互斥方法:Peterson算法 bakery算法 (3)操作系统提供的互斥方法:临界区.互斥量.信号量等(windows) (4)cpu原子操作:把一些常用的指令设计成了原子指令,在windows上面也被称为原子锁 [APUE]关于windows多线程编程的学习笔记

Windows多线程开发之并发线程程序研究

做为一名分布式服务器开发人员,在服务器开发领域.多线程开发和并发编程方面有自己的心得和经验,愿意分享给同仁,今讨论下Windows下线程并发程序开发. 下面用用两个线程实现一个简单的数组排序,演示了线程的基本用法. 原理是: 为了节省执行时间而添加并行,把问题划分为几个小问题,并分配给几个线程(分而治之),把问题划分成若干更小的单元,更容易在实现中创建并行逻辑.同时,在并行中使用系统资源能优化应用程序并提高其运行速度. #include "stdafx.h"  #include <

Windows多线程初探

线程是进程中的一个执行单位(每个进程至少有一个主线程),一个进程可以有多个线程,而一个线程只存在于一个进程中.在数据关系上属于一对多的关系.线程不占有系统资源,它所使用的资源全部由所属进程向系统申请. 在多处理器中,不同的线程可以同时运行在不同的CPU上,这样可以提高程序的运行效率.除此之外,有些时候必须使用多线程.例如,杀毒软件在查杀病毒的时候,它需要一边扫描相关的磁盘文件,一边显示当前的扫描进度以及发现的问题.如果把这几个工作放在一个线程中执行,会让程序看上去像卡住一样.在这种情况下,分为多

windows多线程编程(一)(转)

源出处:http://www.cnblogs.com/TenosDoIt/archive/2013/04/15/3022036.html CreateThread:Windows的API函数(SDK函数的标准形式,直截了当的创建方式,任何场合都可以使用),提供操作系统级别的创建线程的操作,且仅限于工作者线程 beginthread beginthreadex:MS对C Runtime库的扩展SDK函数,首先针对C Runtime库做了一些初始化的工作,以保证C Runtime库工作正常,然后,调