所有涉及的线程相关的操作都是在用户空间中线程操作,在 Linux 系统中,pthread 是一个遵循 POSIX 标准的通用线程库,具有良好的可移植性。
下面开始将要讨论的线程接口来自POSIX.1-2001,线程接口也称为 “pthread” 或 “POSIX线程”。
一、线程的概念
1.1、线程概述
典型的UNIX进程可以看成只有一个控制线程:一个进程在某一时刻只能做一件事情。有了多个控制线程以后,在程序设计时就可以把进程设计成在某一时刻能够做不止一件事,每个线程处理各自独立的任务。
每个线程都包含有表示执行环境所必需的信息,其中包括进程中标识线程的线程ID,一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量以及线程私有数据。
一个进程的所有信息对该进程的所有线程都是共享的,包括 可执行程序的代码、程序的全局内存和堆内存、栈 以及 文件描述符。
1.2、线程标识
1.2.1、概述
每个线程都有一个 线程ID,就像进程ID一样,进程ID 在整个操作系统中是唯一存在的,但是线程ID不同,线程ID只有在他所属的进程上下文中才有意义。
进程ID 用 pid_t 数据类型来表示的,pid_t 是一个非负整数。线程ID 是用 pthread_t 数据类型来表示的,Linux 系统中,在 /usr/include/
路径下,有线程定义的头文件 pthread.h,在其前面中包含了头文件#include <bits/pthreadtypes.h>
,然后在对应目录下找到此文件,可以使用指令 find . -name "pthreadtypes.h"
,在 Ubuntu 16.04 LTS 中,可以通过下面的方法找到文件。
Ubuntu@songshuai:/usr/include$ find . -name "pthreadtypes.h"
./x86_64-linux-gnu/bits/pthreadtypes.h
查看文件内容,可以找到 pthread_t 的定义如下。
/* Thread identifiers. The structure of the attribute type is not
exposed on purpose. */
typedef unsigned long int pthread_t;
由上面代码可以看出来,目前线程ID采用的是一个非0的长整形数据类型,但是在一起可移植的操作系统中,线程ID的实现的时候可以用一个结构来代表 pthread_t 数据类型,所以可移植的操作系统实现不能把它作为整数处理。因此必须使用一个函数来对两个 线程ID 进行比较。
1.2.2、线程ID的比较
在目前的Linux系统中,已经提供了一个函数用来比较两个线程是否相等。
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
Compile and link with -pthread.
功能:
比较两个线程的线程ID是否相等
参数:
t1 :线程1的ID
t2 :线程2的ID
返回:
若线程ID相等,返回非0数值
若线程ID不相等,返回0
1.2.3、线程ID的获取
pthread 是一个遵循 POSIX 标准的通用线程库,想要获取线程的ID,则可以使用下面函数获取。
#include <pthread.h>
pthread_t pthread_self(void);
Compile and link with -pthread.
功能:
返回调用线程的线程的ID,与创建此线程的 pthread_create() 函数中返回的值相同
参数:
无
返回:
函数永远返回成功,返回调用线程的ID
需要注意的是:
线程ID只有在同一个进程中才有意义,所以在不同进程中,每个线程的 pthread_self() 可能返回是一样的。
用下面的代码进行测试,查看线程ID等,代码如下。
#include <pthread.h> /* for pthread_create */
#include <stdio.h> /* for printf */
#include <stdlib.h> /* for exit */
#include <unistd.h> /* for usleep */
/* 线程的执行体函数 */
void *callback_func(void *arg)
{
/* 延时50ms,用于确保主线程执行完成 */
usleep(50000);
/* 定义临时变量,存储线程ID */
pthread_t mypid = 0;
/* 获取线程的ID */
mypid = pthread_self();
/* 输出调试信息,并线程ID */
printf("I'm thread %d, my PID = %X, my ID = %lu, HEX = %08lX, \n",
*(int *)arg, getpid(), mypid, mypid);
}
/* 1、线程是依附于进程存在的 */
/* 2、进程本身被称为主线程,其他线程都叫做副线程*/
int main(int argc, const char *argv[])
{
/* 定义两个变量,用于存储线程ID */
pthread_t pthid1 = 0;
pthread_t pthid2 = 0;
/* 定义参数,用于区别是哪一个线程 */
int encode1 = 1; /* 线程1,将此值赋值为1 */
int encode2 = 2; /* 线程2,将此值赋值为2 */
/* 输出调试语句 */
printf("pthread ID test!\n");
/* 创建线程 */
if (pthread_create(&pthid1, NULL, callback_func, &encode1) != 0)
{
perror("pthread_create error"); /* 打印错误信息 */
exit(EXIT_FAILURE); /* 结束整个进程 */
}
/* 延时5ms,用于确保执行的顺序 */
usleep(5000);
/* 创建线程 */
if (pthread_create(&pthid2, NULL, callback_func, &encode2) != 0)
{
perror("pthread_create error"); /* 打印错误信息 */
exit(EXIT_FAILURE); /* 结束整个进程 */
}
/* 输出调试信息,并线程ID */
printf("I'm create caller, return pthid1 = %lu, HEX = %08lX\n", pthid1, pthid1);
printf("I'm create caller, return pthid2 = %lu, HEX = %08lX\n", pthid2, pthid2);
/* 阻塞等待用于输入,防止主线程运行退出 */
getchar();
return 0;
}
编译上述程序并运行,程序执行的效果如下图1.1所示。
由上图可看出来,两个线程的进程ID(PID:92)相同,但线程ID(ID1=7F36503D0700、ID2=7F364FBC0700)不同。
如果把线程ID看成是十进制整数,那么这两个值看起来很奇怪,但是如果把它们转化成十六进制,看起来就更合理了。
尽管Linux线程ID是用无符号长整型来表示的,但是它们看起来像指针。
二、线程的创建
2.1、概述
在传统UNIX进程模型中,每个进程只有一个控制线程。从概念上讲,这与基于线程的模型中每个进程只包含一个线程是相同的。在POSIX线程的情况下,程序开始运行时,它也是以单进程中的单个控制线程启动的。在创建多个控制线程以前,程序的行为与传统的进程并没有什么区别。
创建一个新线程的函数为 pthread_create()
函数,函数原型如下所示。
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.
功能:
在当前进程下开启一个新线程
参数:
thread :存储线程ID,函数成功返回时,新创建线程的线程ID 会被设置成thread指向的内存单元。
attr :线程的属性,一般为NULL,代表默认属性,8M的栈空间
start_routine:线程的执行体函数
arg :用于作为 start_routine 唯一的参数
返回:
成功:返回为 0
失败:返回一个错误码,并且 *thread 指针将无定义
说明:
- 新创建的线程从 start_routine 函数的地址开始运行,该函数只有一个无类型指针参数 arg。如果需要向 start_routine 函数传递的参数有一个以上,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为 arg 参数传入;
- 线程创建时并不能保证新创建的线程先运行还是调用线程先运行,新创建的线程可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集会被清除;
- pthread 函数在调用失败时通常会返回错误码,它们并不设置 errno。
- 创建线程的函数时要指定线程指定的执行函数,线程创建之后,就开始执行相应的线程函数。在该函数运行完之后,线程结束。
2.2、代码示例
编写如下程序,主要实现创建新线程,在线程中输出自身的线程ID,以及当前运行的计数,同时在主线程中输出创建的子线程的线程ID。
主要的代码如下所示。
#include <pthread.h> /* for pthread_create */
#include <stdio.h> /* for printf */
#include <stdlib.h> /* for exit */
#include <unistd.h> /* for usleep */
/* 线程的执行体函数 */
void *callback_func(void *arg)
{
/* 延时50ms,确保主进程先输出相关信息 */
usleep(50000);
/* 临时变量,用于循环计数 */
int idx = 0;
/* 将传入的参数进行转换 */
int cnt = *(int *)arg;
/* 定义临时变量,存储线程ID */
pthread_t mypid = 0;
/* 获取线程的ID */
mypid = pthread_self();
/* 输出调试信息,并线程ID */
printf("I'm created thread, my ID = %lX\n", mypid);
/* 开始执行线程的任务 */
for (idx = 0; idx < cnt; idx++)
{
usleep(500000); /* 延时500ms */
printf("I'm running, Cnt = %d\n", idx);
}
}
/* 1、线程是依附于进程存在的 */
/* 2、进程本身被称为主线程,其他线程都叫做副线程*/
int main(int argc, const char *argv[])
{
/* 定义一个变量,用于存储线程ID */
pthread_t pthid = 0;
/* 定义线程函数的参数 */
int counter = 9;
/* 输出调试语句 */
printf("pthread create test!\n");
/* 创建线程 */
if (pthread_create(&pthid, NULL, callback_func, &counter) != 0)
{
perror("pthread_create error"); /* 打印错误信息 */
exit(EXIT_FAILURE); /* 结束整个进程 */
}
/* 输出调试信息,并线程ID */
printf("I'm create caller, return ID = %lX\n", pthid);
/* 阻塞等待用于输入,防止主线程运行退出 */
getchar();
return 0;
}
使用到POSIX线程函数的时候,在编译的时候需要链接 -lpthread 。不然会报错相关函数未定义(显示效果如下图2.1所示)。
编译上述程序并运行,程序执行的效果如下图2.1所示。
上面综述测试了线程的概要和线程的创建相关的知识点,那么关于线程的退出的问题,我们下一篇继续说明。
好啦,废话不多说,总结写作不易,如果你喜欢这篇文章或者对你有用,请动动你发财的小手手帮忙 **点个赞**,当然 **关注一波** 那就更好了,就到这儿了,么么哒(*  ̄3)(ε ̄ *)。