线程

线程的概念

线程是轻量级的进程(LWP:light weight process),在 Linux 环境下线程的本质仍是进程。
在计算机上运行的程序是一组指令及指令参数的组合,指令按照既定的逻辑控制计算机运行。
操作系统会以进程为单位,分配系统资源。进程是资源分配的最小单位,线程是操作系统调度执行的最小单位。

线程和进程之间的区别:
(1)线程是程序的最小执行单位,进程是操作系统中最小的资源分配单位。
每个进程对应一个虚拟地址空间,一个进程只能抢一个 CPU 时间片。
一个地址空间中划分出多个线程,能够抢更多的 CPU 时间片。
(2)进程有自己独立的地址空间,多个线程共用同一个地址空间。
线程更加节省系统资源,效率更高。
在一个地址空间中多个线程独享:每个线程都有属于自己的栈区、寄存器 (内核中管理的)
在一个地址空间中多个线程共享:代码段、全局数据区、堆区、打开的文件 (文件描述符表) 都是线程共享的
(3)CPU 的调度和切换:线程的上下文切换比进程快很多
上下文切换:进程/线程分时复用 CPU 时间片,在切换之前会将上一个任务的状态进行保存,
下次切换回这个任务的时候,加载这个状态继续运行,任务从保存到再次加载这个过程就是一次上下文切换。
(4)线程启动速度更快,退出也快,对系统资源的冲击小。

在处理多任务程序的时候使用多线程比使用多进程更有优势,但是线程并不是越多越好,如何控制线程的个数呢?
(1)IO 密集型
文件 IO 对 CPU 是使用率不高,因此可以分时复用 CPU 时间片,线程的个数 = 2 * CPU 核心数(效率最高)
(2)CPU 密集型
主要是 CPU 进行运算,线程的个数 = CPU 的核心数(效率最高)

创建线程

线程函数

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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, 线程库的全名: libpthread.so libptread.a
/*
参数:
thread:
传出参数,是无符号长整形数,线程创建成功,会将线程 ID 写入到这个指针指向的内存
attr:
线程的属性,一般情况下使用默认属性 NULL
start_routine:
函数指针,创建出的子线程的处理动作,也就是该函数在子线程中执行。
arg:
作为实参传递到 start_routine 指针指向的函数内部
返回值:
线程创建成功返回 0,创建失败返回对应的错误号
*/

示例

创建线程的示例代码,在创建过程中一定要保证编写的线程函数与规定的函数指针类型一致:void* (*start_routine) (void*)

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
36
37
// pthread_create.c 
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

// 子线程的处理代码
void* callback(void* arg)
{
printf("child thread, ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf("child thread, i = %d\n", i);
}
return NULL;
}

int main()
{
// 1.创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, callback, NULL);

// 2.子线程不会执行下边的代码, 主线程执行
printf("main thread, ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("main thread, i = %d\n", i);
}

// 休息一会儿...
sleep(1);

return 0;
}
// gcc .\pthread_create.c -lpthread -o app

在打印的输出中为什么子线程处理函数没有执行完毕呢(只看到了子线程的部分日志输出)?
主线程一直在运行,执行期间创建出了子线程,说明主线程有 CPU 时间片,在这个时间片内将代码执行完毕了,主线程就退出了。子线程被创建出来之后需要抢 cpu 时间片, 抢不到就不能运行。如果主线程退出,虚拟地址空间就被释放,子线程一并被销毁。但是如果某一个子线程退出,主线程仍在运行,虚拟地址空间依旧存在。

结论:在没有人为干预的情况下,虚拟地址空间的生命周期和主线程是一样的,与子线程无关。
解决方案:让子线程执行完毕,主线程再退出,可以在主线程中添加挂起函数 sleep()

线程退出

线程函数

在编写多线程程序的时候,如果想要让线程退出,但是不会导致虚拟地址空间的释放(针对于主线程),可以调用线程库中的线程退出函数,调用该函数当前线程就马上退出,并且不会影响到其他线程的正常运行,不论是在子线程或者主线程中都可以使用。

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
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// pthread_exit.c 
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

// 子线程的处理代码
void* callback(void* arg)
{
printf("child thread, ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf("child thread, i = %d\n", i);
}
return NULL;
}

int main()
{
// 1.创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, callback, NULL);

// 2.子线程不会执行下边的代码, 主线程执行
printf("main thread, ID: %ld\n", pthread_self());
// 主线程退出
pthread_exit(NULL);

return 0;
}
// gcc .\pthread_exit.c -lpthread -o app

主线程退出后,不影响子线程的执行。

线程回收

线程函数

线程和进程一样,子线程退出的时候其内核资源主要由主线程回收。
线程库中提供的线程回收函叫做 pthread_join(),这个函数是一个阻塞函数。
如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次只能回收一个子线程;如果有多个子线程,则需要循环进行回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <pthread.h>
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞
// 子线程退出,函数解除阻塞,回收对应的子线程资源,类似于回收进程使用的函数 wait()
int pthread_join(pthread_t thread, void **retval);
/*
参数:
thread:
要被回收的子线程的线程 ID
retval:
二级指针,指向一级指针的地址,是一个传出参数,
这个地址中存储了 pthread_exit () 传递出的数据,如果不需要这个参数,可以指定为 NULL
返回值:
线程回收成功返回 0,回收失败返回错误号。
*/

