线程
线程的概念:在进程内部运行, 是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 {
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 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的情况