1. pthread 线程库
因为 Linux 中没有专门为线程设计一个内核数据结构,所以内核中并没有很明确的线程的概念,而是用进程模拟的线程,只有轻量级进程的概念。这就注定了 Linux 中不会给我们直接提供线程的系统调用,只会给我们提供轻量级进程的系统调用!可是我们用户需要线程的接口,所以在用户和系统之间,Linux 开发者们给我们开发出来一个 pthread 线程库,这个库是在应用层的,它是对轻量级进程的接口进行了封装,为用户提供直接线程的接口!虽然这个是第三方库,但是这个库是几乎所有的 Linux 平台都是默认自带的!所以在 Linux 中编写多线程代码,需要使用第三方库 pthread 线程库!
(1)pthread_create()
接下来我们介绍 pthread 库中的第一个接口,创建一个线程:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
其中第一个参数是一个输出型参数,一旦我们创建好线程,我们是需要线程 id 的,所以该参数就是把线程 id 带出来;第二个参数 attr 为线程的属性,我们不用关心,设为 nullptr 即可。
第三个参数是一个函数指针类型,也就是说我们需要传一个函数进去。当我们创建线程的时候,我们是想让执行流执行代码的一部分,那么我们就可以把该线程要执行入口函数地址传进去,线程一启动就会转而执行该指针指向的函数处!关于该函数指针的返回值和参数,都是 void*,因为 void* 可以接收或者返回任意指针类型,这样就可以支持泛型了。而第四个参数 arg 是一个输入型参数,当线程创建成功,新线程回调线程函数的时候,如果需要参数,这个参数就是给线程函数传递的,也就是说该参数是给第三个参数函数指针中的参数传递的。
而函数的返回值,如果我们创建成功就返回0;如果失败会返回错误码,而没有设置 errno.
最后我们在编译的时候需要加上 -lpthread 指定库名称。
示例代码:
void* pthread_handler(void* attr)
{
while(1)
{
cout << "i am a new thread, pid: " << getpid() << endl;
sleep(2);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, pthread_handler, nullptr);
while(1)
{
cout << "i am main thread, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
如上图,我们以前写的代码中是不可能出现两个死循环的,但是使用创建线程之后就可以了,这就说明它们是不同的执行流。而它们的 pid 是一样的,就说明它们是同一个进程。
而我们右侧终端中,正在查看两个执行流,其中查看执行流的指令为:ps -aL
,我们上面循环打印了方便观察,我们看到 pid 是一样的,但是 LWP 是什么呢?为什么会不一样呢?在 Linux 中没有具体的线程概念,只有轻量级进程的概念,所以 CPU 在调度时,不仅仅只要看 pid,更重要的是每一个轻量级进程也要有自己对应的标识符,所以轻量级进程就有了 LWP (light weight process)这样的标识符,所以 CPU 是按照 LWP 来进行调度的!
但是我们如果杀掉上面任意一个执行流的 LWP,默认整个进程都会被终止,这就是线程的健壮性差的原因。
如果我们定义一个函数,或者全局变量,分别在两个执行流中执行,它们都可以读取到该函数和全局变量,如下代码:
void Print(const string& str)
{
cout << str << endl;
}
void* pthread_handler(void* attr)
{
while(1)
{
cout << "i am a new thread, pid: " << getpid() << ", val = " << val << endl;
Print("i am new thread");
sleep(2);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, pthread_handler, nullptr);
while(1)
{
cout << "i am main thread, pid: " << getpid() << ", val = " << val << endl;
Print("i am main thread");
val++;
sleep(1);
}
return 0;
}
有关线程的 id 的问题我们后面再谈。
(2)pthread_join()
那么创建线程后是主线程先运行还是新线程先运行呢?不确定,要看CPU先调度谁,那么肯定的是主线程是最后退出的!因为主线程退了整个进程就退出了,所以主线程要进行线程等待!如果主线程不进行线程等待,会导致类似于僵尸进程的问题!而 pthread_join() 就是进行线程等待的接口。
int pthread_join(pthread_t thread, void **retval);
其中第一个参数,为线程的 id;第二个参数 retval 我们先不管,后面再介绍,设为 nullptr 即可。下面我们简单写一个程序:
void* pthread_handler(void* attr)
{
int cnt = 5;
while(cnt--)
{
cout << "i am a new thread, pid: " << getpid() << endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, pthread_handler, nullptr);
pthread_join(tid, nullptr);
cout << "main thread quit..." << endl;
return 0;
}
结果如下:
我们可以看到当新线程在运行的时候,主线程并没有直接运行结束,而是进行阻塞等待!
接下来我们说一下第二个参数 retval;其实我们给线程分配的函数,它的返回值是直接写入 pthread 库中的,而 retval 也是被封装在库中,所以我们可以根据 retval 读取到函数的返回值,也就是说这个 retval 就是一个输出型参数!首先我们需要定义一个 void* 类型的变量,然后将这个变量取地址当作 pthread_join 的第二个参数传入即可!例如以下代码:
void* pthread_handler(void* attr)
{
return (void*)1234;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, pthread_handler, nullptr);
void* retval;
pthread_join(tid, &retval);
cout << "main thread quit, retval = " << (long long)retval << endl;
return 0;
}
(3)pthread_exit()
那么除了在函数中直接 return 终止线程外,还有什么方法吗?有的,pthread_exit() 接口就是用来终止线程的:
void pthread_exit(void *retval);
参数就是和 void* 返回值一样。注意线程内不能使用 exit() 系统接口终止线程,因为 exit() 是用来终止进程的!例如:
void* pthread_handler(void* attr)
{
pthread_exit((void*)1234);
}
(4)pthread_cancel()
除了上面的方法,pthread_cancel() 也可以取消一个线程,参数就是目标线程的 id:
int pthread_cancel(pthread_t thread);
返回值如下:
如果 thread 线程被别的线程调用 pthread_ cancel 异常终掉, pthread_join 第二个参数 retval 所指向的单元里存放的是常数PTHREAD_ CANCELED,也就是 -1.
(5)简单使用 pthread 库
假设我们现在需要写一个线程进行整数相加,代码如下:
Request 类为一个需求类,_start 和 _end 为需要求的整数相加的范围。
class Request
{
public:
Request(int start, int end)
:_start(start)
,_end(end)
{}
~Request()
{
cout << "~Request()" << endl;
}
public:
int _start;
int _end;
};
Result 类为一个结果的类,Run 方法为求和方法;_result 为计算结果;_exitcode 为记录计算结果是否可靠。
class Result
{
public:
Result(int result, int exitcode)
:_result(result)
,_exitcode(exitcode)
{}
void Run(int start, int end)
{
for(int i = start; i <= end; i++)
{
_result += i;
}
}
~Result()
{
cout << "~Result()" << endl;
}
public:
int _result; // 计算结果
int _exitcode; // 计算结果是否可靠
};
下面为测试代码:
void* countSum(void* args)
{
Request* rq = static_cast<Request*>(args);
Result* res = new Result(0, 0);
res->Run(rq->_start, rq->_end);
return res;
}
int main()
{
Request* rq = new Request(1, 100);
pthread_t tid;
pthread_create(&tid, nullptr, countSum, rq);
void* res;
pthread_join(tid, &res);
Result* req = static_cast<Result*>(res);
cout << req->_result << endl;
delete req;
delete rq;
return 0;
}
结果如下:
所以线程的参数和返回值,不仅仅可以用来进行传递一般参数,也可以传递对象!
2. 理解线程库
(1)线程 id
我们上面学习了 pthread_create() 接口,但是第一个参数就是线程的 id,我们至今都没有介绍过它,所以我们可以尝试打印一下看一下它究竟长什么样,如下:
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, mythread, nullptr);
cout << "tid = " << tid << endl;
pthread_join(tid, nullptr);
return 0;
}
我们可以看到 tid 是一个非常大的数字,假设我们换成十六进制呢?如下图:
我们可以看到,它很像一个地址。
如果线程想要获得自己的线程 id,还可以通过线程库中的接口获得,如下:
pthread_t pthread_self(void);
返回值就是线程的 id.
那么这个线程 id 究竟是什么呢?因为 Linux 中没有明确的线程概念,所以没有直接提供线程的系统接口,只能给我们提供轻量级进程的系统接口,那么系统中是怎么创建轻量级进程呢?其实是用 clone()
接口,如下:
其实这个接口就是创建一个子进程,fork()
的底层原理和 clone()
类似,但是 clone()
是专门用来创建轻量级进程的。第一个参数函数指针类型,就是新创建执行流要执行的函数地址入口;第二个参数 child stack 就是自己自定义的栈;第三个参数就是是否让地址空间共享;后面的参数就不用关心了。
所以,这个接口就被线程库封装了,给我们提供的就是我们上面所介绍的线程库的接口。所以,clone()
允许用户传入一个回调函数和一个用户空间,来代表这个轻量级进程运行过程中所执行的代码,它在运行中的临时变量全部放在用户空间栈上。也就是说,线程库需要封装 clone()
的话,线程库中每一个线程都要给 clone()
提供执行方法,还要在线程库中开辟空间。所以,线程的概念是库给我们维护的。另外,我们用的第三方线程库,是需要加载到内存里的!而且是加载到共享区中!那么,在 pthread 库里面,每个创建好的线程,它就要为该线程在库里面开辟一段空间,用来充当新线程的栈!也就是说,新线程的栈是在共享区当中的!
那么,线程的概念是库给我们维护的,也就是说线程库要维护线程的概念,不需要维护线程的执行流。也就是,线程库中的线程在底层中对应的其实是轻量级进程的执行流,但是线程相关的属性等字段,必须需要库来维护!所以线程库注定了要维护多个线程属性的集合,所以线程库需要先描述,再组织管理这些线程!如下图:
所以,我们每创建一个线程,在线程库中就要为我们创建线程库级别的线程,我们把它叫做线程控制块。所以这个线程控制块我们就可以理解成 tcb,那么对于每一个 tcb 在库中可以理解成用数组的方式进行管理维护。所以为了让我们快速找到在共享库中的每一个 tcb,我们把每一个 tcb 在内存中的起始地址称为线程的 tid,即线程的 id!
(2)线程栈
每一个线程在运行时,一定要有自己独立的栈结构,因为每一个线程都要有自己的调用链,也就是说每一个线程都要有自己调用链所对应的栈帧结构。这个栈结构会保存任何一个执行流在运行过程中的所有临时变量。其中,主线程用地址空间提供的栈结构即可,而新线程则是首先在库中创建一个线程控制块,这个控制块中有包含默认大小的空间,就是线程栈;然后库就要帮我们调用系统接口 clone()
帮我们创建执行流,最重要的是它会帮我们把线程栈传递给 clone()
,作为它的第二个参数!
所以,所有对应的非主线程的栈都在库中进行维护,即在共享区中维护,具体来说,是在 pthread 库中 tid 指向的线程控制块中!
我们可以写代码验证一下每一个线程都有自己独立的栈,代码链接:验证独立栈.
结果如下,test_stack 是三个线程里的临时变量,它们的地址都不一样:
同时我们也可以验证,全局变量是可以被所有线程同时看到并访问的。
其实线程和线程之间,几乎没有秘密,虽然它们是独立的栈,但是线程上的数据也是可以被其它线程访问到的。
(3)线程局部存储
我们知道,全局变量是可以被所有线程访问的,但是假设我们的线程想要一个私有的全局变量呢?我们可以在一个全局变量前加上 __thread
,如下:
__thread int g_val = 100;
接下来我们使用上面的代码,设置这样一个全局变量,并打印它的信息出来观察:
我们发现,每一个线程中的 g_val 的地址都是不一样的!而且对 g_val 运算的时候,它们互不干扰!所以这个 g_val 加上 __thread,就变成了线程的全局变量!其实 __thread 不是 C/C++ 提供的,而是一个编译选项。我们发现,打印出来的地址非常大,因为它是在堆栈之间的地址!它是位于线程控制块的线程局部存储区域!
注意,线程局部存储只能定义内置类型,不能定义自定义类型!
3. 分离线程
-
默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成系统泄漏。
-
如果不关心线程的返回值,join 是一种负担,因为主线程需要等待其它线程,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源,这就叫做线程的分离,可以用如下接口:
int pthread_detach(pthread_t thread);
其中参数就是线程的 tid.
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());