Linux——多线程互斥(上)

简介: Linux——多线程互斥(上)

抢票问题

这里用上一篇:

的封装函数。

这里还需要用一个函数:

这里是以微妙做单位进行休眠的。

假设有1000张火车票,一共四个接口在抢,最后我们要看到什么现象呢?

因为多个线程进行交叉执行。

多个线程交叉执行本质:就是让调度器尽可能的频繁发生线程调度与切换。

线程一般在什么时候发生切换?当时间片到了,来了更高优先级的线程,线程等待的时候。

那么线程是什么时候检测上面的问题?是从内核态切换到用户态的时候,线程要对调度状态进行检测,如果可以,就直接发生线程切换。

#include "Thread.hpp"
int tickets = 1000;//票数
void* thread_run(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        if(tickets > 0)
        {
            usleep(1234);//1秒=1000毫秒=1000000微秒
            cout << name << "用户正在抢票:"<< tickets-- <<endl;
        }
        else
            break;
    }
}
int main()
{
    unique_ptr<Thread> thread1(new Thread(thread_run, (void*)"user1",1));
    unique_ptr<Thread> thread2(new Thread(thread_run, (void*)"user2",2));
    unique_ptr<Thread> thread3(new Thread(thread_run, (void*)"user3",3));
    unique_ptr<Thread> thread4(new Thread(thread_run, (void*)"user4",4));
    thread1->join();
    thread2->join();
    thread3->join();
    thread4->join();
    return 0;
}

但是结果竟然出现了0,-1,-2,为什么会发生这种现象呢?

首先:判断的逻辑有两步

1.读取内存数据到CPU的寄存器中。

2.进行判断

因为CPU只有一个,每次线程只能有一个进行判断。

线程1运行的时候,CPU将tickets的数据放进内存其中与线程1的数据进行对比,但是对比结束之后,突然时间片到了,线程切换,线程2也进行了如上步骤,刚对比完又切换了。

如果极端场景,四个进程都在这个时候对比,tickets的数据一直都是1,那么这个时候线程1被唤醒,线程1带着他的上下文回到CPU,CPU处理这段代码,tickets的数据进行- - ,处理完又去处理线程2,线程3,线程4。

这也就导致了出现0,-1,-2的结果。

还有另一种情况。

对一个全局变量进行多线程更改,这个操作也不是安全的。

对于++,- -这两种操作,在C,C++上看起来只有一条语句,其实汇编用了三条语句。

1.从内存中读取数据到CPU寄存器中。

2.在寄存器中让CPU进行对应的逻辑运算。

3.写回新的结果到内存中变量的位置。

假设线程1先将票数减少了333张。

然后CPU本来要将666传给内存中的票数时突然进行了线程切换,到了线程2一看票数还是999。

于是线程2开始继续抢票:

线程2将票数减少到了222,这个时候,又换回了线程1.

这个时候首先恢复的是上下文,然后更新内存中的数据,一下子变成了666,之前变成222等于白做事情了。

总结:我们定义的全局数据在没有保护的时候往往是不安全的,像上面多个线程在交替执行造成的数据安全问题,发生数据不一致。

那么如何解决呢?

互斥锁

锁的接口

之前说过原子性是要么做,要么不做,这里再结合上面抢票问题说一下。

像上面进行++操作,需要三条汇编语言,CPU在执行的时候是一定会将一条汇编语言执行完毕(失败与否不关心),也就是说这会导致有中间状态,是可以被打断的,这就叫做非原子性。

那么原子性其实就是一个对资源进行的操作,如果只用一条汇编能完成,这个就是原子性,反之就不是原子性。

这个时候,对于以上提出问题的解决方案叫做加锁。

锁也有对应的函数:

这把锁的类型是:

第一个函数是释放锁,第二个函数是初始化锁

这里是对全局定义的锁初始化方式。

这里第一个函数是对对应的锁进行加锁。

第三个函数是解锁。

#include "Thread.hpp"
int tickets = 1000;//票数
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//全局锁
void* thread_run(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&lock);//这里进行加锁
        if(tickets > 0)
        {
            usleep(1234);//1秒=1000毫秒=1000000微秒
            cout << name << "用户正在抢票:"<< tickets-- <<endl;
            pthread_mutex_unlock(&lock);//解锁
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
    }
}
int main()
{
    unique_ptr<Thread> thread1(new Thread(thread_run, (void*)"user1",1));
    unique_ptr<Thread> thread2(new Thread(thread_run, (void*)"user2",2));
    unique_ptr<Thread> thread3(new Thread(thread_run, (void*)"user3",3));
    unique_ptr<Thread> thread4(new Thread(thread_run, (void*)"user4",4));
    thread1->join();
    thread2->join();
    thread3->join();
    thread4->join();
    return 0;
}

在运行的过程中,这个速度也变慢了,因为现在的线程是串行执行,所以也没有发生之前奇怪的打印结果。

那么为什么一直都是一个线程抢到票了呢?这是因为锁虽然规定了串行执行,但是并没有去管理线程的竞争,这里第四个线程竞争力最强,所以每次都是线程4抢到票。

