天天看点

线程线程

线程

一、简介

1.定义

线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。

另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。

线程也有就绪、阻塞和运行三种基本状态。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。线程是程序中一个单一的顺序控制流程。在单个程序中同时运行多个线程完成不同的工作,称为多线程。

2.特点

在多线程OS中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。

1)轻型实体

线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源,比如,在每个线程中都应具有一个用于控制线程运行的线程控制块TCB,用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。

2)独立调度和分派的基本单位

在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。

3)可并发执行

在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。

4)共享进程资源

在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。

3.线程与进程的比较

进程是资源分配的基本单位。所有与该进程有关的资源,都被记录在进程控制块PCB中。以表示该进程拥有这些资源或正在使用它们。另外,进程也是抢占处理机的调度单位,它拥有一个完整的虚拟地址空间。当进程发生调度时,不同的进程拥有不同的虚拟地址空间,而同一进程内的不同线程共享同一地址空间。与进程相对应,线程与资源分配无关,它属于某一个进程,并与进程内的其他线程一起共享进程的资源。

线程只由相关堆栈(系统栈或用户栈)寄存器和线程控制表TCB组成。寄存器可被用来存储线程内的局部变量,但不能存储其他线程的相关变量。

通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度,从而显著提高系统资源的利用率和吞吐量。因而近年来推出的通用操作系统都引入了线程,以便进一步提高系统的并发性,并把它视为现代操作系统的一个重要指标。

线程与进程的区别可以归纳为以下4点:

1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。

2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。

3)调度和切换:线程上下文切换比进程上下文切换要快得多。

4)在多线程OS中,进程不是一个可执行的实体。

二、线程创建函数

微软在Windows API中提供了建立新的线程的函数CreateThread,当使用CreateProcess调用时,系统将创建一个进程和一个主线程。

CreateThread将在主线程的基础上创建一个新线程,大致做如下步骤: 

1在内核对象中分配一个线程标识/句柄,可供管理,由CreateThread返回 

2把线程退出码置为STILL_ACTIVE,把线程挂起计数置1 

3分配context结构 

4分配两页的物理存储以准备栈,保护页设置为PAGE_READWRITE,第2页设为PAGE_GUARD 

5 lpStartAddr和lpvThread值被放在栈顶,使它们成为传送给StartOfThread的参数 

6把context结构的栈指针指向栈顶(第5步)指令指针指向startOfThread函数 

MSDN中CreateThread原型: 

HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, 
  DWORD dwStackSize, 
  LPTHREAD_START_ROUTINE lpStartAddress, 
  LPVOID lpParameter, 
  DWORD dwCreationFlags, 
  LPDWORD lpThreadId); 
           

参数说明: 

lpThreadAttributes:指向 SECURITY_ATTRIBUTES 型态的结构的指针。在 Windows 98 中忽略该参数。在 Windows NT 中,它被设为 NULL ,表示使用缺省值。 

dwStackSize:线程堆栈大小,一般 =0 ,在任何情况下, Windows 根据需要动态延长堆栈的大小。 

lpStartAddress:指向线程函数的指针,形式: @ 函数名,函数名称没有限制,但是必须以下列形式声明: 

   DWORD WINAPI ThreadProc (LPVOID pParam) ,格式不正确将无法调用成功。 

lpParameter:向线程函数传递的参数,是一个指向结构的指针,不需传递参数时,为 NULL 。 

dwCreationFlags :线程标志 , 可取值如下 

CREATE_SUSPENDED: 创建一个挂起的线程 

  0 :创建后立即激活。 

lpThreadId:保存新线程的id。 

返回值:

函数成功,返回线程句柄;函数失败返回false。 

函数说明:

创建一个线程。语法: 

hThread = CreateThread (&security_attributes, dwStackSize, ThreadProc,pParam, dwFlags, &idThread) ; 

  一般并不推荐使用 CreateTheard函数,而推荐使用RTL 库里的System单元中定义的 BeginTheard函数,因为这除了能创建一个线程和一个入口函数以外,还增加了几项保护措施。 

三、多线程示例

#include "stdafx.h"
#include<windows.h>//需要访问Windows API函数CreateThread
#include <iostream>
using namespace std;
DWORD WINAPI Fun1Proc(//注意线程函数的声明形式
LPVOID pParam //thread data
);
DWORD WINAPI Fun2Proc(LPVOID pParam);
int index=0;
int _tmain(int argc, _TCHAR* argv[])
{
HANDLE hThread1;
HANDLE hThread2;
hThread1 = CreateThread(NULL,//让新线程使用默认的安全性
0,//让新线程采用与调用线程一样的栈大小
Fun1Proc,//指定线程1入口函数的地址
NULL,//传递给线程1的参数 可以是结构体(包含多个数据),此处不需要使用这个参数 设为NULL
0,//线程创建标记 设为0 让线程一旦创建就立即运行
NULL);//新线程的ID 这里不需要使用该ID 设为NULL
CloseHandle(hThread1);//关闭新线程的句柄
hThread2 = CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
CloseHandle(hThread2);//关闭新线程的句柄
//while (++index < 1000)
cout<<"main thread is running\r\n"<<endl;
//Sleep(10);//延时10ms
system("pause");
return 0;
}
//线程1的入口函数
DWORD WINAPI Fun1Proc(LPVOID pParam)
{
//while (++index < 1000)
cout<<"thread1 is running\r\n"<<endl;
return 0;
}
 