示例

在子线程退出的时候可以使用 pthread_exit() 的参数将数据传出,在回收这个子线程的时候可以通过 phread_join() 的第二个参数来接收子线程传递出的数据。

接收数据有很多种处理方式,列举几种:
(1)使用子线程栈
将子线程退出数据保存在子线程自己的栈区。如果多个线程共用同一个虚拟地址空间,每个线程在栈区都有一块属于自己的内存,相当于栈区被这几个线程平分。当线程退出时,线程在栈区的内存也就被回收。因此,随着子线程的退出,写入到栈区的数据也就被释放。
(2)使用全局变量
位于同一虚拟地址空间中的线程,虽然不能共享栈区数据,但是可以共享全局数据区和堆区数据。因此,在子线程退出的时可以将传出数据存储到全局变量、静态变量或者堆内存中。
(3)使用主线程栈
虽然每个线程都有属于自己的栈区空间,但是位于同一个地址空间的多个线程是可以相互访问对方的栈空间上的数据的。由于很多情况下还需要在主线程中回收子线程资源,所以主线程一般都是最后退出,基于这个原因将子线程返回的数据保存到了主线程的栈区内存中。

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
36
37
38
39
40
41
42
43
44
45
46
// pthread_join.c 
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

struct Test
{
int num;
int age;
};

// 子线程的处理代码
void* callback(void* arg)
{
printf("child thread, ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf("child thread, i = %d\n", i);
}

struct Test* t = (struct Test*)arg;
t->num = 100;
t->age = 6;
pthread_exit(t);

return NULL;
}

int main()
{
struct Test t;
// 1.创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, callback, &t);
// 2.子线程不会执行下边的代码, 主线程执行
printf("main thread, ID: %ld\n", pthread_self());

void* ptr;
pthread_join(tid, &ptr);
printf("num: %d, age = %d\n", t.num, t.age);

return 0;
}
// gcc .\pthread_join.c -lpthread -o app

在上面的程序中,创建子线程并将主线程中栈空间变量 p 的地址传递到了子线程中,在子线程中将要传递出的数据写入到了这块内存中。在程序的 main() 函数中,通过指针变量 ptr 或者通过结构体变量 p 都可以读出子线程传出的数据。

线程分离

线程函数

在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用 pthread_join() 只要子线程不退出主线程就会一直被阻塞,主要线程的任务也就不能被执行。

在线程库函数中线程分离函数 pthread_detach(),调用这个函数之后指定的子线程和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收。

线程分离之后在主线程中使用 pthread_join() 就回收不到子线程资源。

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

示例

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
36
37
38
39
// pthread_detach.c 
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

// 子线程的处理代码
void* callback(void* arg)
{
printf("child thread, ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf("child thread, i = %d\n", i);
}

return NULL;
}

int main()
{
// 1.创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, callback, NULL);
// 2.子线程不会执行下边的代码, 主线程执行
printf("main thread, ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("main thread, i = %d\n", i);
}

// 设置子线程和主线程分离
pthread_detach(tid);
// 让主线程自己退出
pthread_exit(NULL);

return 0;
}
// gcc .\pthread_detach.c -lpthread -o app

其他线程函数

线程取消

线程取消:在某些特定情况下在一个线程中杀死另一个线程。
使用这个函数杀死一个线程需要分两步:
(1)在线程 A 中调用线程取消函数 pthread_cancel,指定杀死线程 B,这时候线程 B 是死不了的。
(2)在线程 B 中进程一次系统调用(从用户区切换到内核区),否则线程 B 可以一直运行。

1
2
3
4
5
6
7
8
9
#include <pthread.h>
// 参数是子线程的线程ID
int pthread_cancel(pthread_t thread);
/*
参数:
要杀死的线程的线程 ID
返回值:
函数调用成功返回 0,调用失败返回非 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
36
37
38
39
40
41
42
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 子线程的处理代码
void* working(void* arg)
{
int j=0;
for(int i=0; i<9; ++i)
{
j++;
}
// 这个函数会调用系统函数, 因此这是个间接的系统调用
printf("child thread, ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf("child thread, i = %d\n", i);
}

return NULL;
}

int main()
{
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);

// 2. 子线程不会执行下边的代码, 主线程执行
printf("main thread, ID: %ld\n", pthread_self());

// 杀死子线程, 如果子线程中做系统调用, 子线程就结束了
pthread_cancel(tid);

// 让主线程自己退出
pthread_exit(NULL);

return 0;
}
// gcc .\pthread_cancel.c -lpthread -o app

线程ID比较

在 Linux 中线程 ID 是一个无符号长整形,因此可以直接使用比较操作符比较两个线程的 ID。
但是线程库是可以跨平台使用的,在某些平台上 pthread_t 可能不是一个单纯的整形,这中情况下比较两个线程的 ID 必须要使用比较函数。

1
2
3
4
5
6
7
8
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
/*
参数:
t1 和 t2 是要比较的线程的线程 ID
返回值:
如果两个线程 ID 相等返回非 0 值,如果不相等返回 0
*/

参考资料

https://subingwen.cn/linux/thread/


线程
https://lcf163.github.io/2021/08/18/线程/
作者
乘风的小站
发布于
2021年8月18日
许可协议