1、前言
线程对于程序开发而言是一个很重要的概念,由于在实际的项目开发过程中经常会用到线程、多线程技术,所以就对线程的概念与使用进行一下简单的总结,并对线程相关的概念如程序、进程、线程同步、线程池等概念也会进行相关的介绍。由于不同的环境、平台会用到不同的线程开发技术,所以在本文章中也会对其他不同平台的线程技术进行简单的介绍。由于内容较多,所以具体更新时间不定。
2、概述
在使用线程技术之前,需要先介绍一下进程的概念,然后说明一下进程与线程的不同与联系,以免造成混淆。
2.1、进程
说到进程,我们可能首先会想到程序。因为对于开发人员来说,程序是可见的,即是最终生成的可执行 *.exe 文件,而进程是看不见的。所以大部分人也就会认为程序就是进程。这样理解是不正确的。
程序是计算机指令的集合,它以文件的形式存储在磁盘上,使我们经常见到的*.exe程序;而进程通常被定义为一个正在运行的程序的实例,是一个程序在其自身的地址空间中的一次执行活动。由此也可以看出,程序与进程的不同。程序是“静止”的,而进程是“活动”的,进程是在地址空间中执行程序指令集合的一次过程。
程序可以有多个进程。比如说,经常使用的记事本程序,我们能够打开多个记事本文件,每一个记事本文件都是一个执行中的进程。
进程是资源申请、调度和独立运行的单位,因此,它使用系统中的运行资源;而程序不能申请系统资源,不能被系统调度,也不能作为独立运行的单位,所以,程序不占用系统的运行资源。
2.2、进程的组成
进程由两部分组成:
(1)、操作系统用来管理进程的内核对象
内核对象是系统用于存储关于进程的统计信息的地方。内核对象是操作系统内部分配的一个内存块,用于存储、维护该对象的各种信息。内核对象的数据结构只能被内核访问并使用,所以只能通过Windows提供的一个系统函数对内核对象进行操作。
(2)、地址空间
它包含所有的可执行模块或DLL模块的代码和数据。同时也包含动态内存分配的空间,例如线程的栈和堆的分配空间。
系统为每一个进程都分配了独立的虚拟地址空间。对于32位系统来说,它的寻址范围为2的32次方,即4GB,所以对于32位进程来说,分配的虚拟地址空间大小为4GB。
2.3、进程与线程的关系
进程是不执行任何东西的,它只是线程的容器,进程是靠线程去执行操作的。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程。此线程负责执行包含在进程地址空间中的代码。也就是说,真正完成代码执行的是线程,而进程只是线程的容器,或者说是线程的执行环境。
一个进程可以拥有多个线程,每个进程至少拥有一个线程。当创建一个进程时,操作系统会自动创建这个进程的第一个线程。当然我们可能感觉不到,因为这个第一个线程被称为主线程,main()函数或者WinMain()函数可以被当为该主线程的入口函数。然后就可在主线程中去完成其它线程的创建了。
3、线程
线程有两部分组成:
(1)、线程的内核对象
操作系统通过线程的内核对象对线程实施管理,同时内核对象也是系统用来存放线程统计信息的地方。
(2)、线程栈
当创建一个线程时,系统会为该线程分配一个线程内核对象。系统可已通过该内核对象对线程进行相关的操作。
线程总是在某个进程环境中创建的。系统从进程的地址空间中分配内存,供线程的栈使用。新线程运行的进程环境与创建线程的环境相同。因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同进程中的其他线程的堆栈。这使得单个进程中多个线程能够非常容易的互相通信。
线程只有一个内核对象和一个栈,保留的记录很少,因此需要的内存也很少。由于线程需要的开销比进程少,因此在编程中进场采用多线程解决编程问题。
操作系统为每一个运行线程安排一定的CPU时间---时间片。系统通过一种循环的方式为线程提供时间片,线程在自己的时间片中运行,因为时间片很短,因此就会造成多个线程同时进行的错觉。如果在多CPU环境下,就会实现真正意义上的多线程同时运行。
3.1、线程的创建
3.1.1 CreatThread函数
在Windows环境下,线程的创建可以通过Windows API:CreatThread函数来完成的。CreatThread是Windows API,是最基础的创建线程的函数。
CreatThread函数声明如下:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程的安全性属性,可以设置为NULL,使该线程使用默认安全属性
DWOED dwStaticSize, //设置线程的初始栈大小,即线程可以将多少地址空间用于它自己的栈,已字节为单位。可以设置为0,表示默认使用与调用函数的线程相同的栈空间大小
LPTHREAD_START_ROUTINE lpStartAddress, //指向应用程序定义的该类型函数的指针,该函数将由新线程执行,表明新线程的起始地址
LPVOID lpRarameter, //通过该参数给创建的新线程传递参数
DWOED dwCreationFlags, //用于设置控制线程创建的附加标记。可以将值设为0,表示,线程创建之后就立即执行
LPWORD lpThreadId //该参数是一个返回值,用于接收线程ID,可以将值设为NULL,表示对线程ID不感兴趣
);
其中,lpStartAddress表示为该线程的入口函数。入口函数的定义需遵照如下的声明形式:
DWORD WINAPI ThreadFun(LPVOID lpParameter);
该新线程入口函数有一个LPVOID类型的参数,并且返回值是DWORD类型。
线程创建实例如下所示:
DWORD WINAPI ThreadFun(LPVOID lp);
void main()
{
HANDLE hThread1;
hThread1 = CreateThread(NULL, 0, ThradFun, 0, NULL);
CloseHandle(hThread1); //关闭线程
}
DWORD WINAPI ThreadFun(LPVOID lp)
{
***
return 0;
}
3.1.2 _beginthreadex函数
_beginthreadex函数C语言运行库函数,当在windows下使用C/C++进行编程时,优先使用该函数进行创建线程的操作。_beginthreadex函数原型如下所示,需包含头文件<windows.h>,<process.h>:
uintptr_t _beginthreadex(
void *security,//线程安全性,指定是否可以被继承,默认为NULL,表示不可继承
unsigned stack_size,//新线程的堆栈大小,默认为0,含义与CreateThread函数一样
unsigned ( __stdcall *start_address )( void * ),//线程调用函数,unsigned __stdcall Func(void *) 类型的函数指针
void *arglist,//参数列表
unsigned initflag,//表示新线程的初始状态,默认为0,表示立即执行
unsigned *thrdaddr//表示线程的ID
);
线程调用函数原型:
unsigned __stdcall ThreadFunc(void *pPara);
应用实例:
#include <Windows.h>
#include <process.h>
unsigned __stdcall thread_1_func(void *pNull)
{
printf(">>> 启动线程 1\n");
printf("\n>>> 退出线程 1\n");
return 0;
}
unsigned __stdcall thread_2_func(void *pNull)
{
printf(">>> 启动线程 2\n");
printf("\n>>> 退出线程 2\n");
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
HANDLE hThread_1, hThread_2;
unsigned threadId_1, threadId_2;
hThread_1 = (HANDLE)_beginthreadex(NULL, 0, &thread_1_func, NULL, 0, &threadId_1);
hThread_2 = (HANDLE)_beginthreadex(NULL, 0, &thread_2_func, NULL, 0, &threadId_2);
char ch = '0';
while( ch != 'q' )
{
ch = getchar();
}
return 0;
}
3.1.3 AfxBeginThread函数
AfxBeginThread函数是在当使用MFC编程时,MFC提供的一种创建线程的方式,在MFC环境下,优先考虑使用该函数。
MFC提供了两种AfxBeginThread的重载函数定义,一种是用于用户界面线程,一种是用于工作者线程,着重介绍工作者线程的创建。
AfxBeginThread函数原型如下所示:
CWinThread AfxBeginThread(
AFX_THREADPROC pfnThreadProc, //线程的入口函数
LPVOID pParam, //传递如线程的参数,如果无参数,置为NULL
int nPriority, //指定线程的优先级,如果为0,表示与创建该线程的线程优先级相同
UINT nStackSize, //指定线程的堆栈大小,如果为0,表示与创建该线程的线程优先级相同
DWORD dwCreateFlags,//创建标识,默认为0
LPSECURITY_ATTRIBUTES lpSecurityAttrs //线程的安全属性,NT下有用
);
线程入口函数定义:
UINT AFXThreadFunc(LPVOID pParam);
应用实例:
CWinThread *thread = AfxBeginThread( AfxThreadFunc, NULL, 0, 0, 0, NULL);
if(thread == NULL)
{
// 创建线程失败!
}
UINT AfxThreadFunc(LPVOID pParam)
{
// 进入线程入口函数
return 0;
}
3.2、线程的同步
在多线程运行环境中,如果有多个线程要对同一块内存地址进行操作,则必须要考虑线程同步的问题。线程同步说白了就是,保证在某一时刻只有一个线程去操作那同一块内存地址。线程同步可以采用多种方式:关键段方式、内核对象方式。下main就简单介绍一下同步的使用。
3.2.1 关键段(CriticalSection)
1、初始化关键段
CRITICAL_SECTION g_cs;
InitializeCriticalSectionAndSpinCount(LPCRITICAL_SECTION lp_cs, DWORD spin_count);
/* 之所以使用带旋转锁的初始化方式,是因为:当线程试图进入一个关键段时,如果该关键段正在被另一个线程
占用,函数会立即把调用线程切换到等待状态。意味着线程必须从用户模式切换到内核模式,而这种切换的开销非
常大。为了提高关键段的性能,把旋转锁合并到关键段中,当调用EnterCriticalSection时,他会用一个旋转锁
不断的循环,会尝试在一段时间内获得对资源的访问权。*/
该函数的第一个参数为定义的关键段结构的地址,第二个参数为希望旋转的次数。可以默认设为4000(用来保护进程堆的关键段使用的旋转次数大约为4000)
2、进入关键段
EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection); //进入关键段
3、离开关键段
LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection); //离开关键段
4、销毁关键段
DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection); //销毁关键段
5、实例
#include <Windows.h>
#include <process.h>
CRITICAL_SECTION g_cs; // 定义关键段结构
int g_count = 0;
unsigned __stdcall thread_1_func(void *pNull)
{
printf(">>> 启动线程 1\n");
EnterCriticalSection(&g_cs);
g_count ++;
LeaveCriticalSection(&g_cs);
printf("\n>>> 退出线程 1\n");
return 0;
}
unsigned __stdcall thread_2_func(void *pNull)
{
printf(">>> 启动线程 2\n");
EnterCriticalSection(&g_cs);
g_count ++;
LeaveCriticalSection(&g_cs);
printf("\n>>> 退出线程 2\n");
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
HANDLE hThread_1, hThread_2;
unsigned threadId_1, threadId_2;
InitializeCriticalSectionAndSpinCount(&g_cs, spin_count);
hThread_1 = (HANDLE)_beginthreadex(NULL, 0, &thread_1_func, NULL, 0, &threadId_1);
hThread_2 = (HANDLE)_beginthreadex(NULL, 0, &thread_2_func, NULL, 0, &threadId_2);
DeleteCriticalSection(&g_cs);
return 0;
}
3.2.2 互斥量(Mutex)
1、创建互斥量
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lp_ma, BOOL bInitialOwner, LPCSTR lpName);
2、等待互斥量,以获取使用权
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
3、释放互斥量,放弃使用权
BOOL ReleaseMutex(HANDLE hMutex);
4、销毁互斥量
CloseHandle(HANDLE hHandle);
5、实例
#include <Windows.h>
#include <process.h>
HANDLE g_hmu; // 定义关键段结构
int g_count = 0;
unsigned __stdcall thread_1_func(void *pNull)
{
printf(">>> 启动线程 1\n");
WaitForSingleObject(g_hmu, INFINITE);
g_count ++;
ReleaseMutex(g_hmu);
printf("\n>>> 退出线程 1\n");
return 0;
}
unsigned __stdcall thread_2_func(void *pNull)
{
printf(">>> 启动线程 2\n");
WaitForSingleObject(g_hmu, INFINITE);
g_count ++;
ReleaseMutex(g_hmu);
printf("\n>>> 退出线程 2\n");
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
HANDLE hThread_1, hThread_2;
unsigned threadId_1, threadId_2;
g_hmu = CreateMutex(NULL, FALSE, NULL);
hThread_1 = (HANDLE)_beginthreadex(NULL, 0, &thread_1_func, NULL, 0, &threadId_1);
hThread_2 = (HANDLE)_beginthreadex(NULL, 0, &thread_2_func, NULL, 0, &threadId_2);
CloseHandle(g_hmu);
return 0;
}
3.2.3 关键段与互斥量比较