目录
- 线程与进程区别
- pthread库接口介绍
- pthread_create
- pthread_self和syscall(SYS_gettid);
- pthread_equal
- 测试主线程的栈空间大概是多大
- pthread_setname_np
- pthread_exit
- pthread_join
- 为什么要连接退出的线程
- pthread_detach
线程与进程区别
进程是一个动态的实体,有自己的生命周期。线程是操作系统进程调度器可以调度的最小执行单位。
一个进程可能包含多个线程。
进程之间,彼此的地址空间是独立的,但同一个进程的多个线程共享内存地址空间,即全局内存区域,包括初始化数据段、未初始化数据段和动态分配的内存段。
这种共享给线程带来了更多的优势:
- 创建线程花费的时间要比创建进程花费的时间少
- 终止线程花费的时间要比终止进程花费的时间少
- 线程之间上下文切换的开销要比进程之间上下文切换的开销小
- 线程之间共享数据要比进程之间共享数据简单
线程共享地址空间的设计,让多个线程之间的通信变得非常简单。一个进程内的多个线程,就像一个软件研发小组内部的不同员工,共享代码、服务器、打印机、资料,彼此之间有分工协作,沟通协作成本比较低。进程之间的通信代价则要高很多。进程之间不得不采用一些进程间通信的手段(如管道、共享内存及信号量等)来协作。
需要强调的一点是,线程和进程不一样,进程有父进程的概念,但在线程组里,所有的线程都是对等的关系:
- 并不是只有主线程才能创建线程,被创建出来的线程同样可以创建线程。
- 不存在类似于fork函数那样的父子关系,大家都归属于同一个线程组,进程ID都相等,而且各有各的线程ID。
- 并非只有主线程才能调用pthread_join连接其他线程,同一线程组内的任意线程都可以对某线程执行pthread_join函数。
- 并非只有主线程才能调用pthread_detach函数,其实任意线程都可以对同一线程组内的线程执行分离操作。
pthread库接口介绍
POSIX函数 | 函数功能描述 |
---|---|
pthread_create | 创建一个线程 |
pthread_exit | 退出线程 |
pthread_self | 获取线程ID |
pthread_equal | 检查两个线程ID是否相等 |
pthread_join | 等待线程退出 |
pthread_detach | 设置线程状态为分离状态 |
pthread_cancel | 线程的取消 |
pthread_setname_np | 设置线程的名称 |
pthread_cleanup_push | 线程退出,清理函数注册和执行 |
pthread_cleanup_pop | 线程退出,清理函数注册和执行 |
pthread_create
程序开始启动的时候,产生的进程只有一个线程,我们称之为主线程或初始线程。对于单线程的进程而言,只存在主线程一个线程。如果想在主线程之外,再创建一个或多个线程,就需要用到这个接口了。
函数接口如下:
#include <pthread.h>
int pthread_create(pthread_t *restrict thread,const pthread_attr_t *restrict attr,void *(*start_routine)(void*),void *restrict arg);
- pthread_create函数的第一个参数是pthread_t类型的指针,线程创建成功的话,会将分配的线程ID填入该指针指向的地址。线程的后续操作将使用该值作为线程的唯一标识。
- 第二个参数是pthread_attr_t类型,通过该参数可以定制线程的属性,比如可以指定新建线程栈的大小、调度策略等。如果创建线程无特殊的要求,该值也可以是NULL,表示采用默认属性。
- 第三个参数是线程需要执行的函数。创建线程,是为了让线程执行一定的任务。线程创建成功之后,该线程就会执行start_routine函数,该函数之于线程,就如同main函数之于主线程。
- 第四个参数是新建线程执行的start_routine函数的入参。
返回值:如果成功,则pthread_create返回0;如果不成功,则pthread_create返回一个非0的错误码。常见的错误码如表:
返回值 | 描述 |
---|---|
EAGAIN | 系统资源不够,或者创建线程的个数超过系统对一个进程中线程总数的限制 |
EINVAL | 第二个参数attr不合法 |
EPERM | 没有合适的权限来设置调度策略或参数 |
// pthread_create示例#include <iostream>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>void* ThreadFunc(void* _threadId)
{std::cout << "线程ID: " << pthread_self() << " hello world" << std::endl;pthread_exit(nullptr);
}int main()
{// 创建五个线程pthread_t tid[5];for (int i = 0;i < 5;i++){int rc = pthread_create(&tid[i], nullptr, ThreadFunc, (void*)tid[i]);if (rc != 0){std::cout << "线程创建失败" << std::endl;;return -1;}}// 等待所有线程结束pthread_exit(nullptr);return 0;
}
pthread_self和syscall(SYS_gettid);
这两个函数都是获取自身的线程ID
#include <pthread.h>pthread_t pthread_self();// --------------------------------- //int TID = syscall(SYS_gettid);
区别是:
- syscall(SYS_gettid),属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一标识该线程。
- pthread_self(),属于NPTL线程库的范畴,线程库的后续操作,就是根据该线程ID来操作线程的。
// 示例#include <iostream>
#include <pthread.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>void* ThreadFunc(void* _threadId)
{int TID = syscall(SYS_gettid);std::cout << "TID: " << TID << std::endl;std::cout << "pthread_self: " << pthread_self() << std::endl;pthread_exit(nullptr);
}int main()
{pthread_t tid;int rc = pthread_create(&tid, nullptr, ThreadFunc, (void*)tid);if (rc != 0){std::cout << "线程创建失败" << std::endl;;return -1;}// 等待线程退出int res = pthread_join(tid, nullptr);if (res != 0){std::cout << strerror(res) << std::endl;return -1;}std::cout << "线程已经退出" << std::endl;return 0;
}[root@Zhn 线程]# g++ pthread_detach.cpp -o pthread_detach -lpthread
[root@Zhn 线程]# ./pthread_detach
TID: 8718
pthread_self: 140005981333248
线程已经退出
[root@Zhn 线程]#
pthread_equal
在同一个线程组内,线程库提供了接口,可以判断两个线程ID是否对应着同一个线程:
#include <pthread.h>int pthread_equal(pthread_t t1, pthread_t t2);
返回值是0的时候,表示两个线程是同一个线程,非零值则表示不是同一个线程。
pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
测试主线程的栈空间大概是多大
// 查看一个线程的栈最大占用多少内存空间#include <iostream>
#include <pthread.h>
#include <unistd.h>int i = 0;void func()
{// int类型是4个字节,256个int类型是4 * 256 = 1024,也就是1kB// 每执行一次func函数就会占用1kB的栈空间,看看可以执行多少次这个函数int buffer[256];std::cout << "i = " << i << std::endl;i++;func();
}int main()
{func();sleep(100);
}
可以执行8053次,每次是1kB,也就是8053 / 1024,大概是8MB。
pthread_setname_np
给线程设置名称
#include <pthread.h>int pthread_setname_np(pthread_t thread, const char *name);
// pthread_setname_np示例#include <iostream>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>void* ThreadFunc(void* _threadId)
{// 设置线程名称pthread_setname_np(pthread_self(), std::to_string(pthread_self()).c_str());while (1){std::cout << "线程ID: " << pthread_self() << " hello world" << std::endl;sleep(1);}pthread_exit(nullptr);
}int main()
{pthread_t tid[5];for (int i = 0;i < 5;i++){int rc = pthread_create(&tid[i], nullptr, ThreadFunc, (void*)tid[i]);if (rc != 0){std::cout << "线程创建失败" << std::endl;;return -1;}}// 等待所有线程结束pthread_exit(nullptr);return 0;
}
创建了五个线程,给每个线程设置名称为自己线程ID。
CMD字段就是设置的线程名称。
pthread_exit
有生就有灭,线程执行完任务,也需要终止。
下面的三种方法中,线程会终止,但是进程不会终止(如果线程不是进程组里的最后一个线程的话):
- 创建线程时的start_routine(线程执行函数)函数执行了return,并且返回指定值。
- 线程调用pthread_exit。
- 其他线程调用了pthread_cancel函数取消了该线程。如果线程组中的任何一个线程调用了exit函数,或者主线程在main函数中执行了return语句,那么整个线程组内的所有线程都会终止。
pthread_exit函数的定义:
#include <pthread.h>void pthread_exit(void *value_ptr);
value_ptr是一个指针,存放线程的“临终遗言”。线程组内的其他线程可以通过调用pthread_join函数接收这个地址,从而获取到退出线程的临终遗言。如果线程退出时没有什么遗言,则可以直接传递NULL指针,如下所示:
pthread_exit(NULL);
但是这里有一个问题,就是不能将遗言存放到线程的局部变量里,因为如果用户写的线程函数退出了,线程函数栈上的局部变量可能就不复存在了,线程的临终遗言也就无法被接收者读到。
那我们应该如何正确地传递返回值呢?
- 如果是int型的变量,则可以使用“pthread_exit((int*)ret);”。
- 使用全局变量返回。
- 将返回值填入到用malloc在堆上分配的空间里。·使用字符串常量,如pthread_exit(“hello,world”)。
线程退出有一种比较有意思的场景,即线程组的其他线程仍在执行的情况下,主线程却调用pthread_exit函数退出了。这会发生什么事情?首先要说明的是这不是常规的做法,但是如果真的这样做了,那么主线程将进入僵尸状态,而其他线程则不受影响,会继续执行。
pthread_join
线程库提供了pthread_join函数,用来等待某线程的退出并接收它的返回值。这种操作被称为连接(joining)。
#include <pthread.h>int pthread_join(pthread_t thread, void **retval);
第一个参数表示要等待的线程ID,第二个参数代表线程退出的返回值。
根据等待的线程是否退出,可得到如下两种情况:
- 等待的线程尚未退出,那么pthread_join的调用线程就会陷入阻塞。
- 等待的线程已经退出,那么pthread_join函数会将线程的退出值(void*类型)存放到retval指针指向的位置。
线程的连接(join)操作有点类似于进程等待子进程退出的等待(wait)操作,但还是有不同之处:
- 第一点不同之处是进程之间的等待只能是父进程等待子进程,而线程则不然。线程组内的成员是对等的关系,只要是在一个线程组内,就可以对另外一个线程执行连接(join)操作。
- 第一点不同之处是进程之间的等待只能是父进程等待子进程,而线程则不然。线程组内的成员是对等的关系,只要是在一个线程组内,就可以对另外一个线程执行连接(join)操作。
返回值:
返回值 | 说明 |
---|---|
ESRCH | 传入的线程ID不存在,查无此线程 |
EINVAL | 线程不是一个可连接的线程或者已经有其他线程连接 |
EDEADLK | 死锁 |
pthread_join不能连接线程组内任意线程的做法,并不是NPTL线程库设计上的瑕疵,而是有意为之的。如果听任线程连接线程组内的任意线程,那么所谓的任意线程就会包括其他库函数私自创建的线程,当库函数尝试连接(join)私自创建的线程时,发现已经被连接过了,就会返回EINVAL错误。如果库函数需要根据返回值来确定接下来的流程,这就会引发严重的问题。正确的做法是,连接已知线程ID的那些线程,就像pthread_join函数那样。
pthread_join函数之所以能够判断是否死锁和连接操作是否被其他线程捷足先登,是因为目标线程的控制结构体struct pthread中,存在如下成员变量,记录了该线程的连接者:
struct pthread *joinid;
该指针存在三种可能,如下。
- NULL:线程是可连接的,但是尚没有其他线程调用pthread_join来连接它。
- 指向线程自身的struct pthread:表示该线程属于自我了断型,执行过分离操作,或者创建线程时,设置的分离属性为PTHREAD_CREATE_DETACHED,一旦退出,则自动释放所有资源,无需其他线程来连接。
- 指向线程组内其他线程的struct pthread:表示joinid对应的线程会负责连接。
为什么要连接退出的线程
如果不连接退出的线程,会导致资源无法释放
如果不执行连接操作,线程的资源就不能被释放,也不能被复用,这就造成了资源的泄漏。
值得一提的是,纵然调用了pthread_join,也并没有立即调用munmap来释放掉退出线程的栈,它们是被后建的线程复用了,这是NPTL线程库的设计。释放线程资源的时候,NPTL认为进程可能再次创建线程,而频繁地munmap和mmap会影响性能,所以NTPL将该栈缓存起来,放到一个链表之中,如果有新的创建线程的请求,NPTL会首先在栈缓存链表中寻找空间合适的栈,有的话,直接将该栈分配给新创建的线程。
始终不将线程栈归还给系统也不合适,所有缓存的栈大小有上限,默认是40MB,如果缓存起来的线程栈的空间总和大于40MB,NPTL就会扫描链表中的线程栈,调用munmap将一部分空间归还给系统。
// pthread_join示例#include <iostream>
#include <pthread.h>
#include <unistd.h>#define Threads 5void* ThreadFunc(void* _args)
{int thread_num = *((int*)_args);printf("Thread %d: Hello, World!\n", thread_num);int* retval = new int;*retval = thread_num * 2;pthread_exit(reinterpret_cast<void*>(retval));
}int main()
{pthread_t tid[Threads];for (int i = 0;i < Threads;i++){int* p = new int;*p = i;int rc = pthread_create(&tid[i], nullptr, ThreadFunc, reinterpret_cast<void*>(p));if (rc != 0){std::cout << "线程" << i << "创建失败" << std::endl;return -1;}}// 等待线程结束并接收返回值for (int i = 0;i < Threads; i++){void* retval;pthread_join(tid[i], &retval);std::cout << "线程" << i << "退出, 返回值是" << *reinterpret_cast<int*>(retval) << std::endl;}std::cout << "线程全部退出" << std::endl;return 0;
}[root@Zhn 线程]# g++ pthread_join.cpp -o pthread_join -lpthread
[root@Zhn 线程]# ./pthread_join
Thread 0: Hello, World!
Thread 1: Hello, World!
Thread 3: Hello, World!
Thread 2: Hello, World!
Thread 4: Hello, World!
线程0退出, 返回值是0
线程1退出, 返回值是2
线程2退出, 返回值是4
线程3退出, 返回值是6
线程4退出, 返回值是8
线程全部退出
[root@Zhn 线程]#
可以看到,程序会阻塞在pthread_join,直到所有线程全部退出。
pthread_detach
默认情况下,新创建的线程处于可连接(Joinable)的状态,可连接状态的线程退出后,需要对其执行连接操作,否则线程资源无法释放,从而造成资源泄漏。
如果其他线程并不关心线程的返回值,那么连接操作就会变成一种负担:你不需要它,但是你不去执行连接操作又会造成资源泄漏。这时候你需要的东西只是:线程退出时,系统自动将线程相关的资源释放掉,无须等待连接。NPTL提供了pthread_detach函数来将线程设置成已分离(detached)的状态,如果线程处于已分离的状态,那么线程退出时,系统将负责回收线程的资源,如下:
#include <pthread.h>int pthread_detach(pthread_t thread);
参数就是要分离的线程ID,可以是线程组内其他线程对目标线程进行分离,也可以是线程自己执行pthread_detach函数,将自身设置成已分离的状态,如下:
pthread_detach(pthread_self())
线程的状态之中,可连接状态和已分离状态是冲突的,一个线程不能既是可连接的,又是已分离的。因此,如果线程处于已分离的状态,其他线程尝试连接线程时,会返回EINVAL错误。
返回值:
返回值 | 说明 |
---|---|
ESRCH | 传入的线程ID不存在,查无此线程 |
EINVAL | 线程不是一个可连接的线程,已经处于分离状态 |
// pthread_detach示例#include <iostream>
#include <pthread.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>void* ThreadFunc(void* _threadId)
{int TID = syscall(SYS_gettid);std::cout << "TID: " << TID << std::endl;std::cout << "pthread_self: " << pthread_self() << std::endl;pthread_exit(nullptr);
}int main()
{pthread_t tid;#ifdef USE_ATTRpthread_attr_t attr;// 将线程状态设置为分离状态pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);int rc = pthread_create(&tid, nullptr, ThreadFunc, (void*)tid);if (rc != 0){std::cout << "线程创建失败" << std::endl;;return -1;}#elseint rc = pthread_create(&tid, nullptr, ThreadFunc, (void*)tid);if (rc != 0){std::cout << "线程创建失败" << std::endl;;return -1;}int res = pthread_detach(tid);if (res != 0){std::cout << strerror(res) << std::endl;return -1;}#endifstd::cout << "已设置线程分离" << std::endl;return 0;
}[root@Zhn 线程]# g++ pthread_detach.cpp -o pthread_use_attr_detach -DUSE_ATTR -lpthread
[root@Zhn 线程]# g++ pthread_detach.cpp -o pthread_no_attr_detach -lpthread
[root@Zhn 线程]# ./pthread_no_attr_detach
已设置线程分离TID:
8428
pthread_self: pthread_self: 139697266493184 hello world
[root@Zhn 线程]# ./pthread_no_attr_detach
已设置线程分离TID: 8485
pthread_self: 140716723963648 hello world[root@Zhn 线程]# ./pthread_use_attr_detach
已设置线程分离
[root@Zhn 线程]#
我们使用两种方式设置线程分离,一种是通过属性设置,还有一种是调用pthread_detach设置分离,执行了三次程序,可以发现第三次线程没有输出,这是为什么?因为我们设置线程分离,那么主线程先抢到CPU执行之后就退出了,还没轮到子线程执行。