线程创建相关API函数
//头文件 #include <pthread.h> //创建线程函数 int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void*), void *restrict arg);
//获取自身线程ID函数
pthread_t pthread_self(void);
//比较两个线程ID是否相等函数
int pthread_equal(pthread_t t1, pthread_t t2);
<说明> 在Linux3.2,phtread_t表示的是无符号长整型数据类型,即unsigned long。Solaris 10把pthread_t数据类型表示为无符号整型,即unsigned int。MacOS系统是用一个指向pthread结构的指针来表示pthread_t数据类型。
线程概念
【问题1】什么是线程?
- 线程是比进程更小的活动单位,它是进程中的一条执行路径。
线程可以这样来描述:
- 线程是进程中的一条执行路径;
- 它有自己私有的堆栈和处理机执行环境;
- 它与父进程共享分配给父进程的地址空间;
- 它是单个进程所创建的许多个同时存在的线程中的一个。
【问题2】进程与线程的联系?
## 进程概念
进程是表示资源分配的基本单位,又是调度运行的基本单位。例如,用户运行自己的程序,系统就创建一个进程,并为它分配资源,包括各种表格、内存空间、
磁盘空间、I/O设备等。然后,把该进程放人进程的就绪队列。进程调度程序选中它,为它分配CPU以及其它有关资源,该进程才真正运行。所以,进程是系统中的
并发执行的单位。
在Mac、Windows NT等采用微内核结构的操作系统中,进程的功能发生了变化:它只是资源分配的单位,而不再是调度运行的单位。在微内核系统中,真正调度
运行的基本单位是线程。因此,实现并发功能的单位是线程。
线程概念
线程是进程中执行运算的最小单位,亦即执行处理机调度的基本单位。如果把进程理解为在逻辑上操作系统所完成的任务,那么线程表示完成该任务的许多可
能的子任务之一。例如,假设用户启动了一个窗口中的数据库应用程序,操作系统就将对数据库的调用表示为一个进程。假设用户要从数据库中产生一份工资单报
表,并传到一个文件中,这是一个子任务;在产生工资单报表的过程中,用户又可以输人数据库查询请求,这又是一个子任务。这样,操作系统则把每一个请求――
工资单报表和新输人的数据查询表示为数据库进程中的独立的线程。线程可以在处理器上独立调度执行,这样,在多处理器环境下就允许几个线程各自在单独处理
器上进行。操作系统提供线程就是为了方便而有效地实现这种并发性。
## 引入线程的好处
(1)易于调度。
(2)提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。
(3)开销少。创建线程比创建进程要快,所需开销很少。。
(4)利于充分发挥多处理器的功能。通过创建多线程进程(即一个进程可具有两个或更多个线程),每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。
## 进程和线程的关系
(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。
(3)处理机分给线程,即真正在处理机上运行的是线程。
(4)线程在执行过程中,需要协作同步。
【问题3】为什么有了进程的概念,还要再引入线程呢?使用多线程到底有哪些好处?什么样的系统应该选用多线程?
进程切换开销大;进程间通信效率受限;线程能更好地描述多处理机中的并行处理行为。
使用多线程的理由之一是和进程相比,线程是一种非常"节俭"的多任务操作方式。在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数
据表来维护它的代码段、堆栈段和数据段,这是一种代价"高昂"的多任务处理方式;而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分
数据,启动一个线程所花费的空间远远小于一个进程所花费的空间,而且线程间彼此切换所需的时间也远远少于进程间切换所需要的时间。
使用多线程的理由之二是线程间通信机制方便。对不同进程来说,它们具有独立的地址空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且
很不方便。线程则不然,由于同一进程下的线程之间共享地址空间,所以一个线程的数据可以直接为其他线程所用,这不仅快捷,而且方便。当然,数据的共享也会带
来其他的一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些是编写多线程程序时最需要注意的地方。除了以上所说的有点外,与进程比较,多线程程序作为一种多任务、并发的工作方式,还有一下的优点:
(1)提高应用程序响应。
(2)使多处理机系统更加高效。操作系统会保证线程数不大于CPU数目时,不同的线程运行在不同的CPU上。
(3)改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会有利于理解和修改。
线程创建
Linux下的线程遵循POSIX线程接口,称为"pthread" 或者 “POSIX线程”。编写Linux下的多线程,需要使用头文件pthread.h,连接是需要使用库文件libpthread.a,编译时需要添加链接库选项 -lpthread。
创建线程实际上就是调用该线程函数的入口点,使用的函数是pthread_create。在线程创建以后,就开始运行相关的线程函数。
//线程创建函数 int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void*), void *restrict arg);
【参数】
thread:存放线程ID值。
attr:设置线程属性。如果不需要设置线程属性,可以置为NULL,系统会创建一个具有默认属性的线程。
start_routine:线程函数指针,它是线程函数的入口地址。
arg:传入线程函数的参数。如果不需要传参,可以设置为NULL。
【返回值】成功,返回0;失败,返回错误码。
【说明】
1、每个线程都包含有表示执行环境所必需的信息,包括进程中标识线程的线程ID、一组寄存器值、栈区、调度优先级和策略、信号屏蔽字、errno全局变量以及线程的私有数据。一个进程的所有信息对该进程中的所有线程都是共享的,包括可执行程序的代码,程序的全局变量和堆内存,进程栈区以及文件描述符。
2、POSIX线程的功能测试宏是 _POSIX_THREADS。用于程序可以把这个宏用于 #ifdef 测试,从而在编译时确定是否支持线程。也可以把 _SC_THREADS 常数用于调用sysconf()函数,进而在运行时确定是否支持线程。遵循SUSv4的系统定义符号 _POSIX_THREADS 的值为 200809L。
3、新创建的线程是从start_routine函数的首地址开始运行的,该函数只有一个无类型的指针参数arg。如果需要向start_routine函数传递的参数不止一个,则需要把这些参数放到一个结构体中,然后把这个结构体的地址作为arg参数传入。
4、线程创建时,并不能保证哪个线程先运行:是新创建的线程,还是调用线程。新创建的线程可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集会被清除。
【注意】pthread函数在调用失败时通常会返回错误代码,它们并不像其他的 POSIX 函数一样设置 errno。每个线程都提供 errno 的副本,这只是为了与使用 errno 的现有函数兼容。在线程中,从函数中返回错误代码更为清晰整洁,不需要依赖那些随着函数执行不断变化的全局状态,这样可以把错误的范围限制在引起出错的函数中。
线程标识
就像每个进程有一个进程ID一样,每个线程也有一个线程ID。进程ID在整个系统中是唯一的,但线程ID不同,线程ID只有在它所属的进程上下文中才有意义。
进程ID是用 pit_t 数据类型来表示的,它是一个非负整数,即unsigned int。线程ID是用pthread_t数据类型来表示的。
线程可以通过调用pthread_self()函数获得自身的线程ID。
//获取线程ID函数 pthread_t pthread_self(void);
【返回值】返回线程ID值。
//比较两个线程ID是否相等函数
int pthread_equal(pthread_t t1, pthread_t t2);
【返回值】相等,返回非0数值;否则,返回0。
<说明> 当线程需要识别以线程ID作为标识的数据结构时,pthread_self()函数与pthread_equal()函数可以一起结合使用。例如,主线程把工作任务放在一个队列中,用线程ID来控制每个工作线程处理哪些作业。如下图所示,主线程把新的作业任务放在一个工作队列中,由3个工作线程组成的线程池从工作队列中移出作业。主线程不允许每个线程任意处理从队列顶端取出的作业,而是由主线程控制作业的分配,主线程会在每个待处理作业的结构中放置处理该作业的线程ID,每个工作线程只能移出标有自己线程ID的作业。
实例1:打印线程ID。编译的时候需要加上-lpthread。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <pthread.h> 4 5 pthread_t ntid; //定义一个全局变量 6 7 void printids(const char *s) 8 { 9 pid_t pid; 10 pthread_t tid; 11 12 pid = getpid(); //获取进程ID 13 tid = pthread_self(); //获取当前线程ID 14 printf("%s pid:%lu, tid:%lu (0x%lx)\n", s, pid, tid, tid); 15 } 16 17 void* thr_fn(void *arg) 18 { 19 printids("new thread:"); 20 return ((void*)0); 21 } 22 23 int main(int argc, char *argv[]) 24 { 25 int err; 26 err = pthread_create(&ntid, NULL, thr_fn, NULL); //创建新线程 27 if(err != 0){ 28 printf("Error: can`t create thread!\n"); 29 return -1; 30 } 31 // printf("ntid = %lu\n", ntid); 32 printids("main thread:"); 33 sleep(1); 34 return 0; 35 }
## 编译:gcc threadid.c -o threadid -lpthread
## 运行结果1:
main thread: pid:22037, tid:47719253331488 (0x2b668124ce20)
new thread: pid:22037, tid:47719263824192 (0x2b6681c4e940)
## 主线程不休眠的情况下,运行结果2:
main thread: pid:22634, tid:47061461720608 (0x2acd59b55e20)
## 输出pthread_create函数的第一个参数(tidp),运行结果3:
ntid = 46944340744512
main thread: pid:24833, tid:46944330251808 (0x2ab21420fe20)
new thread: pid:24833, tid:46944340744512 (0x2ab214c11940)
## 分析:
从运行结果来看,在主线程和新线程中打印出的进程ID是一样的,而两个线程ID值是不同的。
需要说明的是:
1、主线程需要休眠(1秒),如果不休眠,主线程可能会退出,而新进程还没有机会运行,整个进程可能就已经终止了。这种行为特征取决于操作系统的线程实现
和调度算法。
2、新线程是通过调用pthread_self()函数获取自己的线程ID的,而不是从共享内存中读出的,或者从线程的启动例程中以参数的形式接收到的。pthread_create函数,它会通过第一个参数(ntid)返回新建线程的线程ID。在本例中,主线程把新线程ID存放在ntid中,但是新建的线程却不能安全地使用它,因为如果新线程在主线程调用pthread_create()函数返回之前就开始运行了,那么新线程看到的是未经初始化的ntid的值,这个值并不是正确的新线程ID。
3、从运行结果3可以得知,在主线程中调用pthread_create函数获取的新线程ID(ntid)和在新线程函数中调用pthread_self函数获取的新线程ID(tid)的值是一样的。