操作系统C++实践:深入理解多线程编程

多线程

多线程是什么?怎么使用?多线程和多进程有什么区别?本篇就作一些简单介绍带你入门

写在前面

本篇仅作简单介绍,内容适合入门级别,可通过目录查看。重点在于熟悉每一个经典使用模式

文中相关api就不做过多介绍,不然字数就太多了,可自行去api手册了解

语言不保证严谨,内容也不保证完全,欢迎指正错误、补充

如果你已经看过我之前的多进程博客(并且能适应我的破烂语言组织),这篇文章会让你更容易理解。多线程和多进程在很多概念上是相通的,但也有本质区别。

一、多线程基础

一、进程 vs 线程(简单区分)

其实大多数人心中对这两个有一个模糊的对比,那么简单来讲就是:一个工厂(进程)里面有多个工人(线程),工人共享工厂资源,协作完成一个任务。

二、多线程基础

  • 多线程也是实现多任务并发的一种
  • 进程是资源分配的基本单位,线程是任务器进程进行任务调度的最小单位
  • 一个进程可以拥有多个线程,同一个进程中多个线程共享进程资源
  • 因为共享进程资源,多线程的主要问题是竞态,会发生资源抢占问题,也每多进程安全
  • 多线程执行顺序是按时间片轮转的
  • 一个进程中的线程占用越多,任务器分配给该进程的时间片就越多

三、多线程相关基础api

  • C库没有提供多线程相关操作,对于多线程编程,要依赖第三方库
  • 头文件:#include <pthread.h>
  • 编译时也要加上 -lpthread-pthread 链接上这个库
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);

// 线程资源回收
int pthread_join(pthread_t thread, void **retval);

// 线程分离态:分离态的线程退出后,系统自动回收资源
int pthread_detach(pthread_t, thread);

// 线程退出
void pthread_exit(void *retval);

// 获取线程ID
pthread_t pthread_self(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
#include <stdio.h>
#include <pthread.h>

void *task(void *arg)
{
printf("分支线程, tid = %#x\n", pthread_self());

sleep(3);

pthread_exit(NULL);// 线程退出
}

int main(int argc, const char *argv[])
{
pthread_t tid = -1;
// 创建分支线程
if(pthread_create(&tid, NULL, &task, NULL) != 0)
{
printf("pthread_create error\n");
return -1;
}
printf("这里是主线程, tid = %#x\n", tid);

// 阻塞回收分支线程资源
// pthread_join(tid, NULL);

// 将线程设置成分离态
pthread_detach(tid);

sleep(5);

return 0;
}

二、线程的同步互斥

  • 由于同一个进程的多个线程会共享进程的资源,这些被共享的资源称为临界资源
  • 多个线程抢占进程资源的现象称为竞态
  • 类似于解决多进程共享内存问题使用了信号量集的同步功能,多线程的竞态问题也可以用同步控制来解决

一、互斥锁

  • 像名字一样,是一个锁,某个线程拥有这个锁后就能“锁住”自己正在访问的临界资源,其他线程打不开这个锁,也就访问不了那一片的临界资源了。
  • 互斥锁的本质也是一个特殊的临界资源,当该临界资源被某个线程所拥有后,其他线程就不能拥有该资源(同一时刻,一个互斥锁只 能被一个线程所拥有)

互斥锁相关api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1、创建一个互斥锁:只需定义一个pthread_mutex_t 类型的变量即创建了一个互斥锁
pthread_mutex_t mutex;

2、初始化互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //静态初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr); // 动态初始化

3、获取锁资源
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:获取锁资源,如果要获取的互斥锁已经被其他线程锁定,那么该函数会阻塞,直到能够获取锁资源

4、释放锁资源,释放对锁的拥有权
int pthread_mutex_unlock(pthread_mutex_t *mutex);

5、销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 经典使用模式
  • 可以把互斥锁的操作去掉/加上,分别运行程序,做一个对比
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <stdio.h>
#include <pthread.h>

// 创建一个互斥锁
pthread_mutex_t mutex;

// 全局变量
int num = 1000;

void *task1(void *arg)
{
while(1)
{
// 可以把互斥锁的操作去掉/加上,分别运行程序,做一个对比
sleep(1);
// 获取锁资源
pthread_mutex_lock(&mutex);

num -= 10;
printf("线程1拿走了10, 剩余%d\n", num);

// 释放锁资源
pthread_mutex_unlock(&mutex);
}
}

void *task2(void *arg)
{
while(1)
{
sleep(1);
// 获取锁资源
pthread_mutex_lock(&mutex);

num -= 20;
printf("线程2拿走了20, 剩余%d\n", num);

// 释放锁资源
pthread_mutex_unlock(&mutex);
}
}

int main(int argc, const char *argv[])
{
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);

// 创建两个分支线程
pthread_t tid1, tid2;
if(pthread_create(&tid1, NULL, &task1, NULL) != 0)
{
printf("pthread_create tid1 error\n");
return -1;
}
if(pthread_create(&tid2, NULL, &task2, NULL) != 0)
{
printf("pthread_create tid2 error\n");
return -1;
}

printf("主线程:tid1 = %#x, tid2 = %#x\n", tid1, tid2);

// 阻塞回收分支线程资源
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);