//线程1的入口函数
DWORD WINAPI Fun2Proc(LPVOID pParam)
{
//while (++index < 1000)
cout<<"thread2 is running\r\n"<<endl;
return 0;
}
           

运行结果:

线程线程

四、线程同步

4.1定义

同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。“同”字从字面上容易理解为一起动作。其实不是,“同”字应是指协同、协助、互相配合。如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。

所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法。按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。例如Window API函数SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的LRESULT值返回给调用者。

在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。

4.2线程同步的方式和机制

临界区、互斥区、事件、信号量四种方式

临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphore)、事件(Event)的区别

1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占;

2、互斥量:采用互斥对象机制。 只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享;

3、信号量:它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目;

4、事 件: 通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作。 

五、利用互斥对象实现线程同步

5.1互斥对象

互斥对象(mutex)属于内核对象,它能确保线程拥有对单个资源的互斥访问权。互斥对象包含一个使用数量,一个线程ID和一个计数器。其中ID用于标识系统中哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。

为了创建互斥对象,需要调用函数:CreateMutex,该函数可以创建或打开一个命名的或匿名的互斥对象,然后程序就可以利用互斥对象完成线程间的同步。CreateMutex函数原型声明如下:

HANDLE CreateMutex(
LPSECURITY_ATTRIBUTESlpMutexAttributes, // 指向安全属性的指针
BOOLbInitialOwner, // 初始化互斥对象的所有者
LPCTSTRlpName // 指向互斥对象名的指针
);
           

参数 类型及说明

lpMutexAttributes SECURITY_ATTRIBUTES,指定一个SECURITY_ATTRIBUTES结构,或传递零值(将参数声明为ByVal As Long,并传递零值),表示使用不允许继承的默认描述符

bInitialOwner Long,如创建进程希望立即拥有互斥体,则设为TRUE。一个互斥体同时只能由一个线程拥有

lpName String,指定互斥体对象的名字。用vbNullString创建一个未命名的互斥体对象。如已经存在拥有这个名字的一个事件,则打开现有的已命名互斥体。这个名字可能不与现有的事件、信号机、可等待计时器或文件映射相符

返回值

Long,如执行成功,就返回互斥体对象的句柄;零表示出错。会设置GetLastError。即使返回的是一个有效句柄,但倘若指定的名字已经存在,GetLastError也会设为ERROR_ALREADY_EXISTS 

5.2示例

#include "stdafx.h"
#include<windows.h>//需要访问Windows API函数CreateThread
#include <iostream>
using namespace std;
DWORD WINAPI Fun1Proc(//注意线程函数的声明形式
LPVOID pParam //thread data
);
DWORD WINAPI Fun2Proc(LPVOID pParam);
int index=0;
int tickets=100;
HANDLE hMutex;
int _tmain(int argc, _TCHAR* argv[])
{
HANDLE hThread1;
HANDLE hThread2;
hThread1 = CreateThread(NULL,//让新线程使用默认的安全性
0,//让新线程采用与调用线程一样的栈大小
Fun1Proc,//指定线程1入口函数的地址
NULL,//传递给线程1的参数 可以是结构体(包含多个数据),此处不需要使用这个参数 设为NULL
0,//线程创建标记 设为0 让线程一旦创建就立即运行
NULL);//新线程的ID 这里不需要使用该ID 设为NULL
hThread2 = CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
CloseHandle(hThread1);//关闭新线程的句柄
CloseHandle(hThread2);//关闭新线程的句柄
//创建互斥对象
hMutex=CreateMutex(NULL,//让互斥对象拥有默认的安全性
FALSE,//该线程将不会获得所创建的互斥对象的所有权 true 相反
NULL);//创建一个匿名的互斥对象
Sleep(4000);//延时4s
system("pause");
return 0;
}
 
//线程1的入口函数
DWORD WINAPI Fun1Proc(LPVOID pParam)
{
while (true)
{
WaitForSingleObject(hMutex,INFINITE);
if (tickets>0)
{
Sleep(1);
cout<<"thread1 sell tickets:\r\n"<<tickets--<<endl;
} 
else
{
break;
}
ReleaseMutex(hMutex);//释放互斥对象
}
return 0;
}
 
//线程2的入口函数
DWORD WINAPI Fun2Proc(LPVOID pParam)
{
while (true)
{
//实现线程必须主动请求共享对象的使用权才有可能获得该所有权
WaitForSingleObject(hMutex,//所请求对象的句柄
INFINITE);//指定等待的时间间隔,以毫秒为单位
if (tickets>0)
{
Sleep(1);
cout<<"thread2 sell tickets:\r\n"<<tickets--<<endl;
} 
else
{
break;
}
ReleaseMutex(hMutex);
}
return 0;
} 
           

运行结果:

线程线程

继续阅读