欢迎来到Cefler的博客😁
🕌博客主页:折纸花满衣
🏠个人专栏:题目解析
🌎推荐文章:【LeetCode】winter vacation training
目录
- 👉🏻线程封装
- Thread.cpp
- 👉🏻线程互斥
- 多个线程操作共享变量带来的问题
- 👉🏻 互斥量(mutex)的接口函数
- pthread_mutex_init
- pthread_mutex_lock函数
- pthread_mutex_unlock函数
- pthread_mutex_destory函数
- pthread_mutex_trylock函数
- 线程互斥访问共享变量代码示例
- 👉🏻关于互斥的一些总结与小问题
👉🏻线程封装
Thread.cpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
// 设计方的视角
//typedef std::function<void()> func_t;
template<class T>
using func_t = std::function<void(T)>;
template<class T>
class Thread
{
public:
Thread(const std::string &threadname, func_t<T> func, T data)
:_tid(0), _threadname(threadname), _isrunning(false), _func(func), _data(data)
{}
static void *ThreadRoutine(void *args) // 类内方法,
{
// (void)args; // 仅仅是为了防止编译器有告警
Thread *ts = static_cast<Thread *>(args);
ts->_func(ts->_data);
return nullptr;
}
bool Start()//线程创建
{
int n = pthread_create(&_tid, nullptr, ThreadRoutine, this/*?*/);
if(n == 0)
{
_isrunning = true;
return true;
}
else return false;
}
bool Join()//线程等待
{
if(!_isrunning) return true;
int n = pthread_join(_tid, nullptr);
if(n == 0)
{
_isrunning = false;
return true;
}
return false;
}
std::string ThreadName()
{
return _threadname;
}
bool IsRunning()
{
return _isrunning;
}
~Thread()
{}
private:
pthread_t _tid;
std::string _threadname;
bool _isrunning;
func_t<T> _func;
T _data;
};
👉🏻线程互斥
🌈线程互斥基本概念
线程互斥是指在多线程编程中,为了避免多个线程同时访问共享资源而导致数据不一致的情况,需要采取措施来保证同一时间只有一个线程可以访问共享资源。这种机制可以通过使用互(mutex)来实现。
当一个线程要访问共享资源时,它首先尝试获取互斥量的锁。如果这个锁已经被其他线程占用,那么当前线程就会被阻塞,直到锁被释放为止。一旦线程成功获取了锁,它就可以安全地访问共享资源,并在完成操作后释放锁,以便其他线程可以继续访问这个资源。
通过使用线程互斥机制,可以有效地避免多个线程之间发生竞争条件(race condition),从而确保数据的一致性和程序的正确性。
🌈以下是一些关于线程互斥相关的名词介绍:
1.临界资源
:多线程执行流共享的资源就叫做临界资源
2.临界区
:每个线程内部,访问临界资源的代码,就叫做临界区
3.互斥
:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
4.原子性
:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
多个线程操作共享变量带来的问题
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void *route(void *arg)
{
char *id = (char*)arg;
while ( 1 ) {
if ( ticket > 0 ) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
} else {
break;
}
}
}
int main( void )
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
}
//一次执行结果:
thread 4 sells ticket:100
...
thread 4 sells ticket:1
thread 2 sells ticket:0
thread 1 sells ticket:-1
thread 3 sells ticket:-2
这里对共享变量ticket进行减减的操作属于非原子操作,而非原子操作的指令与只有一条指令的原子操作不同的是,非原子操作的指令有三条:
- load :将共享变量ticket从内存加载到寄存器中
- update : 更新寄存器里面的值,执行-1操作
- store :将新值,从寄存器写回共享变量ticket的内存地址
这里的ticket减到-1,主要问题是多个线程同时进行了if ( ticket > 0 ) 的判断,并进入了语句中,而数据在内存中,本质是被线程共享的,数据被读取到寄存器中,本质变成了线程的上下文,属于线程私有数据!。
所以为什么可以减减到-1,按理来说只有ticket>0的情况下才可以进入语句执行减减操作,但是因为每个线程的上下文数据都是独立的,而进行判断的数据是从CPU的寄存器中读取的,此时每个同时进来的线程在进来前存储在CPU上寄存器上的数据,也即是自己的上下文中的ticket值都是为1,也就是>0,所以才会判断合法。
那么如何解决这种线程挤占共享资源的情况呢,这里我们就要引入互斥的一些接口函数了。
👉🏻 互斥量(mutex)的接口函数
pthread_mutex_init
pthread_mutex_init
函数是 POSIX 线程库中用于初始化互斥锁(mutex)的函数。互斥锁是一种线程同步机制,用于保护临界区(critical section)代码,防止多个线程同时访问共享资源而导致的竞争条件(race condition)。调用 pthread_mutex_init
函数可以对互斥锁进行初始化,设置其属性等。
这个函数的原型如下:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
mutex
参数是指向要初始化的互斥锁的指针。attr
参数是一个指向互斥锁属性的指针,通常可以设置为NULL
,表示使用默认的属性。
成功初始化互斥锁后,可以使用 pthread_mutex_lock
和 pthread_mutex_unlock
来分别加锁和解锁互斥锁。
需要注意的是,在使用完互斥锁后,应该使用 pthread_mutex_destroy
函数来销毁互斥锁以释放资源。
pthread_mutex_lock函数
函数原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数解释:
- mutex:指向要获取锁的互斥量的指针。互斥量是一种用于线程同步的对象,通过对互斥量加锁和解锁来控制线程对共享资源的访问。
使用方法总结:
- 首先,定义并初始化一个互斥量变量
pthread_mutex_t mutex;
。 - 在需要对共享资源进行保护的临界区内,使用
pthread_mutex_lock(&mutex);
来获取互斥量的锁。 - 在临界区内执行对共享资源的操作。
- 最后,使用
pthread_mutex_unlock(&mutex);
来释放互斥量的锁,允许其他线程访问共享资源。
pthread_mutex_unlock函数
函数原型:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数解释:
- mutex:指向要释放锁的互斥量的指针。该参数是一个 pthread_mutex_t 类型的指针,表示需要释放锁的互斥量。
使用方法:
- 在临界区内使用
pthread_mutex_unlock(&mutex);
来释放互斥量的锁。这样做可以让其他线程获取该互斥量的锁,继续访问共享资源。 - 通常情况下,
pthread_mutex_unlock
应该与pthread_mutex_lock
配对使用,以确保正确的互斥访问共享资源。
pthread_mutex_destory函数
函数原型:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数解释:
- mutex:指向要销毁的互斥量的指针。该参数是一个 pthread_mutex_t 类型的指针,表示需要销毁的互斥量。
使用方法:
- 在不再需要使用互斥量时,可以调用
pthread_mutex_destroy(&mutex);
来销毁互斥量。 - 在销毁互斥量之前,确保所有线程已经停止使用该互斥量。
pthread_mutex_trylock函数
函数原型:
int pthread_mutex_trylock(pthread_mutex_t *mutex);
参数解释:
- mutex:指向要尝试获取锁的互斥量的指针。互斥量是一种用于线程同步的对象,通过对互斥量加锁和解锁来控制线程对共享资源的访问。
使用方法:
pthread_mutex_trylock
函数尝试获取互斥量的锁,如果互斥量当前未被其他线程占用,则获取锁成功并返回 0;如果互斥量已经被其他线程占用,则立即返回一个非零值。- 通过检查
pthread_mutex_trylock
的返回值来确定是否成功获取了互斥量的锁。 - 相较于
pthread_mutex_lock
函数,pthread_mutex_trylock
是非阻塞的,不会使线程进入等待状态,而是立即返回结果。
线程互斥访问共享变量代码示例
下面是一个简单的示例代码,演示了如何使用 pthread_mutex_init、pthread_mutex_lock、pthread_mutex_unlock 和 pthread_mutex_destroy 来实现线程互斥访问共享变量的情况:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_THREADS 2
#define MAX_COUNT 10000
int shared_variable = 0;
pthread_mutex_t mutex;
void* thread_function(void* arg) {
for (int i = 0; i < MAX_COUNT; i++) {
pthread_mutex_lock(&mutex);
shared_variable++;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
pthread_mutex_init(&mutex, NULL);
for (int i = 0; i < NUM_THREADS; i++) {
pthread_create(&threads[i], NULL, thread_function, NULL);
}
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
pthread_mutex_destroy(&mutex);
printf("Final value of shared_variable: %d\n", shared_variable);
return 0;
}
在这个示例中,我们定义了一个共享变量 shared_variable
,并通过两个线程对其进行累加操作。在每次对共享变量进行操作之前,线程会先获取互斥量的锁,操作完成后释放锁。这样确保了只有一个线程可以访问临界资源,避免了竞争条件的发生。
当程序执行完毕后,会输出最终的 shared_variable
的值。由于两个线程对其进行递增操作,最终的结果应该是 MAX_COUNT * NUM_THREADS
,即 20000。
👉🏻关于互斥的一些总结与小问题
🌕加锁:
1.我们要尽可能的给少的代码块加锁,会导致效率变慢
加锁会导致效率变慢的原因主要有两个方面:
- 线程阻塞和切换:当一个线程获得了锁,其他试图获取锁的线程会被阻塞,直到锁被释放。在多个线程同时竞争一个锁的情况下,会导致线程频繁地进入阻塞状态和切换上下文,这会带来较大的开销。此外,线程在阻塞和唤醒过程中的切换可能会导致缓存失效,影响程序的性能。
- 串行执行:在使用锁的情况下,只有一个线程可以访问临界区,其他线程需要等待锁的释放。这意味着多个线程无法并行地执行对共享资源的操作,而是被强制按顺序进行。这种串行化的执行方式会降低程序的并发性和并行度,从而影响整体的执行效率。
虽然加锁会带来一定的性能开销,但是在多线程环境下确保数据的一致性和避免竞态条件是至关重要的。因此,在设计并发程序时,需要权衡锁的使用,避免不必要的锁竞争和锁粒度过大的问题,以最大限度地提高程序的性能和并发性。
🍉线程切换
线程切换是指在多线程环境下,操作系统将 CPU 的执行权从一个线程转移到另一个线程的过程。当一个线程无法继续执行(例如被阻塞
、主动让出 CPU
或时间片用完
),操作系统会进行线程切换以确保其他线程能够得到执行机会。
线程切换通常包括以下几个步骤:
(1). 保存上下文:操作系统会保存当前线程的上下文信息,包括寄存器的值、程序计数器、堆栈指针等。这样做是为了在将来重新执行该线程时能够从切换前的状态继续执行。
(2). 选择新线程:操作系统会选择一个新的就绪线程,并将 CPU 的执行权分配给它。选择的方式可以基于调度算法,如先来先服务、轮转法、优先级调度等。
(3). 恢复上下文:操作系统会恢复所选线程的上下文信息,将寄存器的值、程序计数器、堆栈指针等设置为该线程切换前保存的值。
(4). 执行新线程:CPU 开始执行新线程的指令,从上一次线程切换的位置或者新线程的起始位置开始执行。
线程切换是操作系统实现并发的重要手段之一。它使得多个线程能够共享 CPU 的执行时间,实现并发执行。然而,线程切换也会带来一定的开销,包括保存和恢复上下文的开销、缓存失效等。因此,在设计高效的多线程应用程序时,需要尽量减少线程切换的次数,提高 CPU 利用率和系统性能。
2.一般加锁,都是给临界区(共享资源)加锁
3.根据互斥的定义,任何时刻,只允许一个线程申请锁成功,多个线程申请锁失败,失败的线程怎么办?
答:失败的线程会在mutex 上进行阻塞,本质就是等待
4.一个线程在访问临界资源的时候,可不可能发生线程切换?
答:当然可以;只是不能同时访问临界区,而不是不允许切换,切换也没有用,因为被上锁了,不能对临界资源进行操作
如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长