💻文章目录
- 📄前言
- 🌺linux线程基础
- 线程的概念
- 线程的优缺点
- 线程与进程的区别
- 线程的创建
- 🌻linux线程冲突
- 概念
- 互斥锁函数介绍
- 加锁的缺点
- 📓总结
📄前言
无论你是否为程序员,相信多线程这个词汇应该都有所耳闻,像是在某个优化很差的游戏中听闻这游戏甚至是单线程的,如果你对多线程感兴趣,不妨点进本文来学习多线程编程,即使没有深厚的C/C++编程基础,你也能到本文学习到如何编写多线程程序。
🌺linux线程基础
线程的概念
线程指的是系统中的执行路径,每个线程都线程系统中的一切进程都至少有一个线程,它们共享同一个进程.
其实在linux中,实际并没有真正的线程,线程通常被称为轻量级进程(LWP),这是因为在linux的实现中,线程和进程并没有什么本质的区别,只是线程被设计得更加轻量,以便更高效实现并发执行。
线程pcbtask_struc -+ +-------------------++--------+ | | 内核映射区域 || | | +-------------------++--------+ | | 栈 || +-------------------+task_struc | | |+--------+ | | 共享库 || | | | |+--------+ | +-------------------+| | 堆 |task_struc | 指向同一地址空间 +-------------------++--------+ |-------------------+> | 数据段 || | | +-------------------++--------+ | | 未初始化数据区 || +-------------------+task_struc | | 已初始化数据区 |+--------+ | +-------------------+| | | | 代码段 |+--------+ -+ +-------------------+
线程的优缺点
- 优点:
- 共享资源:在同一线程的线程共享着大部分内存空间,如:代码段、数据段、文件描述符、堆、共享内存区等。这使得线程间通信非常地高效,无需IPC机制开销。
- 独立调度:虽然线程中大部分地址空间都与主线程共享,但线程也有自己的一部分数据,如:栈与寄存器状态,这使得他们可以独立于其他线程运行。
- 响应性:在多线程程序中,一个进程的阻塞不会影响到其他进程。
- 资源利用率:多线程可以提高在多核处理器上运行的效率,实现并行执行。
- 缺点:
- 编程困难:因为多线程需要考虑到临界区、互斥、同步等问题,所以对程序员的代码能力要求较高。
- 同步复杂性:多线程的资源共享需要谨慎处理,否则会出现数据二义性问题。
- 调试困难: 多线程调试一直都是令人头疼的问题,因为bug可能会难以复现,并且不是所有调试工具都支持多线程调试。
- 健壮性:如果任意一个线程触发了异常,则整个程序都会终止。
线程与进程的区别
- 定义:进程是资源分配的最小单位,线程则是cpu调度执行的最小单位。
- 资源共享:进程之间资源独立,同一进程内的线程共享进程资源。
- 创建开销:线程的创建和切换开销都小于进程,因为线程之间资源共享。
线程的创建
- 函数接口介绍:头文件:<pthread.h>
- 创建线程:
// 创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);// pthread_t 是 POSIX 线程(Pthreads)库中定义的一个数据类型,用于唯一标识一个线程
- 参数
- thread: 线程
- attr: 指定线程属性的指针,可设为NULL
- start_routine:线程开始执行的函数
- arg: start_routine 函数的参数
- 回收线程:
// 等待线程结束并回收线程的资源,防止类似“僵尸进程”的情况
int pthread_join(pthread_t thread, void** retval);
- 参数:
- thread:用于回收的线程id;
- retval:用于存储线程的返回值。
- 退出线程:
// 用于终止当前的线程,因为exit会终止整个进程,所以有了这个函数
void pthread_exit(void* retval);
- 参数:
- retval:退出线程时返回的值
- 分离线程:
// 如果觉得join操作是一种负担的时候,可以使用pthread_detach
// 用于分离线程,当线程结束时,自动回收线程资源。
int pthread_detach(pthread_t thread);
- 参数:
- thread:分离的线程id
介绍完了函数接口,就到实践的时间啦。
- 使用函数
#include <pthread.h>
#include <iostream>void *thread_func(void *arg)
{// 获取当前线程的tidstd::cout << "Thread" << (char *)arg << " id:" << gettid() << " started" << std::endl;int cnt = 10;while (cnt >= 0){std::cout << "Thread" << (char *)arg << " id:" << gettid() << " is running, cnt = " << cnt << std::endl;cnt--;sleep(1);}// 子线程退出pthread_exit(nullptr); // 可有可无
}void *func_test(void* args)
{printf("I LOVE LINUX\n");pthread_detach(pthread_self()); // 使用pthread_self()可以使子线程自己分离。return nullptr;
}int main()
{pthread_t thread, thread2;pthread_create(&thread, nullptr, thread_func, (void *)"-1");pthread_create(&thread2, nullptr, func_test, nullptr);// 主线程等待子线程结束pthread_join(thread, NULL); // 回收线程return 0;
}
🌻linux线程冲突
概念
多线程的高效率也是存在着代价的,当多个线程同时访问一份资源时,就会发生线程冲突(数据二义性),我们一般将这些多个线程都要访问的资源称为临界区。
要探讨数据二义性问题,就得从汇编代码开始讲解
; 例如一个简单的++操作,看似只做了一个操作,但在汇编中却并不是这样。MOV EAX, [x] ; 将x的值加载到EAX寄存器
INC EAX ; 将EAX寄存器的值增加1
MOV [x], EAX ; 将修改后的值存回内存位置x; 多个线程同时访问这个资源(x),当线程1在将x放入寄存器EAX时,线程2可能就已经将x++,并改变了内存的数值
; 线程1将寄存器的值++后,又放回了x的内存。建议使用vs2022 进行反汇编调试来观看现象。
- 线程冲突演示
#include <pthread.h>
#include <iostream>int x = 0;void *func(void *args)
{for (int i = 0; i < 100000000; i++) //数值越大,冲突概率越大++x;pthread_exit(nullptr);
}int main()
{// 线程冲突演示pthread_t pid1, pid2;pthread_create(&pid1, nullptr, func, nullptr);pthread_create(&pid2, nullptr, func, nullptr);pthread_join(pid1, nullptr); //回收线程pthread_join(pid2, nullptr);cout << "x = " << x << endl;return 0;
}
// 结果:
// x = 154698688
解决方案: 为了解决这种情况,就得当线程访问临界区资源时限制为一个线程访问,也就是说,需要给线程加锁。
互斥锁函数介绍
- 创建锁
// 初始化锁 pthread_mutex_t 用于声明互斥量(mutex)对象。// 静态加锁 (全局变量或静态进行初始化)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 动态初始化
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
- 线程加锁
// 给线程加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 参数:
- mutex:指向互斥锁对象
- 互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 参数:
- mutex:指向需要解锁的互斥锁对象的指针。
注意:加锁操作本身时原子性的,所以不用担心锁的二义性。
- 互斥锁的使用:
int x = 0;
// 初始化锁对象
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void *func(void *args)
{for (int i = 0; i < 100000000; i++){pthread_mutex_lock(&mutex); //加锁++x;pthread_mutex_unlock(&mutex); //解锁}pthread_exit(nullptr);
}int main()
{// 线程冲突演示pthread_t pid1, pid2;pthread_create(&pid1, nullptr, func, nullptr);pthread_create(&pid2, nullptr, func, nullptr);pthread_join(pid1, nullptr);pthread_join(pid2, nullptr);cout << "x = " << x << endl;return 0;
}
加锁的缺点
如果我们尝试运行程序,会发现加锁后的运行速度明显慢了不少。锁的使用会增加性能的开销,而且线程可能会变成串行执行,为了避免多余的性能开销,每次使用锁都应该避免将非临界区的资源加锁。
在一些特殊的情况下,可能会
- 死锁演示:
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;void *func(void *args)
{int* cnt = (int*)args;if(*cnt <= 0) return nullptr;pthread_mutex_lock(&mtx); // 第二次递归时等待着线程解锁std::cout << "func()" << std::endl; --(*cnt);func(args); // 递归进入下一层,但锁还没解锁。pthread_mutex_unlock(&mtx); // 程序永远走不到这里。return nullptr;
}int main()
{// 线程冲突演示pthread_t pid;int* cnt = new int(10);pthread_create(&pid, nullptr, func, (void*)cnt);pthread_join(pid, nullptr);return 0;
}
📓总结
多线程编程 | ||
---|---|---|
优点 | 缺点 | |
资源共享 | 线程间共享进程资源(如代码段、数据段、文件描述符等),使得线程间通信非常高效,无需通过IPC机制开销。 | 多线程的资源共享需要通过同步机制(如互斥锁)来管理,否则可能导致数据不一致或竞争条件的问题。 |
独立调度 | 线程可以独立于其他线程运行,拥有自己的执行路径。这增加了应用程序的响应性和处理效率。 | 线程调度引入了上下文切换的开销,尤其是在高度竞争的环境中,可能降低整体性能。 |
效率提升 | 在多核处理器上,多线程能够利用额外的核心执行更多的任务,提高了程序的执行效率和资源利用率。 | 编写高效的多线程程序需要深入理解并发、同步等概念,增加了开发的复杂度。 |
多线程编程是一把双刃剑,使用多线程能够显著提升程序的性能,但它也为程序带来了许多潜在的风险,在处理器核心数越来越多的当今,学习多线程也变得越发重要,希望本文对你的学习有所帮助。
📜博客主页:主页
📫我的专栏:C++
📱我的github:github