线程概念、线程创建、线程同步

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 关键段与互斥量比较

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章