这里我们在用一下局部锁,并且解决一下刚才的问题。

#include "Thread.hpp"
int tickets = 1000;//票数
class ThreadData
{
public:
    ThreadData(const string& threadname, pthread_mutex_t *mutex_p):_threadname(threadname),_mutex_p(mutex_p)
    {}
    ~ThreadData()
    {}
public:
    string _threadname;
    pthread_mutex_t *_mutex_p;
};
void* thread_run(void* args)
{
    ThreadData* p = static_cast<ThreadData*>(args);
    while(true)
    {
        pthread_mutex_lock(p->_mutex_p);//加锁
        if(tickets > 0)
        {
            usleep(1234);//1秒=1000毫秒=1000000微秒
            cout << p->_threadname << "用户正在抢票:"<< tickets-- <<endl;
            pthread_mutex_unlock(p->_mutex_p);
        }
        else
        {
            pthread_mutex_unlock(p->_mutex_p);
            break;
        }
        usleep(1234);//模拟抢完票形成一个订单,这里也就等于阻止了竞争力强的线程,让竞争力强的到后面排队去
    }
}
int main()
{
    pthread_mutex_t lock;
    pthread_mutex_init(&lock, nullptr);//初始化锁,第二个参数设为nullptr就可以
    vector<pthread_t> arr(4);
    for(int i = 0;i < 4; i++)
    {
        char buffer[64];
        snprintf(buffer,sizeof(buffer),"thread %d",i+1);
        ThreadData* p = new ThreadData(buffer, &lock);
        pthread_create(&arr[i], nullptr, thread_run, p);
    }
    for(const auto& e:arr)
    {
        pthread_join(e,nullptr);
    }
    pthread_mutex_destroy(&lock);//解锁
    return 0;
}

理解锁

锁的背景概念

临界资源:多线程执行流共享的资源就叫做临界资源。

临界区:每个线程内部,访问临界资源的代码,就叫做临界区。

互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。

原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

如何看待锁:

1.我们在使用锁的时候,锁能被每个线程都看到,所以锁本身就是共享资源。锁是保护资源的,那么锁的安全谁来保护呢?

2.pthread_mutex_lock枷锁的过程中必须是安全的。(其实就是原子的)

3.如果申请成功,就继续向后执行,如果失败执行流就阻塞。

注意,我这里申请了两次加锁。

这里就阻塞了。

4.谁持有锁,谁就进入临界区。

假如线程1持有锁,进入临界资源,其他线程在阻塞,那么在这个过程中线程1是可以被切换的。

这也说明,线程1是和锁一起被切走了。

所以对于其他线程而言,线程1有意义的状态只有两个。

申请锁前

释放锁后

站在其他线程角度,看待当前线程持有锁的过程就是原子的。

未来我们在使用锁的时候,一定要尽量保证临界区的粒度(锁中间保护的代码)非常小。

并且,加锁是程序员的行为,针对某一处公共资源,对于一个线程加锁,其他线程也要想办法加锁。

相关文章
|
2月前
|
算法 Unix Linux
linux线程调度策略
linux线程调度策略
55 0
|
2月前
|
存储 设计模式 NoSQL
Linux线程详解
Linux线程详解
|
2月前
|
缓存 Linux C语言
Linux线程是如何创建的
【8月更文挑战第5天】线程不是一个完全由内核实现的机制,它是由内核态和用户态合作完成的。
|
2月前
|
Linux API 调度
重温Linux内核:互斥和同步
本文全面回顾了Linux内核中的互斥和同步机制,包括中断屏蔽、原子变量、自旋锁、读写锁、顺序锁、信号量、互斥量、RCU机制以及完成量等,提供了它们的定义、实现原理、API用法和使用时的注意事项。
40 0
|
2月前
|
Java C语言 C++
并发编程进阶:线程同步与互斥
并发编程进阶:线程同步与互斥
30 0
|
2月前
|
算法 Java 调度
【多线程面试题二十】、 如何实现互斥锁(mutex)?
这篇文章讨论了在Java中实现互斥锁(mutex)的两种方式:使用`synchronized`关键字进行块结构同步,以及使用`java.util.concurrent.locks.Lock`接口进行非块结构同步,后者提供了更灵活的同步机制和扩展性。
|
2月前
|
负载均衡 Linux 调度
在Linux中,进程和线程有何作用?
在Linux中,进程和线程有何作用?
|
2月前
|
缓存 Linux C语言
Linux中线程是如何创建的
【8月更文挑战第15天】线程并非纯内核机制,由内核态与用户态共同实现。
|
3月前
|
安全 算法 Linux
【Linux】线程安全——补充|互斥、锁|同步、条件变量(下)
【Linux】线程安全——补充|互斥、锁|同步、条件变量(下)
43 0
|
3月前
|
存储 安全 Linux
【Linux】线程安全——补充|互斥、锁|同步、条件变量(上)
【Linux】线程安全——补充|互斥、锁|同步、条件变量(上)
51 0
下一篇
无影云桌面