文章目录
- 线程库
- 线程的创建
- 线程库中的线程ID
- 线程等待及线程退出
- C++11 中的线程库
- 线程库的线程与轻量型进程
线程库
在Linux内核中没有实际的线程概念,只有轻量级进程的概念,即使用task_struct
内核数据结构模仿线程;
所以本质上在Linux内核中无法直接调用系统调用接口创建线程,只能调用系统调用接口创建轻量级进程;
在Linux上需要对线程进行控制需要使用第三方线程库,即<pthread.h>
,该库封装了Linux下的轻量级进程接口;
同时该库也成为了Linux平台下的默认库,即Linux系统将会自带该库;
该库为动态库,封装了一系列的线程控制接口,当使用该库创建线程时其对应的线程的结构将被该动态库进行组织与维护;
该库为第三方库,所以在使用该库时需要使用-lpthread
动态链接该库;
该库中使用了大量的void*
类型为用户提供了一定程度的泛型编程能力,使得其能够传递不同类型的变量,也包括对象;
线程的创建
线程的创建通常使用pthread_create()
接口;
NAME
pthread_create - create a new thread
SYNOPSIS
#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.
RETURN VALUE
On success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.
当函数调用成功时返回0
,调用失败时返回!0
;
-
pthread_t *thread
该参数为一个输出型参数,类型为
pthread_t*
,其中pthread_t
类型本质上是一个对unsigned long int
类型的typedef
;typedef unsigned long int pthread_t;
该输出型参数用于返回线程
thread
的id
; -
const pthread_attr_t *attr
该参数用于指向线程属性结构的指针,如果为
nullptr
则使用默认属性;可以用来设置线程的各种属性,如栈的大小,调度策略等,通常情况下使用
nullptr
默认情况即可; -
void *(*start_routine) (void *)
该参数为一个函数指针,用于传递一个函数;
创建的新线程将会使用该回调函数,接收的参数为一个
void*
的类型的参数并返回void*
类型的返回值;同时该函数为新线程的入口点;
-
void *arg
传递给
start_routine
函数的参数,若是不需要传递参数则设置为nullptr
;
该函数中众多的void*
类型为用户提供了一定程度的泛型编程能力,使得其能够传递不同类型的变量,也包括类;
-
void*
类型的大小void*
与void
两个类型一个是空指针类型一个是空类型,两种类型都具有大小;其中
void*
为指针,大小与所有指针相同,在不同机器位的机器下的大小都不同;void
的大小在Linux中一般为1
;int main() { printf("sizeof(void) = %d\n", sizeof(void)); printf("sizeof(void*) = %d\n", sizeof(void*)); return 0; }
结果为:
$ ./mythread sizeof(void) = 1 sizeof(void*) = 8
不同的是
void*
可以用来声明变量,void
不能声明变量;
pthread_create()
使用如下:
void* Print(void* arg) {
while (true) {
cout << "I am "<<(const char*)arg<<" , the PID : " << getpid() << endl;
sleep(2);
}
return nullptr;
}
int main() {
pthread_t newthread;
pthread_create(&newthread, nullptr, Print, (void*)"new_thread1");
sleep(1); // 确保线程已经被运行
while (true) {
cout << "I am main thread , the PID : " << getpid() << endl;
sleep(1);
}
return 0;
}
定义一个pthread_t
类型的变量用于接收该新线程的返回id
;
使用pthread_create()
函数创建一个线程并传入一个字符串作为回调函数的参数并强转为(void*)
以方便传参;
同时设置了一个名为Print()
函数作为该线程的入口点;
新线程所用函数与主线程相同都对自身的PID
进行打印;
使用-lpthread
编译后可使用ldd
进行验证该库是否为第三方库;
$ ldd mythread
linux-vdso.so.1 => (0x00007ffe6f966000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f6598b80000) # 已链接pthread库
libstdc++.so.6 => /home/__USR/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6 (0x00007f65987ff000)
libm.so.6 => /lib64/libm.so.6 (0x00007f65984fd000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f65982e7000)
libc.so.6 => /lib64/libc.so.6 (0x00007f6597f19000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6598d9c000)
在新窗口中使用shell
脚本对进程进行监控;
$ while :;
do ps axj | head -1 && ps axj | grep mythread | grep -v grep ;
echo "----------------------------" ;
sleep 1 ;
done
执行结果为如下:
# 程序所在窗口
$ ./mythread
I am new_thread1 , the PID : 6974
I am main thread , the PID : 6974
...
# shell脚本所在窗口
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
6416 6974 6974 6416 pts/0 6974 Sl+ 1001 0:00 ./mythread
----------------------------
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
6416 6974 6974 6416 pts/0 6974 Sl+ 1001 0:00 ./mythread
----------------------------
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
6416 6974 6974 6416 pts/0 6974 Sl+ 1001 0:00 ./mythread
其中脚本可以看出只有一个进程在运行,且进程中的两个线程作为不同的执行流都打印同一个PID
;
可使用ps -aL | grep <Processname>
查看当前对应运行进程中的轻量级进程的基本属性(该命令为内置命令,且Linux中不存在线程概念,所以查看的属性为轻量级进程的基本属性);
$ ps -aL | head -1 && ps -aL | grep mythread
PID LWP TTY TIME CMD
6974 6974 pts/0 00:00:00 mythread
6974 6975 pts/0 00:00:00 mythread
其中LWP
为Light-weight process
轻量级进程 的缩写,对应的PID
与LWP
相同的轻量级进程即可看出是 “主线程” ;
线程库中的线程ID
pthread_create()
函数中的*thread
参数用于返回所创建线程的tid
;
而实际上该参数所返回的是一个地址;
-
pthread_self()
在线程中可使用
pthread_myself()
接口获取自身线程的tid
;NAME pthread_self - obtain ID of the calling thread SYNOPSIS #include <pthread.h> pthread_t pthread_self(void); Compile and link with -pthread. DESCRIPTION The pthread_self() function returns the ID of the calling thread. This is the same value that is returned in *thread in the pthread_create(3) call that created this thread. RETURN VALUE This function always succeeds, returning the calling thread's ID.
返回值为一个
pthread_t
类型的参数,即线程本身的tid
;
void* Print(void* n) {
printf("I am newthread , the tid:%p\n",pthread_self());
sleep(1);
return nullptr;
}
int main() {
pthread_t newthread;
pthread_create(&newthread, nullptr, Print, nullptr);
sleep(1); // 确保线程已经被运行
printf("the new thread tid : %p\n", newthread);
sleep(3);
return 0
}
该程序中创建了一个新线程,并且在新线程中使用pthread_self()
获取自身线程tid
并打印;
运行结果如下:
$ ./mythread
I am newthread , the tid:0x7f18642ef700
the new thread tid : 0x7f18642ef700
主线程与新线程打印tid
结果相同;
本质上是因为在Linux中线程是<pthread.h>
线程库所提供的概念,当需要用到线程时需要将<pthread.h>
库链接至内存共享区中;
线程的概念为<pthread.h>
提供,故该库也应需要对线程进行组织与管理,使用<pthread.h>
库所创建的线程(除主线程外)都将被该库在共享区中进行组织与管理;
故在Linux(CentOS7)中,本身pthread_t *thread
参数为一个可以索引至共享区中对该应线程属性结构的线性地址(虚拟地址);
由于地址具有唯一性,该设计可以使得其TID
既保证了能够进行唯一标识,也可以快速(直接或间接)访问到对应的TCB
结构体当中;
一般来说该TCB
结构体为了防止线程的安全用户无法直接访问,只能通过该库内部的机制进行间接访问;
线程等待及线程退出
线程与进程相同都需要被等待,否则将出现类似于僵尸进程一样的问题,即线程已经执行完毕但未被回收导致资源被占用;
通常主线程需要使用pthread_join()
对新线程进行等待回收;
NAME
pthread_join - join with a terminated thread
SYNOPSIS
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
Compile and link with -pthread.
RETURN VALUE
On success, pthread_join() returns 0; on error, it returns an error
number.
该函数调用成功时返回0
,失败时返回!0
;
-
pthread_t thread
该参数用于需要等待的一个线程
ID
,即调用该函数的前提是必须存在一个pthread
库所创建的线程; -
void **retval
该参数用于返回线程所调用回调函数的返回值;
以二级指针的方式对返回值进行接收;
参数返回时不能直接返回线程栈上开辟空间的数据,因为线程的栈是其独立的,当程序执行完毕后表示该线程的生命周期结束,对应的其栈的空间将会被回收;
如果直接返回线程栈中的数据则可能会出现段错误;
若是不关心该线程的返回值或是线程无有用返回值可设置为
nullptr
;
void* Print(void* arg) {
cout << "I am " << (const char*)arg << " , the PID : " << getpid() << endl;
int* n = new int;
*n = 42;
cout << "new thread quit..." << endl;
sleep(1);
return n;
}
int main() {
pthread_t newthread;
pthread_create(&newthread, nullptr, Print, (void*)"new_thread1");
sleep(1); // 确保线程已经被运行
cout << "I am main thread , the PID : " << getpid() << endl;
unsigned long int* ret;
pthread_join(newthread, (void**)&ret);
sleep(2);
printf("new thread quit... , the exit_code:%lu\n", *ret);
delete ret;
return 0;
}
该程序中创建了一个线程,并在该线程的结束时返回一个通过new
创建的一个数据(42
)以避免直接返回线程栈中的数据;
主线程中创建了一个ret
变量用于接收该线程执行完毕的返回值并对该返回值进行打印,调用pthread_join()
等待该线程;
可使用shell
脚本对线程进行观察:
$ while :;
do ps -aL | head -1 && ps -aL | grep mythread ;
echo "------------------------------------" ;
sleep 1 ;
done
结果如下:
# 程序所在会话
$ ./mythread
I am new_thread1 , the PID : 8942
new thread quit...
I am main thread , the PID : 8942
## 中间间隔了两秒
new thread quit... , the exit_code:42
# shell脚本所在会话
PID LWP TTY TIME CMD
8942 8942 pts/0 00:00:00 mythread
8942 8943 pts/0 00:00:00 mythread
------------------------------------
PID LWP TTY TIME CMD
8942 8942 pts/0 00:00:00 mythread
------------------------------------
PID LWP TTY TIME CMD
8942 8942 pts/0 00:00:00 mythread
------------------------------------
PID LWP TTY TIME CMD
------------------------------------
运行结果为线程所调用的回调函数返回值被主线程接收,同时可以清楚看到新线程执行完毕后退出同时资源被回收;
主线程中间隔了两秒才打印最终结果表示该函数默认行为为阻塞等待;
-
线程的退出
线程的退出无法使用
exit()
;本质原因是
exit()
的退出是调用了_exit()
系统调用接口的,该系统调用接口直接会将进程进行退出,退出的是进程而不是线程;一般情况下线程的退出不仅可以使用
return
,也可以使用pthread_exit()
函数;NAME pthread_exit - terminate calling thread SYNOPSIS #include <pthread.h> void pthread_exit(void *retval); Compile and link with -pthread.
该函数为退出正在执行的线程;
传递一个
void*
类型的参数以表示该线程的返回值,通常该值不能是线程栈上开辟的空间中的数据,必须是在非栈上这种共享空间中,因为该线程的生命周期已经结束,若是访问线程栈中的数据将会导致段错误;-
pthread_cancel()
该接口函数可以在主线程中取消一个对应
tid
的线程;NAME pthread_cancel - send a cancellation request to a thread SYNOPSIS #include <pthread.h> int pthread_cancel(pthread_t thread); Compile and link with -pthread. RETURN VALUE On success, pthread_cancel() returns 0; on error, it returns a nonzero error number.
参数为
pthread_t thread
类型表示需要传入一个该线程对应的标识符tid
;当主线程使用该函数取消一个线程时对应的该线程会返回一个
PTHREAD_CANCELED
的宏,该宏的值默认为-1
;
-
-
线程的崩溃
进程在退出时具有三种情况:
- 程序执行完毕,结果正确
- 程序执行完毕,结果错误
- 程序未执行完毕异常退出
线程不具备第三种情况,原因是异常结束本质上是收到了信号,而信号的概念是与进程相关的;
这表明线程收到信号后就代表进程收到了信号;
-
使用线程函数传入对象完成总数相加计算小程序
该程序为计算
start
到end
相加的和;class Request { public: Request(int start, int end, const string &threadname) : start_(start), end_(end), threadname_(threadname) {} public: int start_; int end_; string threadname_; }; class Response { public: Response(int result, int exitcode) : result_(result), exitcode_(exitcode) {} public: int result_; int exitcode_; }; void *sumCount(void *args) { Request *rq = static_cast<Request *>(args); // static_cast<Request *>(args) 类比于 (Request*)args Response *ret = new Response(0, 0); for (int i = rq->start_; i <= rq->end_; ++i) { ret->result_ += i; } return ret; } int main() { pthread_t tid; Request *rq = new Request(1, 100, "Thread1"); pthread_create(&tid, nullptr, sumCount, rq); Response *rsp; pthread_join(tid, (void **)&rsp); printf("the sumSount sucess , the result is %d , the exitcode is %d\n", rsp->result_, rsp->exitcode_); delete rsp; delete rq; return 0; }
该程序设计了两个类,分别为计算数值传入的类
Request
,一个为返回对应计算结果与退出码的类Response
;并设计了一个
sumCount()
函数作为线程的入口点也是用于计算的方法;
C++11 中的线程库
C++11
已经支持了线程库;
在不同的平台下其封装的底层实现不同,在Linux下C++11
的线程库主要是对pthread
库的封装
这意味在Linux下使用C++11
的线程库时仍需在编译时链接对应的pthread
库;
void thread_cpp() {
while (true) {
cout << " I am a new thread from cpp" << endl;
sleep(1);
}
}
int main() {
thread t(thread_cpp);
t.join(); // 用于等待线程 对线程进行清理工作
return 0;
}
运行结果为:
$ ./mythread
I am a new thread from cpp
I am a new thread from cpp
...
^C
线程库的线程与轻量型进程
pthread
线程库本质上就是封装了Linux系统关于轻量型进程的系统调用接口;
在Linux系统中创建轻量级进程的系统调用接口为clone()
;
NAME
clone, __clone2 - create a child process
SYNOPSIS
/* Prototype for the glibc wrapper function */
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
/* Prototype for the raw system call */
long clone(unsigned long flags, void *child_stack,
void *ptid, void *ctid,
struct pt_regs *regs);
Feature Test Macro Requirements for glibc wrapper function (see fea‐
ture_test_macros(7)):
clone():
Since glibc 2.14:
_GNU_SOURCE
Before glibc 2.14:
_BSD_SOURCE || _SVID_SOURCE
/* _GNU_SOURCE also suffices */
RETURN VALUE
On success, the thread ID of the child process is returned in the caller's
thread of execution. On failure, -1 is returned in the caller's context, no
child process will be created, and errno will be set appropriately.
该函数用来创建一个新的进程(轻量型进程);
其中本质上fork()
系统调用接口就是封装了该接口;
当该函数调用成功时将返回新轻量型进程的ID
,失败时返回-1
并设置errno
;
-
int (*fn)(void *)
该参数用于传递一个回调函数,该回调函数为该轻量型进程的入口点;
-
void *chile_stack
这是一个指向子进程的栈底指针,新创建的这个进程(轻量型进程)将使用这个栈;
通常着需要指向一个预先分配好的栈内存区域,并且栈的顶端应该对齐;
-
int flags
这是一个标志位参数,用于指定新进程的行为和资源共享方式;
-
void *arg
该参数用于传给
fn
的参数,子进程将以此为参数调用fn
函数; -
可选参数
pid_t *ptid
,struct user_desc *tls
,pid_t *ctid
;
对应的pthread_create()
则是封装了该函数;