线程简介

线程是轻量级的进程。操作系统会以进程为单位,分配系统资源。进程是资源分配的最小单位,线程是操作系统调度执行的最小单位。

线程与进程

进程有独立的地址空间,多个线程共用同一个地址空间。

  • 线程更加节省系统资源。
  • 在一个地址空间中,每个线程都有属于自己的栈区,寄存器。
  • 在一个地址空间中,代码段,堆区,全局数据区,文件描述符表都是线程共享的。

线程是程序的最小执行单位,进程是操作系统中最小的资源分配单位。

线程的上下文切换比进程快的多。

线程更加廉价,启动速度更快,退出也快,对系统资源的冲击小。

在处理多任务程序的时候使用多线程比使用多进程要更有优势,但是线程并不是越多越好。

  1. 处理复杂的算法(主要是CPU进行运算),线程的个数=CPU的核心数。
  2. 处理IO密集型任务时,因为可以分时复用CPU时间片,所以线程个数可以略大于CPU的核心数(两倍)。

创建线程

线程函数

每个线程都有一个唯一的线程ID,类型为pthread_t,是一个无符号长整形。

1
pthread_t pthread_self(void); // 返回当前线程的线程ID

在一个进程中调用线程创建函数,就得到一个子线程,和进程不同,需要给每一个创建出的线程指定一个处理函数,否则这个线程无法工作。

1
2
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
  • thread:传出参数;线程创建成功,会将线程ID写入到这个指针指向的内存中。
  • attr:线程的属性,一般情况下使用默认属性,即NULL。
  • start_routine:函数指针,创建出的子线程的处理动作,该函数在子线程中执行。
  • arg:作为实参传递到start_routine指针指向的函数内部。
  • 返回值:创建成功返回0,创建失败返回对应的错误号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

void* callback(void* arg)
{
for(int i = 0; i < 5; i ++)
{
printf("子线程: i = %d\n", i);
}
printf("子线程:%ld\n", pthread_self());
return NULL;
}

int main()
{
pthread_t tid;
pthread_create(&tid, NULL, callback, NULL);
for(int i = 0; i < 5; i++)
{
printf("主线程: i = %d\n", i);
}
printf("主线程: %ld\n", pthread_self());
return 0;
}

执行结果。子线程还未执行,主线程就执行完毕,将资源释放掉了,所以子线程最终没有执行。

image-20240313132124485

在主线程中加入sleep,等待子线程执行完后,再释放资源。

image-20240313132221404

image-20240313132321069

线程退出

在编写多线程程序的时候,如果想要让线程退出,但是不会导致虚拟地址空间的释放,我们就可以调用线程库中的线程退出函数。

1
2
#include <pthread.h>
void pthread_exit(void *retval);
  • 参数:线程退出时携带的数据,当前子线程的主线程会得到该数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void* callback(void* arg)
{
for(int i = 0; i < 5; i ++)
{
printf("子线程: i = %d\n", i);
}
printf("子线程:%ld\n", pthread_self());
return NULL;
}

int main()
{
pthread_t tid;
pthread_create(&tid, NULL, callback, NULL);
printf("主线程: %ld\n", pthread_self());
pthread_exit(NULL);
return 0;
}

主线程结束后资源并未被释放,子线程继续执行完。

image-20240313133206157

线程回收

线程函数

1
2
3
4
#include <ptread.h>
// 这是一个阻塞函数,运行到这个函数时,线程会被阻塞
// 子线程退出,函数解除阻塞,回收对应的子线程资源
int pthread_join(pthread_t thread, void **retval);
  • 参数:
    • thread:要被回收的子线程的线程ID
    • retval:二级指针,是一个传出参数,这个地址中存储了pthread_exit()传递出的数据,如果不需要,可以指定为NULL。
  • 返回值:线程回收成功返回0,回收失败返回错误号。

回收子线程数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
struct Test{
int num;
int age;
};

void* callback(void* arg)
{
for(int i = 0; i < 5; i ++)
{
printf("子线程: i = %d\n", i);
}
printf("子线程:%ld\n", pthread_self());
struct Test t;
t.num = 100;
t.age = 6;
pthread_exit(&t);
return NULL;
}

int main()
{
pthread_t tid;
pthread_create(&tid, NULL, callback, NULL);
printf("主线程: %ld\n", pthread_self());
void* ptr;
pthread_join(tid, &ptr);
struct Test* pt = (struct Test*)ptr;
printf("num : %d, age : %d\n", pt->num,pt->age);
return 0;
}

image-20240314210611771

因为t在栈区,所以当子进程结束后, ptr指向的地址空间会被释放,因此最后的输出会是随机数。

将struct Test t定义成全局变量后,num和age正常输出。

image-20240314211832344

或者将t定义在main函数中,并传入到callback内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void* callback(void* arg)
{
for(int i = 0; i < 5; i ++)
{
printf("子线程: i = %d\n", i);
}
printf("子线程:%ld\n", pthread_self());
struct Test* t = (struct Test*)arg;
t->num = 100;
t->age = 6;
pthread_exit(t);
return NULL;
}

int main()
{
pthread_t tid;
struct Test t;
pthread_create(&tid, NULL, callback, &t);
printf("主线程: %ld\n", pthread_self());
void* ptr;
pthread_join(tid, &ptr);
printf("num : %d, age : %d\n", t.num,t.age);
return 0;
}

image-20240314212709827

线程分离

子线程和主线程分离,当子线程退出时,其占用的内核资源就系统的其他进程接管并回收。

1
2
3
#include <pthread.h>
// 参数:子线程的线程ID。主线程就和这个子线程分离了
int pthread_detach(pthread_t thread);

其他函数

线程取消

在某些特定情况下在一个线程中杀死另一个线程,总共有两步:

1.调用pthread_cancel,被指定的线程B不会马上死亡。

2.线程B中进行一次系统调用后,线程B死亡;否则线程B可以一直运行。