多线程基础

并发

  1. 多线程
  2. 多进程

基础概念

线程是轻量级的进程,在Linux环境下,线程的本质仍然是进程。操作系统会以进程为单位,分配系统资源。进程是资源分配的最小单位,线程是操作系统调度执行的最小单位。

线程和进程的区别:

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

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

  1. 文件IO操作:文件IO对CPU使用率不高,线程数 = 2 * CPU核心数
  2. 处理复杂算法:线程数 = CPU核心数

什么时候用多进程?

  • 在处理一个程序的时候,需要去启动另一个程序的时候,使用多进程。

并发的话,优先考虑多线程。

线程函数

头文件 <pthread.h>

线程ID

每个线程都有一个唯一的线程ID

  • 类型:pthread_t
  • 无符号长整形
1
2
// 返回当前线程的ID
pthread_t pthread_self(void);

创建线程

1
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void*(*start_routine)(void*), void *arg);
  1. 参数
    • thread:传出参数,线程创建成功,会将线程ID写入到这个指针指向的内存中
    • attr:线程属性,一般使用默认属性,写NULL
    • start_routine:函数指针,子线程的处理动作
    • arg:作为实参传递到start_routine指向的函数内部
  2. 返回值:线程创建成功返回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
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

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

int main(){
int pRet = 0;
pthread_t tid;
for(int i = 0; i < 5; i++){
printf("主线程中i = %d\n", i);
}
printf("father_pthread_id:%ld\n", pthread_self());
pRet = pthread_create(&tid, NULL, myPrintf, NULL);
printf("son_pthread_id:%ld\n", tid);
sleep(1); // 延迟一秒(CPU挂起)
return 0;
}

子线程被创建出来之后需要抢cpu时间片,抢不到就不能运行,如果主线程退出了,虚拟地址空间就被释放了,子线程就一并被销毁了。但是如果某一个子线程退出了,主线程仍在运行,虚拟地址空间依旧存在。在没有人为干预的情况下,虚拟地址空间的生命周期和主线程是一样的,与子线程无关。

线程退出

调用该函数,当前线程马上退出,并且不会影响到其他线程的正常运行,不管是子线程还是主线程中都可以使用。

1
void pthread_exit(void *retval)

参数:

  • 线程退出的时候携带的数据,当前子线程的主线程会得到该数据,如果不需要指定为NULL即可。

线程回收

子线程退出的时候,其内核资源由主线程进行回收(非内核资源例如栈,代码区资源会在子线程退出的时候,自动释放)。

1
int pthread_join(pthread_t thread, void **retval);
  1. 阻塞函数
    • 如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。
  2. 参数:
    • thread:要回收的子线程的线程ID
    • retval:二级指针,指向一级指针的地址,是一个传出参数,这个地址中存储了pthread_exit()传递出的数据,如果不需要这个参数,可以指定为NULL
  3. 返回值:
    • 线程回收成功返回0,回收失败返回错误码
  4. 用法:
    • 在子线程退出的时候使用pthread_exit(void* retval)的参数将数据传出,在回收这个子线程的时候通过pthread_join(pthread_t thread, 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
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

struct Person
{
int old;
char* name;
};


void* callback(void *arg){
printf("子线程id:%ld\n", pthread_self());
for(int i = 0; i < 5; i++){
printf("子线程中i = %d\n", i);
}
struct Person* p = (struct Person*)arg;
p -> old = 10;
p -> name = "zhang";
pthread_exit(arg);
return NULL;
}

int main(){
for(int i = 0; i < 5; i++){
printf("主线程中i = %d\n", i);
}
printf("father_pthread_id:%ld\n", pthread_self());

int pRet = 0;
pthread_t tid;
struct Person p1; // 主线程栈
void* ptr = NULL; // 传回参数
pRet = pthread_create(&tid, NULL, callback, &p1);
printf("son_pthread_id:%ld\n", tid);
pthread_join(tid, &ptr);
// 因为ptr是一个void*类型,如果不想这么写,也可以通过类型转换,将ptr转成Person指针
printf("name:%s\n", p1.name);
printf("old:%d\n", p1.old);
return 0;
}

线程分离

线程分离之后,子线程退出的时候,其占用的内核资源就会被系统的其他进程接管并回收。优势在于,不需要在主线程进行阻塞,等待子线程结束,由主线程回收子线程资源。搭配pthread_exit()函数,可以实现主线程退出,子线程依然有其他线程进行资源的回收。
在某些情况下,程序中的主线程也有自己的业务流程,如果想让主线程负责子线程的资源回收,调用pthread_join()函数,子要子线程不退出,主线程就会一直被阻塞,主线程的任务也不能推进。

1
int pthread_detach(pthread_t thread);

参数:
- thread:要分离的子线程ID

示例代码:

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>

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

int main()
{
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("i = %d\n", i);
}
// 设置子线程和主线程分离
pthread_detach(tid);
// 让主线程自己退出即可
pthread_exit(NULL);
return 0;
}

线程取消

在某些情况下,在一个线程中杀死另一个线程:
step1:在线程A中调用pthread_cancel(pthreadB_ID)函数,指定杀死线程B,这个时候线程B并不会直接被杀死
step2:在线程B中若执行一次系统调用(从用户区切换到内核区),线程B自动被杀死,否则线程B可以一直运行下去

1
int pthread_cancel(pthread_t thread);
  1. 参数:要杀死的线程的线程ID
  2. 返回值:函数调用成功返回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
#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("我是子线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf(" child i: %d\n", i);
}
return NULL;
}

int main()
{
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("i = %d\n", i);
}
// 杀死子线程, 如果子线程中做系统调用, 子线程就结束了
pthread_cancel(tid);
// 让主线程自己退出即可
pthread_exit(NULL);
return 0;
}

线程ID比较

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

1
int pthread_equal(pthread_t t1, pthread_t t2);
  1. 参数:t1和t2是要比较的线程的线程ID
  2. 返回值:如果两个线程ID相等返回非0值,否则返回0

多线程基础
http://example.com/2025/02/22/OperatingSystem/多线程基础/
作者
ZhangHangming
发布于
2025年2月22日
许可协议