一、线程介绍
什么是线程:
线程是操作系统能够进行运算执行的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程的发展简史:
60年代,在操作系统中能拥有资源和独立运行的基本单位是进程。
随着计算机技术的发展,进程出现了很多弊端:
一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;
二是由于对称多处理机出现,可以满足多个运行单位,而多个进程并行开销过大。
因此在80年代,出现了能独立运行的基本单位——线程(Threads)。
线程的调度策略:
线程是独立调度和分派的基本单位,有三种不同的调试策略:
1、线程可以为操作系统内核调度的内核线程,如Win32线程;
2、由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;
3、由内核与用户进程进行混合调度,如Windows 7的线程。
多线程适用的范围:
一个进程可以有很多线程,每条线程并行执行不同的任务。
在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。
在单CPU单核的计算机上,使用多线程技术,可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,原因就是线程占用的资源少,被阻塞时不浪费资源。
线程的特点:
轻型实体:
线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。线程的实体包括用于指示被执行指令序列的程序计数器、局部变量、状态参数和返回地址。
线程是动态概念,它的动态特性由线程控制块**TCB(Thread Control Block)**描述,包括以下信息:
1、线程状态
2、当线程不运行时,被保存的现场资源
3、一组执行堆栈
4、存放每个线程的局部变量主存区
5、访问同一个进程中的主存和其它资源
独立调度和分派的基本单位:
在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
可并发执行:
在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。
共享进程资源:
在同一进程中的各个线程,都可以访问该进程的用户空间,此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等,线程可以共享该进程所拥有的资源。所以线程之间互相通信不必调用内核。
二、线程与进程的区别
资源:
进程采用虚拟空间+用户态/内核态机制,所以就导致进程与进程之间是互相独立的,各自的资源不可见。
在同一进程中的各个线程都可以共享该进程所拥有的资源。
通信:
由于进程之间是互相独立的,需要使用各种IPC通信机制,保障多个进程协同工作。
同一进程中的各个线程共享该进程所拥有的资源,线程间可以直接读写进程数据段来进行通信,但需要线程同步和互斥手段的辅助,以保证数据的一致性。
调度:
无论系统采用什么样的线程调试策略,线程上下文切换都比进程上下文切换要快得多。
身份:
进程是个资源单位,线程是个执行单位,并且线程是进程的一部分,线程需要进程安身立命,进程也需要线程当牛做马。
三、POSIX线程库
POSIX线程库介绍:
POSIX线程(POSIX Threads,常被缩写为pthread)是POSIX的线程标准,定义了创建和操纵线程的一套API。
实现POSIX 线程标准的库常被称作pthread,一般用于Unix-likePOSIX 系统,如Linux、Solaris。但是Microsoft Windows上的实现也存在,例如直接使用Windows API实现的第三方库pthread-w32。
API具体内容:
pthread定义了一套C语言的类型、函数与常量,它以pthread.h头文件和一个接口库libpthread.so,gcc和g++编译器没有默认链接该库,需要程序员使用 -l pthread 参数进行手动链接。
pthread API中大致共有100个函数调用,全都以"pthread_"开头,并可以分为四类:
1、线程管理,如创建线程,等待线程,查询线程状态等。
2、互斥锁,有创建、摧毁、锁定、解锁、设置属性等操作
3、条件变量,有创建、摧毁、等待、通知、设置与查询属性等操作
4、使用了互斥锁的线程间的同步管理。
四、创建线程
int pthread_create (pthread_t* restrict thread,
const pthread_attr_t* restrict attr,
void* (*start_routine) (void*),
void* restrict arg);
thread - 线程ID,输出型参数。pthread_t即unsigned long int
attr - 线程属性,NULL表示缺省属性,如果没有特殊需要,一般写NULL即可
start_routine - 线程过程函数指针,参数和返回值的类型都是void*
启动线程本质上就是调用一个函数,只不过是在一个独立的线程中调用的,函数返回即线程结束
arg - 传递给线程过程函数的参数
返回值:成功返回0,失败返回错误码,但不会修改全局的错误变量,也就是无法使用perror获取错误原因。
注意:
1、restrict: C99引入的编译优化指示符,提高重复解引用同一个指针的效率。
2、pthread.h头文件中声明的函数,通常以直接返回错误码的方式表示失败,而非以错误码设置errno并返回-1。
3、应设法保证在线程过程函数执行期间,其参数所指向的目标持久有效。
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void* thread_proc (void* arg)
{
printf ("%lu线程:%s\n", pthread_self (), (char*)arg);
return NULL;
}
int main ()
{
pthread_t tid;
int error = pthread_create (&tid, NULL, thread_proc,"我是快乐的子线程!");
if (error)
{
fprintf (stderr, "pthread_create: %s\n", strerror (error));
return -1;
}
printf ("%lu线程:我是主线程,创建了%lu线程。\n", pthread_self (),tid);
sleep (1);
return 0; //main函数中的return status 等价于exit(status) ,可以把整个进程杀死,是因为整个进程死了,子进程才死的,而不是因为主进程死了,子进程才死的。子进程的死亡与主进程没有关系,主进程无法影响子进程。
//可以加sleep(1) 等待进程执行完再杀死
}
五、线程回收
int pthread_join (pthread_t thread, void** retval);
功能:等待thread参数所标识的线程结束,并回收相关资源,如果thread线程没有结束则阻塞
retval:获得线程正常结束时的返回值,是输出型的参数,用于获取线程入口函数的返回值
返回值:成功返回0,失败返回错误码
从线程过程函数中返回值的方法:
1、线程过程函数将所需返回的内容放在一块内存中,返回该内存的地址,保证这块内存在函数返回,即线程结束,以后依然有效;
2、若retval参数非NULL,则pthread_join函数将线程入口过程函数所返回的指针,拷贝到该参数所指向的内存中。不要返回局部变量的地址,因为局部变量地址使用的是栈内存,栈内存在进程结束后被销毁了。
3、若线程入口函数所返回的指针指向text、data、bss,如果指向heap内存,则还需保证在用过该内存之后释放之。(就是不能是栈内存)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#define PAI 3.14159
void* thread_area (void* arg)
{
double r = *(double*)arg;
/*
double s;
s = PAI * r * r;
return &s; -----> 如果返回的是局部变量的地址,则pthread_join的返回值为NULL
*/
/*
static double s;
s = PAI * r * r;
return &s;
*/
double* s = malloc (sizeof (double));
*s = PAI * r * r;
return s;
}
int main ()
{
printf ("r = ");
double r;
scanf ("%lf", &r);
pthread_t tid;
// 可以传递栈内存的参数,但是有安全隐患
int error = pthread_create (&tid, NULL, thread_area, &r);
if (error)
{
fprintf (stderr, "pthread_create: %s\n", strerror (error));
return -1;
}
// pthread库的函数,不会影响errno全局变量,所以无法使用perror显示错误原因,它们会返回错误码,再使用strerror把错误码转换成错误原因。
//当资源耗尽时创建线程就会失败,大约350左右。
double* s;
if ((error = pthread_join (tid, (void**)&s)) != 0)
{
fprintf (stderr, "pthread_join: %s\n",