// 摧毁互斥锁
pthread_mutex_destroy(&mutex);

printf("主线程:所有线程都执行完毕\n");

return 0;
}

二、无名信号量

  • 本质上也是一个特殊的临界资源,内部维护了一个value值,当某个进行想要执行之前,先申请该无名信号量的value资源,如果value值大于0,则申请资源函数接触阻塞,继续执行后续操作。如果value值为0,则当前申请资源函数会处于阻塞状态,直到其他线程将该value值增加到大于0。
  • 常用于生产者消费者模型

无名信号量相关api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <semaphore.h>
1、创建无名信号量:只需定义一个sem_t 类型的变量即可
sem_t sem;
2、初始化无名信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);

3、申请无名信号量的资源(P操作)
int sem_wait(sem_t *sem);

4、释放无名信号量的资源(V操作)
int sem_post(sem_t *sem);

5、销毁无名信号量
int sem_destroy(sem_t *sem);
  • 经典使用模式
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

// 创建无名信号量
sem_t sem;

// 生产者线程
void *task1(void *arg)
{
int num = 5;
while(num--)
{
// 生产者一般情况下无需申请,除非有原料供应的阶段

sleep(1); // 一秒生产一个
printf("生产了一个雪糕\n");

// 释放无名信号量资源
sem_post(&sem);
}
// 退出线程
pthread_exit(NULL);
}

// 消费者线程
void *task2(void *arg)
{
int num = 5;
while(num--)
{
// 申请无名信号量资源
sem_wait(&sem);

printf("消费了一个雪糕,好吃\n");

// 消费者一般情况下无需释放,只管消费就行了
}
// 退出线程
pthread_exit(NULL);
}

int main(int argc, const char *argv[])
{
// 初始化无名信号量
// 第二个参数0表示用于进程间通信
// 第三个参数0表示初始值为0
sem_init(&sem, 0, 0);

// 1、创建两个分支线程
pthread_t tid1, tid2;
if(pthread_create(&tid1, NULL, task1, NULL) != 0)
{
printf("tid1 create error\n");
return -1;
}
if(pthread_create(&tid2, NULL, task2, NULL) != 0)
{
printf("tid2 create error\n");
return -1;
}

printf("主线程:tid1 = %#x, tid2 = %#x\n", tid1, tid2);

// 2、阻塞等待线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);

// 55、销毁无名信号量
sem_destroy(&sem);
}

三、条件变量

  • 条件变量本质上也是一个临界资源,他维护了一个队列,当消费者线程想要执行时,先进入队列中 等待生产者的唤醒。执行完生产者,再由生产者唤醒在队列中的消费者,这样就完成了生产者和消费者 之间的同步关系。
  • 多个消费者进入队列中也是互斥的,有竞态,为了保证顺序,消费者需要互斥锁来进行互斥操作

条件变量相关api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1、创建一个条件变量,只需定义一个pthread_cond_t类型的全局变量即可
pthread_cond_t cond;

2、初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 静态初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); // 动态初始化

3、消费者线程进入等待队列
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
功能:将线程放入休眠等待队列,等待其他线程的唤醒

4、生产者线程唤醒休眠队列中的任务
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:唤醒条件变量维护的队列中的所有消费者线程

int pthread_cond_signal(pthread_cond_t *cond);
功能:唤醒条件变量维护的队列中的第一个进入队列的消费者线程

5、销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);

  • 经典使用模式
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <stdio.h>
#include <pthread.h>

// 创建一个条件变量
pthread_cond_t cond;
// 创建一个互斥锁
pthread_mutex_t mutex;

// 生产者线程体函数
void *task_c(void *arg)
{
int num = 9;
while(num--)
{
sleep(1);
printf("%#x:生产了一个雪糕,快来吃\n");

// 1.唤醒一个消费者
pthread_cond_signal(&cond);
}
// 2.唤醒所有消费者
// pthread_cond_broadcast(&cond);
// 1和2选一种方法

pthread_exit(NULL);
}

void *task(void *arg)
{
// 解析传入的参数
int i = *(int *)arg;

// 申请锁资源
pthread_mutex_lock(&mutex);

// 进入休眠队列,开始阻塞,等待生产者唤醒
pthread_cond_wait(&cond, &mutex);
printf("%#x:消费了一个雪糕,好吃\n");

// 释放锁资源
pthread_mutex_unlock(&mutex);

// 退出线程
pthread_exit(NULL);
}

int main(int argc, const char *argv[])
{
// 初始化条件变量
pthread_cond_init(&cond, NULL);
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);

// 创建生产者线程
pthread_t tid;
if(pthread_create(&tid, NULL, task_c, NULL) != 0)
{
printf("tid create error\n");
return -1;
}

// 创建几个分支线程用于消费
pthread_t tids[5];
for(int i = 0; i < 5; i++)
{
if(pthread_create(&tids[i], NULL, task, &i) != 0)
{
printf("tid%d create error\n", i);
return -1;
}
}

// 阻塞等待线程结束
pthread_join(tid, NULL);
for(int i = 0; i < 5; i++)
{
pthread_join(tid[i], NULL);
}

// 销毁条件变量
pthread_cond_destroy(&cond);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);

return 0;
}