少女祈祷中...

线程

线程的概念:在进程内部运行, 是CPU调度的基本单位

进程(Process):程序的一次运行实例,拥有自己独立的内存空间(代码段、数据段、堆、栈等)。

线程(Thread):是进程中的一个执行流,多个线程共享同一个进程的内存空间,但每个线程有自己的寄存器程序计数器

在 Linux 内核中,并没有“专门的线程”这一概念,线程其实就是一种特殊的进程。

Linux 使用 轻量级进程(LWP,Lightweight Process) 来实现线程:

  • LWP 是一种进程,只是和别的进程共享了某些资源(如地址空间、文件描述符)。
  • 在内核里,线程和进程都是用 task_struct(任务控制块)描述的。

也就是说,从内核角度看,线程和进程几乎没有区别,区别只是资源是否共享。

线程的调度成分相比于进程更低, 如何理解

1
2
3
4
5
6
7
8
9
10
11
线程的调度开销相比进程更低,主要因为:

线程之间共享内存,不需要保存和恢复大量的内存数据。

线程切换时只涉及局部的上下文(如寄存器和栈),不像进程那样需要切换完整的内存空间。

线程的调度粒度更细,操作系统可以更高效地进行调度。

线程的资源共享减少了上下文切换的成本。

简而言之,由于线程的设计更加轻量,资源共享性强,所以在调度时的开销相对进程更低。

线程封装

极简封装

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
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <functional>

namespace ThreadMoudle {

// using func_t = std::functional<void()>;
typedef void(*func_t)(const std::string &name); // 函数指针
class Mythread {
public:
void Excute() {
_isrunning = true;
_func(_name);

}
public:
Mythread(const std::string& name, func_t func):_name(name), _func(func) {

}
static void *ThreadRoutine(void* args) {
Mythread *self = static_cast<Mythread*>(args);
self->Excute();
return nullptr;
}
bool Start() {
int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);
if (n != 0) {
return false;
}
return true;
}
void Stop() {
if (_isrunning) {
_isrunning = false;
pthread_cancel(_tid);
}
}
std::string Status() {
if (_isrunning) return "running";
else return "sleep";
}
void Join() {
if (!_isrunning) {
pthread_join(_tid, nullptr);
}
}
~Mythread() {
}
private:
std::string _name;
pthread_t _tid;
bool _isrunning;
func_t _func; // 线程要执行的回调函数
};
}

线程互斥

为什么需要互斥?

在多线程编程中,多个线程可能会同时访问和修改共享资源(比如全局变量、链表、文件、socket 等)。
如果没有控制访问顺序,就会出现 竞态条件(race condition),导致数据错误或程序行为不可预测

观察下面模拟抢票过程

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
#include "mythread.hpp"
#include <vector>
using namespace ThreadMoudle;
int cnt = 0;

int tickets = 10000;

void route(const std::string& name) {
while (tickets > 0) {
if (tickets > 0) {
usleep(1000);
cnt++;
tickets--;
}
}
}

int main() {
Mythread t1("thread-1", route);
Mythread t2("thread-2", route);
Mythread t3("thread-3", route);
Mythread t4("thread-4", route);
t1.Start();
t2.Start();
t3.Start();
t4.Start();

t1.Join();
t2.Join();
t3.Join();
t4.Join();
std::cout << cnt << std::endl;


return 0;
}

注意到cnt和tickets不相等!

竞态条件(Race Condition):这是问题的核心。虽然 tickets 是共享资源,但在多线程环境下,每个线程的操作并不是原子性的。具体来说,tickets-- 包含了以下几步:

  • 读取 tickets 当前值
  • 减少 tickets
  • 写回新的 tickets

但是,在并发执行时,多个线程可能同时读取到相同的 tickets 值。比如:

  • 线程1读取到 tickets = 100,准备执行 tickets--
  • 线程2也读取到 tickets = 100,也准备执行 tickets--

然后,线程1和线程2分别更新 tickets,最终导致 tickets 被减去 2,而不是 1,甚至可能在 tickets == 0 时还继续减少,导致 tickets 变成负数。

缺少同步机制:为了避免上述竞态条件,需要引入同步机制,如 互斥锁(mutex)原子操作,确保在某一时刻只有一个线程能够修改 tickets 的值。

如何解决呢?

互斥锁(Mutex,Mutual Exclusion Lock)是一种用于同步多线程程序访问共享资源的机制。在并发编程中,多个线程同时访问共享资源可能会引起数据竞态和不一致的问题。为了解决这个问题,互斥锁确保在某一时刻只有一个线程可以访问某个资源,从而避免冲突。

互斥锁的工作原理

  1. 锁定资源:线程在访问共享资源之前,需要先对资源加锁。加锁会阻塞其他线程,直到该线程释放锁。
  2. 释放锁:当线程完成对共享资源的操作后,需要释放锁,允许其他线程获得资源的访问权限。
  3. 死锁问题:如果两个或多个线程相互等待对方释放锁,就会发生死锁,程序无法继续执行。因此,设计良好的锁机制应该避免死锁。

加锁处理

1
2
3
4
5
6
7
8
9
10
11
12
13
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;

void route(const std::string& name) {
while (tickets > 0) {
pthread_mutex_lock(&gmutex);
if (tickets > 0) {
usleep(1000);
cnt++;
tickets--;
}
pthread_mutex_unlock(&gmutex);
}
}

这样就不会出现cnt > tickets的情况