目录
一.POSIX线程库
站在内核的角度,OS只有轻量级进程,没有线程的概念,但是站在用户的角度我们只有线程没有轻量级进程的概念。因为Linux下没有真正意义上的线程,而是用进程模拟的线程,所以Linux不会提供直接创建线程的系统调用,最多给我们提供创建轻量级进程的接口。
所以linux对下对LWP的接口进行封装,对上给用户提供线程控制的接口------POSIX线程库,pthread库,这是任何一个linux系统都会带的库,又叫原生线程库。
POSIX线程:
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以"pthread_"打头的。
- 要使用这些函数库,要通过引入头文<pthread.h>。
- 链接这些线程函数库时要使用编译器命令的"-lpthread"选项。
二.线程创建
1.创建线程接口
功能:创建一个新的线程。
原型:
cppint pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
参数:
- thread:返回线程ID。
- attr:设置线程的属性,attr为NULL表示使用默认属性。
- start_routine:是个函数地址,线程启动后要执行的函数,该函数返回值是void*,参数是void*。
- arg:传给线程启动函数的参数。
返回值:
- 成功返回0;失败返回错误码。
测试代码:
cpp
#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void *FuncRun(void *argc)
{
while (1)
{
cout << "I am thread,my pid:" << getpid() << endl;
sleep(1);
}
}
int main()
{
//线程id
pthread_t id;
//创建线程
pthread_create(&id, NULL, FuncRun, NULL);
while (1)
{
cout << "I am main,my pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
测试结果:
说明:
- 线程没有父子之分,但是线程有主线程,和新线程的区分。
- 我们可以看到,主线程pid和新线程的pid是相同的。因为他们本身就是同一个进程的一部分。
- 新线程和主线程谁先被调度取决于调度器。
2.查看线程
查看线程使用命令:
bash
ps -aL
3.多线程的健壮性问题
测试代码:
cpp
#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void *FuncRun1(void *argc)
{
int count = 0;
while (1)
{
count++;
cout << "I am thread1,my pid:" << getpid() << endl;
if (count == 5)
{
int tmp = count / 0;
}
sleep(1);
}
}
void *FuncRun2(void *argc)
{
while (1)
{
cout << "I am thread2,my pid:" << getpid() << endl;
sleep(1);
}
}
int main()
{
// 线程id
pthread_t id1, id2;
// 创建线程
pthread_create(&id1, NULL, FuncRun1, NULL);
pthread_create(&id2, NULL, FuncRun2, NULL);
while (1)
{
cout << "I am main,my pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
测试结果:
说明:
- 代码其中有一个线程在第五秒的时候,会出现一个除0的问题。
- 当一个线程因为某一个错处而导致线程终止的时候,整个进程也都会直接终止。
- 因为信号是发送给进程的,最终OS直接对由信号做出的处理动作也是针对进程的。
4.线程函数参数传递
我们想通过给线程函数传参让线程执行更加复杂的任务。
cpp
#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 任务,计算[1-top]的求和,并将结果存储到sum中。
struct task
{
task(int top, int num)
: _thread_name("thread" + to_string(num)), _top(top), _sum(0), _num(num)
{
}
string _thread_name; // 线程名字
int _top; // 计算数据范围
int _sum; // 结果
int _num; // 线程编号
};
// 线程函数
void *FuncRun(void *argc)
{
task *t = (task *)argc;
for (int i = 1; i <= t->_top; i++)
{
t->_sum += i;
}
}
int main()
{
// 线程id
pthread_t id1, id2;
// 创建线程
task t1(100, 1);
task t2(150, 2);
pthread_create(&id1, NULL, FuncRun, &t1);
pthread_create(&id2, NULL, FuncRun, &t2);
// 等待线程计算完再输出结果
sleep(1);
cout << t1._thread_name << ":[1-" << t1._top << "]=" << t1._sum << endl;
cout << t2._thread_name << ":[1-" << t2._top << "]=" << t2._sum << endl;
return 0;
}
测试结果:
说明:
- 由于接口的设计上参数的类型是void* ,这也就使得我们可以给线程函数传递的参数是非常丰富的。
5.线程id和地址空间
- pthread_ create 函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
- 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
- pthread_ create 函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
cpp
pthread_t pthread_self(void);
pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
我们在对线程做操作的时候,根本上是使用线程库对线程进行操作,那么线程库本质就是存在于linux上的动态库,在我们使用线程库的时候,线程库也会向普通的动态库一样加载到共享区中,我们使用线程库方法就是访问自己的地址空间。
线程库中需要被管理的线程会有很多,线程库也必然实现了线程的数据结构------TCB(线程控制块)。
每个线程都有自己的TCB,和独立的上下文数据,以及线程栈空间。pthread_t 就是指向他们的首地址的一个地址。
三.线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit终止自己。
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
1.pthread_exit
功能:线程终止.
原型:void pthread_exit(void *value_ptr);
参数:value_ptr:value_ptr不要指向一个局部变量,返回线程结果。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。
测试代码:
cpp
#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void *FuncRun1(void *argc)
{
int count = 0;
while (1)
{
count++;
cout << "I am thread-1-count:" << count << endl;
sleep(1);
// 三秒后线程1退出
if (count == 3)
{
pthread_exit(NULL);
}
}
}
void *FuncRun2(void *argc)
{
int count = 0;
while (1)
{
count++;
cout << "I am thread-2-count:" << count << endl;
sleep(1);
// 三秒后线程2退出
if (count == 3)
{
pthread_exit(NULL);
}
}
}
int main()
{
// 线程id
pthread_t id1, id2;
// 创建线程
pthread_create(&id1, NULL, FuncRun1, NULL);
pthread_create(&id2, NULL, FuncRun2, NULL);
while (1)
{
cout << "I am main,my pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
测试结果:
说明:
- 线程在退出后,主线程并没有受到影响,进程也有没受到影响。
- 需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
2.pthread_cancel
功能:取消一个执行中的线程
原型:int pthread_cancel(pthread_t thread);
参数:thread:线程ID
返回值:成功返回0;失败返回错误码
测试代码:
cpp
#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void *FuncRun1(void *argc)
{
int count = 0;
while (1)
{
count++;
cout << "I am thread-1-count:" << count << endl;
sleep(1);
}
}
void *FuncRun2(void *argc)
{
int count = 0;
while (1)
{
count++;
cout << "I am thread-2-count:" << count << endl;
sleep(1);
}
}
int main()
{
// 线程id
pthread_t id1, id2;
// 创建线程
pthread_create(&id1, NULL, FuncRun1, NULL);
pthread_create(&id2, NULL, FuncRun2, NULL);
int count = 0;
while (1)
{
count++;
sleep(1);
if (count == 3)
{
// 三秒后终止线程
pthread_cancel(id1);
pthread_cancel(id2);
}
cout << "I am main" << endl;
}
return 0;
}
测试结果:
四.线程等待
为什么需要线程等待?
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
功能:等待线程结束。
原型:int pthread_join(pthread_t thread, void **value_ptr);
参数:thread:线程ID。
value_ptr:它指向一个指针,后者指向线程的返回值。
返回值:成功返回0;失败返回错误码。
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数,PTHREAD_ CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
测试代码:
cpp
void *FuncRun1(void *argc)
{
int *top = (int *)argc;
int *sum = new int;
for (int i = 1; i <= *top; i++)
{
*sum += i;
}
// 线程退出
pthread_exit(sum);
}
void *FuncRun2(void *argc)
{
int *top = (int *)argc;
int *sum = new int;
for (int i = 1; i <= *top; i++)
{
*sum += i;
}
// 线程退出
return sum;
}
void *FuncRun3(void *argc)
{
int *top = (int *)argc;
int *sum = new int;
for (int i = 1; i <= *top; i++)
{
*sum += i;
sleep(1);
}
free(sum);
// 线程退出
}
int main()
{
int top1 = 100;
int top2 = 150;
int top3 = 200;
pthread_t id1;
pthread_t id2;
pthread_t id3;
pthread_create(&id1, NULL, FuncRun1, &top1);
pthread_create(&id2, NULL, FuncRun2, &top2);
pthread_create(&id3, NULL, FuncRun3, &top3);
pthread_cancel(id3);
// 接受线程返回数据
void *ret_ptr1;
void *ret_ptr2;
void *ret_ptr3;
// 等待线程
pthread_join(id1, &ret_ptr1);
pthread_join(id2, &ret_ptr2);
pthread_join(id3, &ret_ptr3);
cout << "ret1:" << *((int *)ret_ptr1) << endl;
free(ret_ptr1);
cout << "ret2:" << *((int *)ret_ptr2) << endl;
free(ret_ptr2);
if (ret_ptr3 == PTHREAD_CANCELED)
cout << "ret3:PTHREAD_CANCELED" << endl;
return 0;
}
测试结果:
五.线程分离
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
cpp
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
cpp
pthread_detach(pthread_self());
joinable 和分离是冲突的,一个线程不能既是joinable又是分离的。
测试代码:
cpp
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void *FuncRun(void *argc)
{
// 线程分离
pthread_detach(pthread_self());
int count = 0;
while (1)
{
count++;
cout << "I am thread-count:" << count << endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
int n = pthread_create(&tid, NULL, FuncRun, NULL);
if (n != 0)
{
cerr << "pthread_create:" << strerror(errno) << endl;
}
sleep(2);
// 线程已经分离,再去线程等待,pthread_join会立即报错。
if (pthread_join(tid, NULL) == 0)
{
printf("pthread wait success\n");
}
else
{
printf("pthread wait failed\n");
}
return 0;
}
测试结果: