零基础Linux_23(多线程)线程安全+线程互斥(加锁)+死锁

news2025/5/25 4:01:19

目录

1. 线程安全

1.1 线程不安全前期

1.2 线程不安全原因

2. 线程互斥

2.1 加锁保护(代码)

2.2 锁的本质

3. 可重入对比线程安全

4. 死锁

4.1 死锁的必要条件

4.2 避免死锁

5. 笔试面试题

答案及解析

本篇完。


1. 线程安全

基于上一篇线程控制,这里创建个linux_23文件,在里面写代码,先看一段模拟抢票的代码:

Makefile:

mythread:mythread.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f mythread

mythread.cc:(创建了三个新线程抢票)

#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <thread>
#include <unistd.h>
#include <pthread.h>
using namespace std;

// 如果多线程访问同一个全局变量,并对它进行数据计算,多线程会互相影响吗
int tickets = 10000; // 在并发访问的时候,导致了我们数据不一致的问题

void *getTickets(void *args)
{
    (void)args;
    while(true)
    {
        if(tickets > 0)
        {
            usleep(1000);
            printf("%p: %d\n", pthread_self(), tickets);
            tickets--;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_t t1,t2,t3,t4;
    // 多线程抢票的逻辑
    pthread_create(&t1, nullptr, getTickets, (void*)"user1");
    pthread_create(&t2, nullptr, getTickets, (void*)"user2");
    pthread_create(&t3, nullptr, getTickets, (void*)"user3");
    pthread_create(&t4, nullptr, getTickets, (void*)"user4");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
}

编译运行:

运行以后,发现出现了负数票,这不合理,票抢完就应该停止了,包括我们的代码逻辑都是这样写的,但是此时就出现了这种情况。

  • 上面现象的原因是发生了线程不安全问题

为什么产生了线程不安全现象:

上面现象故意弄出来的,涉及到了线程调度,利用了线程调度的特性造出了一个这样的现象。要想出现上面的现象,就需要尽可能让多个线程交叉执行。多个线程交叉执行的本质:就是让调度器尽可能的频繁发生线程调度与切换。

虽然看起来是多个线程在同时运行,但这是由于CPU运行速度太快导致的,实际上,CPU是一个线程一个线程执行的。现在就是要让CPU频繁调度,不停的切换线程,一个线程还没有执行完就再执行下一个,每个线程都执行一点,这样交叉执行。

当一个线程进行延时的时候,CPU并不会等它,而是会将它放在等待队列里,然后去执行另一个线程,等延时线程醒来以后才会接着执行。

线程在时间片到来,更高优先级线程到来,线程等待的时候会发生线程切换。

线程是在从内核态转换成用户态的时候检测是否达到线程切换的条件的。

线程检测是否切换是以内核态的身份去检测的,执行的是3~4G内核空间中的代码,本质上是操作系统在检测。


1.1 线程不安全前期

假设tickets已经只剩一张了,即全局变量tickets = 1。

主线程创建好4个新线程以后,4个新线程便开始执行了,在执行到延时的时候,新线程就会被放在等待队列里。看CPU及内核:

if(tickets > 0)判断的本质逻辑: 从内存中读取数据到CPU寄存器 ->  进行判断。

在线程user1执行到if判断时,CPU从内存中将tickets变量中的数据1拿到了CPU的寄存器ebx中。

CPU进行判断后,发现符合大于0的条件。

当线程user1符合条件继续向下执行延时代码时,CPU将线程user1切走了,换上了user2。

在线程user1被切走的时候,它的上下文数据也会被切走。

所以ebx寄存器中的1也会跟着user1的PCB被切走。

user2被调度时仍然重复user1的过程,执行延时被切走,再换上user3,以此类推,直到user4被切走。四个线程都拿到了tickets=1,所以符合条件,都能向下执行。

当user4被挂起后,user1差不多就该醒来了。user1唤醒以后接着被切走的位置继续执行:

执行tickets - - 的本质:

  • 从内存中读取数据到CPU的寄存器
  • 更改数据
  • 写回数据到内存中

虽然C/C++代码只有一条语句,但是汇编后至少有3条语句。

user1执行tickets–以后,抢票成功了,并且将抢票后的tickets=0写回到了内存中。

此时user2醒来了,同样接着它被切走的位置继续执行,此时user2回来后认为tickets=1,所以就向下执行了:

当执行tickets减减时,仍然需要三步:

  • 从内存中读取tickets=0到CPU寄存器ebx中。
  • 修改值,从0变成-1。
  • 将-1写回内存中。

当user2执行完后,user3和user4醒来同样继续向下执行,重复上面的过程,仍然对tickets减一,所以导致结果不合理。


1.2 线程不安全原因

只存在两个线程,对全局变量tickets仅作减减操作:

线程A先被CPU调度,进行减减操作。

  • 从内存中将tickets=1000取到寄存器ebx中。
  • 进行减减操作,tickets变成了999。
  • 在执行第三步写回数据之前,线程A被切走了。

线程A切走的同时,它的上下文,也就是tickets=999也被切走了。

线程B此时被调度,线程A在等待队列。

  • 线程B先从内存中读取tickets = 1000到寄存器ebx中。
  • 进行减减操作。
  • 将减减后的值写回到内存中。
  • 线程B将减减操作完整的执行了很多遍,直到tickets=200时才被切下去。

线程B被切走以后,线程A又接着被调度。

线程A接着被切走的位置开始执行,也就是执行减减的第三步操作壹壹写回。

  • 线程A被调度后,先恢复上下文,将被切走时的tickets=999恢复到了ebx寄存器中。
  • 然后执行第三步,将tickets=999写回到了内存中。

线程B辛辛苦苦将tickets从1000减到了200,线程A重新被调度后,直接将tickets又从200写回到了999。上面这种现象被叫做数据不一致问题

  • 导致数据不一致问题的原因:共享资源没有被保护,多线程对该资源进行了交叉访问。

而解决数据不一致问题的办法就是对共享资源加锁。


2. 线程互斥

看看几个基本概念:

临界资源:多个执行流进行安全访问的共享资源。

上面现象中的tickets很显然就不是临界资源,因为多线程对它的访问并不安全,存在数据不一致问题。

临界区:多个执行流中,访问临界资源的代码。

假设上面例子中的是临界资源,那么每个线程都存在一部分临界区,就是对tickets进行判断,打印,减减部分的代码。多个线程中的这部分代码属于临界区。

线程互斥:让多个线程串行访问共享资源,任何时候只有一个执行流在访问共享资源。

上面例子中如果多个线程能够串行访问tickets,而不是交叉访问,也不会产生数据不一致问题。而让共享资源变成临界资源就是为了实现互斥,也就是让多个线程串行访问原本的共享资源。

原子性:对一个资源进行访问的时候,要么不做,要么就做完。

在C/C++中的减减和加加操作,看似是一句代码,但是对应着三条汇编指令,上面例子中,线程A在执行第三步之前被切走了,导致减减操作没有完成,这种行为就不具有原子性,因为对共享资源的操作没有做完。

对一个资源进行操作,如果只用一条汇编就能完成,那么就具有原子性,反之就不具有原子性。(这是当前的一种理解,这种理解只能算原子性中的一个子集,是为了方便表述。)


2.1 加锁保护(代码)

要想解决多线程的数据不一致问题,就需要做到以下几点:

  • 代码必须要有互斥行为,当一个线程进入临界区执行代码时,不允许其他线程进入该临界区
  • 如果有多个线程同时请求执行临界区代码,并且临界区没有线程在执行代码,那么只允许一个线程进入该临界区。
  • 如果线程不在临界区中执行代码,那么该线程不能阻止其他线程进入临界区。

要做到上面三点,只需要一把锁就可以,持有锁的线程才能进入临界区中执行代码,并且其他线程无法进入该临界区。

锁:就是互斥量,也叫互斥锁。

加锁可以让共享资源临界资源化,从而保护共享资源的安全,让多个线程串行访问共享资源。

锁相关的系统调用:

pthread_mutex_t lock; // 定义一把锁

和创建线程一样,锁也需要创建,POSIX提供了锁的变量类型,如上面代码所示,其中mutext是互斥量的意思。

初始化锁:man pthread_mutex_init:

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
  • 形参1:创建的互斥锁指针
  • 形参2:锁的属性,一般情况下设为nullptr
  • 返回值:初始化成功返回0,失败返回错误码
  • 作用:将创建的锁初始化。

销毁锁:

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 形参:创建的互斥锁指针
  • 返回值:销毁成功返回0,失败返回错误码
  • 作用:当锁使用完后,必须进行销毁

全局或者静态锁初始化:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

如果锁是全局的或者被static修饰的静态锁,只需要使用上面语句初始化锁即可。

加锁:man pthread_mutex_lock:

int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 形参:创建的互斥锁指针
  • 返回值:加锁成功返回0,失败返回错误码
  • 作用:给临界区加锁,让多线程串行访问临界资源

解锁:

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 形参:创建的互斥锁指针
  • 返回值:解锁成功返回0,失败返回错误码
  • 作用:解锁,让多线程恢复并发执行

锁其实起一个区间划分的作用,在加锁和解锁之间的代码就是临界区,多个执行流只能串行执行临界区代码,从而保护公共资源,使之成为临界资源。

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(lock);
//临界区
//...
pthread_mutex_unlock(lock);

加锁和解锁两句代码圈定了临界区的范围。

现在将抢票代码加上锁,看看是否还会出现多线程数据不一致问题:

Makefile:

mythread:mythread.cc
	g++ -o $@ $^ -g -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f mythread

mythread.cc:

在主线程中创建一个互斥锁,并且初始化,在所有新线程等待成功后将锁释放。

但是此时的锁是存在于主线程的栈结构中,需要让所有新线程看到这把锁。(创建成全局就不用)

在线程数据类中再增加一个锁指针,此时所有线程就都能看到这把锁了。

#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <cassert>
#include <thread>
#include <unistd.h>
#include <pthread.h>
using namespace std;

int tickets = 10000; // 在并发访问的时候,导致了我们数据不一致的问题 -> 临界资源

#define THREAD_NUM 10

class ThreadData
{
public:
    ThreadData(const std::string &n,pthread_mutex_t *pm):tname(n), pmtx(pm)
    {}
public:
    std::string tname;
    pthread_mutex_t *pmtx;
};

void *getTickets(void *args)
{
    ThreadData *td = (ThreadData*)args;
    while(true) // 抢票逻辑
    {
        int n = pthread_mutex_lock(td->pmtx); // 加锁
        assert(n == 0);
        // 临界区
        if(tickets > 0) // 判断的本质也是计算的一种
        {
            usleep(rand()%1500);
            printf("%s: %d\n", td->tname.c_str(), tickets);
            tickets--; // 也可能出现问题
            n = pthread_mutex_unlock(td->pmtx); // 解锁
            assert(n == 0);
        }
        else
        {
            n = pthread_mutex_unlock(td->pmtx);  // break之前解锁
            assert(n == 0);
            break;
        }
        
        usleep(rand()%2000); // 抢完票,其实还需要后续的动作
    }
    delete td;
    return nullptr;
}

int main()
{
    time_t start = time(nullptr);
    pthread_mutex_t mtx;
    pthread_mutex_init(&mtx, nullptr);

    pthread_t t[THREAD_NUM];

    for(int i = 0; i < THREAD_NUM; i++) // 多线程抢票的逻辑
    {
        std::string name = "thread ";
        name += std::to_string(i+1);
        ThreadData *td = new ThreadData(name, &mtx);
        pthread_create(t + i, nullptr, getTickets, (void*)td);
    }

    for(int i = 0; i < THREAD_NUM; i++)
    {
        pthread_join(t[i], nullptr);
    }

    pthread_mutex_destroy(&mtx);
    return 0;
}

此时抢票的结果是正常了,最终抢到1结束,符合我们的预期。

但发现抢票的速度比以前慢了好多。

因为加锁和解锁的过程是多个线程串行执行的,并且临界区的代码也是串行执行的,所以速度就变慢了。

需要注意的是

  • 当一个线程从临界区中出来并且释放锁后,执行后续任务时,其他线程才有更大几率去竞争锁。
  • 加锁时,一定要保证临界区的粒度非常小。将那些不是必须放在临界区中的代码放在临界区外。
  • 加锁是程序员行为,要加锁就所有线程都加锁,否则就起不到保护共享资源的效果。

2.2 锁的本质

如何看待锁?

在上面代码中,一个锁必须让所有线程都看到,所以锁本身就是一个共享资源。

既然是共享资源,锁也必须是安全的,那么是谁来保证锁的安全性呢?

锁是通过加锁和解锁是原子的来保证自身的安全的。

一个线程,如果申请成功锁,那么它就会继续向下执行,如果暂时申请不成功呢?

此时就被阻塞住了,线程和进程都是存在的。

  • 一个锁只能被申请一次,只有锁被释放后才能再次申请。

当一个线程申请锁暂时失败以后,就会阻塞不动。

  • 当一个线程申请锁成功,进入临界区访问临界资源,其他线程要想进入临界区只能阻塞等待,等锁释放。
  • 当一个线程申请锁成功,进入临界区访问临界资源,同样是能被切走的,而且该线程是抱着锁走的,其他线程仍然无法申请锁成功。

操作系统内部并不存在锁的概念,所以调度器在调度轻量级进程的时候并不会考虑是否有锁。

所以站在其他线程的角度,锁只有两种状态:

  • 申请锁前
  • 申请锁后

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

加锁解锁的原理:

经过上面的例子,我们认识到一个事实,c/c++中加加和减减的操作并不是原子的,所以会导致多线程数据不一致的问题。

而为了能让加锁过程是原子的,在大多数体系结构了,都提供了swap或者xchange汇编指令,通过一条汇编指令来保证加锁的原子性。

加锁解锁的伪汇编代码:

lock:
	movb %al, $0
	xchange %al, mutex
	if(al寄存器的内容 > 0)
	{
		return 0;
	}
	else
	{
		挂起等待;	
	}
	goto lock;

unlock:
	movb mutex, $1
	唤醒等待mutex的线程;
	return 0;

加锁过程中,xchange是原子的,可以保证锁的安全。

锁只能被一个线程持有,而且由于xchange汇编只有一条指令,即使申请锁的过程被切走也不怕。

一旦一个线程通过xchage拿到了锁,即使它被切走,也是拿着锁走的,其他线程是无法拿到锁的,只有等它将锁释放。

只有持有锁的线程才能执行下去,锁相当于一张入场卷。

这样来看,释放锁的过程其实对原子性的要求并没有那么高,因为释放锁的线程必定是持有锁的线程,不持有锁的线程都不会执行到这里,都在阻塞等待。


3. 可重入对比线程安全

重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。

之前在信号部分就提到过重入,进程在执行一个函数,收到某个信号在处理信号时又调用了这个函数。今天在多线程这里,理解重入更加容易,我们以前写的多线程代码都是可重入的。

可重入和不可重入:一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

常见可重入情况:

  •  不使用全局变量或静态变量。
  •  不使用用malloc或者new开辟出的空间。
  •  不返回静态或全局数据,所有数据都有函数的调用者提供。

常见可重入情况:

  •  不使用全局变量或静态变量。
  •  不使用用malloc或者new开辟出的空间。
  •  不返回静态或全局数据,所有数据都有函数的调用者提供。

总的来说,一个函数中如果使用了全局数据,或者静态数据,以及堆区上的数据,就是不可重入的,反之就是可重入的。

线程安全:

多个线程并发同一段代码时,不会出现不同的结果(数据不一致)。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

互斥锁就是让不安全的线程变安全,也就是前面我们所学习的内容。

常见线程安全情况:

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
  • 类或者接口对于线程来说都是原子操作。
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性。

多线程共同执行的代码段中,如果有全局变量或者静态变量没有被保护,那么就是线程不安全的。

常见线程不安全情况:

  • 不保护共享变量的函数。
  • 函数状态随着被调用,状态发生变化的函数。
  • 返回指向静态变量指针的函数。

可重入与线程安全的联系:

多线程是通过调用函数来实现的,所以线程安全和重入就存在一些联系:

  • 函数是可重入的,那就是线程安全的,因为没有全局或者静态变量,不会产生数据不一致问题。
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。出发对不可重入函数的全局变量进行保护。
  • 如果一个函数中有全局变量并且没有保护,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全的区别:

可重入和线程安全是不同的两个东西,但是又存在一定的交集。

  • 可重入说的是函数。
  • 线程安全说的是线程。

可重入函数是线程安全函数的一种,因为不存在全局或者静态变量。

线程安全不一定是可重入的,而可重入函数则一定是线程安全的。因为线程安全的情况可能是对全局变量进行了保护(加了锁)。

由于线程可以加锁,所以说线程安全的情况比可重入要多。


4. 死锁

我们前面例子中写的都是只有一把锁的情况,在实际使用中有可能会存在多把锁,此时就可能造成死锁。

死锁:一组执行流中的各个执行流均占有不会释放的锁资源,但因互相申请被其他进程所站用不会释放的锁资源而处于的一种永久等待状态。

通俗来说就是一个线程自己持有锁,并且不会释放,但是还要申请其他线程的锁,此时就容易造成死锁。

一把锁也是会有死锁的情况的,连续申请俩次就是死锁。

在上面演示一个线程暂时申请锁失败而阻塞时,就是死锁。


4.1 死锁的必要条件

死锁的四个必要条件:

① 互斥

这一点不用说,只要用到锁就会互斥。

② 请求与保持

请求就是指一个执行流申请其他锁,保持是指不释放自己已经持有的锁。

③ 不剥夺

一个执行流已经持有锁,在不主动释放前不能强行剥夺。

④ 环路等待

线程A,B,C都持有一把锁,并且不释放。

  • 线程A 申请 线程B持有的锁B
  • 线程B 申请 线程C持有的锁C
  • 线程C 申请 线程A持有的锁A

此时就构成了环路阻塞等待。

只有符合上面四个条件就会造成死锁。而要破坏死锁只要破坏其中一个条件即可。


4.2 避免死锁

① 上面四个必要条件中的第一个无法破坏,因为我们使用的就是锁,锁就具有互斥的性质。只能破坏其他三个条件。

② 加锁顺序一致

这是为了避免形参环路等待,只要不构成环路即可。

③ 避免锁位释放的场景

④ 避免锁位释放的场景

临界资源尽量一次性分配好,不要分布在太多的地方加锁,这样的话导致死锁的概率就会增加。

解决死锁的基本方法如下:

预防死锁、避免死锁、检测死锁、解除死锁。

解决死锁的常用策略如下:

鸵鸟策略 对可能出现的问题采取无视态度,前提是出现概率很低

预防策略 破坏死锁产生的必要条件

避免策略 银行家算法,分配资源前进行风险判断,避免风险的发生

检测与解除死锁 分配资源时不采取措施,但是必须提供死锁的检测与解除手段

可以避免(预防)死锁的算法(了解):

  • 死锁检测算法
  • 银行家算法(避免策略)

银行家算法的思想在于将系统运行分为两种状态:安全/非安全,有可能出现风险的都属于非安全。

银行家算法的思想是为了避免出现“环路等待”条件


5. 笔试面试题

1. 以下描述正确的有:

A.可以使用ps -l命令查看轻量级进程信息

B.可以使用ps -L命令查看轻量级进程信息

C.可以使用pthread_self接口获取轻量级进程ID

D.可以使用getpid接口获取轻量级进程ID

2. 以下描述正确的有:[多选]

A.pthread_create函数是一个库函数, 代码当中如果使用该函数创建线程, 则需要在编译的时候链接“libpthread.so”线程库

B.那个线程调用pthread_exit函数, 那个线程就退出。俗称“谁调用谁退出”

C.在有多个线程的情况下,主线程调用pthread_cancel(pthread_self()), 则主线程状态为Z, 其他线程正常运行

D.在有多个线程的情况下,主线程从main函数的return返回或者调用pthread_exit函数,则整个进程退出

3. 下列不属于POSIX互斥锁相关函数的是:()

A.int pthread_mutex_destroy(pthread_mutex_t* mutex)

B.int pthread_mutex_lock(pthread_mutex_t* mutex)

C.int pthread_mutex_trylock(pthread_mutex_t* mutex)

D.int pthread_mutex_create(pthread_mutex_t* mutex)

4. 进程A、B共享变量x,需要互斥执行;

    进程B、C共享变量y,B、C也需要互斥执行,

    因此进程A、C必须互斥执行

A.错

B.对

5. 设两个进程共用一个临界资源的互斥信号量mutex,当mutex=1时表示()。

A.一个进程进入了临界区,另一个进程等待

B.没有一个进程进入临界区

C.两个进程都进入临界区

D.两个进程都在等待

6. 在一段时间内,只允许一个进程访问的资源被称为()

A.共享资源

B.临界区

C.临界资源

D.共享区

7. 简述轻量级进程ID与进程ID之间的区别

8. 简述LWP与pthread_create创建的线程之间的关系

9. 简述什么是LWP

10. 简述什么是线程互斥,为什么需要互斥


答案及解析

1. B

A错误,

B正确 ps命令用于查看进程信息,其中-L选项用于查看轻量级进程信息

C错误 pthread_self() 用于获取用户态线程的tid,而并非轻量级进程ID

D错误 getpid() 用于获取当前进程的id,而并非某个特定轻量级进程

2. ABC

C:主线程调用pthread_cancel(pthread_self())函数来退出自己, 则主线程对应的轻量级进程状态变更成为Z, 其他线程不受影响,这是正确的(正常情况下我们也不会这么做....)

D:主线程调用pthread_exit只是退出主线程,并不会导致进程的退出

3. D

A pthread_mutex_destroy 用于销毁互斥锁

B pthread_mutex_lock 用于加锁保护临界区

C pthread_mutex_trylock 用户非阻塞加锁

D 没有这个函数 pthread_create是线程创建函数,而互斥锁并没有对应的创建函数,而是直接定义pthread_mutex_t类型的互斥锁变量

4. A

进程A操作的x,C并不进行操作;进程C操作的y,进程A并不操作;因此A和C并不需要互斥执行

5. B

mutex简单理解就是一个0/1的计数器,用于标记资源访问状态:

0表示已经有执行流加锁成功,资源处于不可访问,

1表示未加锁,资源可访问。

因此选择B选项,表示没有执行流完成加锁对资源进行访问,资源处于可访问状态。

6. C

A 共享资源表示能够被多个执行流同时访问的资源

B 对临界资源进行操作的代码段被称作临界区

C 临界资源表示同一时间只能有一个执行流访问的共享资源

D 没有这个专业说法,非要简单理解就是可以共同执行的代码片段

题目为选择同一时间只有一个进程能访问的资源,则就是临界资源,因此选择C选项

7. 简述轻量级进程ID与进程ID之间的区别:

因为Linux下的轻量级进程是一个pcb,每个轻量级进程都有一个自己的轻量级进程ID(pcb中的pid),而同一个程序中的轻量级进程组成线程组,拥有一个共同的线程组ID

8. 简述LWP与pthread_create创建的线程之间的关系:

pthread_create是一个库函数,功能是在用户态创建一个用户线程,而这个线程的运行调度是基于一个轻量级进程实现的

9. 简述什么是LWP

LWP是轻量级进程,在Linux下进程是资源分配的基本单位,线程是cpu调度的基本单位,而线程使用进程pcb描述实现,并且同一个进程中的所有pcb共用同一个虚拟地址空间,因此相较于传统进程更加的轻量化

10. 简述什么是线程互斥,为什么需要互斥:

线程互斥指的是在多个线程间对临界资源进行争抢访问时有可能会造成数据二义,因此通过保证同一时间只有一个线程能够访问临界资源的方式实现线程对临界资源的访问安全性

本篇完。

下一篇:零基础Linux_24(多线程)线程同步+条件变量+生产者消费模型。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1137065.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机网络(谢希仁)第八版课后题答案(第三章)

1.数据链路(即逻辑链路)与链路(即物理链路)有何区别? “电路接通了”与”数据链路接通了”的区别何在? 数据链路与链路的区别在于数据链路出链路外&#xff0c;还必须有一些必要的规程来控制数据的传输&#xff0c;因此&#xff0c;数据链路比链路多了实现通信规程所需要的硬…

​​​​​​​如何解决Google play开发者新注册账号,身份验证的地址证明问题?

我们知道&#xff0c;Google Play应用市场的发展速度惊人&#xff0c;但这两年&#xff0c;为了防止恶意软件的传播&#xff0c;谷歌要求开发者账号需要进行身份验证才能发布应用。 而今年越来越严格&#xff0c;不仅在提审时需要进行电话验证&#xff08;链接&#xff09;&am…

TCP三次握手具体过程

四次挥手 1&#xff09;客户端进程发出连接释放报文&#xff0c;并且停止发送数据。释放数据报文首部&#xff0c;FIN1&#xff0c;其序列号为sequ&#xff08;等于前已经传送过来的数据的最后一个字节的序号加1)&#xff0c;此时&#xff0c;客户端进入FIN_WAIT_1&#xff08…

Echarts柱状图渐变色问题变通

问题背景 设计稿中给出了如下图的效果&#xff0c;在柱状图的最上面给出了一个白色的小块&#xff0c;起初我一直在思考亦或者搜索相关的问题&#xff1a;如何在Echarts柱状图顶部实现一个24*4的白色矩形块。始终不得其解&#xff0c;在一个吃饭的瞬间冒出来一个想法是否可以用…

图像的特征点描述与提取

一、说明 特征点算法是图像处理中主要算法之一&#xff0c;它是图像物体匹配的关键步骤&#xff0c;因此&#xff0c;是个极其重要的题目&#xff0c;至今依旧研究不断&#xff0c;本篇讲述历年来学者在领域研究的突出贡献&#xff0c;即六种不同的特征点提取办法&#xff0c;供…

Python------学生管理(文件txt处理)

项目&#xff1a;Python实现学生管理系统 注&#xff1a;免费源码下载 项目介绍&#xff1a; 功能描述&#xff1a;&#xff08;1&#xff09;添加学生信息&#xff08;2&#xff09;删除学生信息&#xff08;3&#xff09;修改学生信息&#xff08;4&#xff09;查询学生信息…

JVM(Java Virtual Machine)G1收集器篇

前言 本文参考《深入理解Java虚拟机》&#xff0c;本文主要介绍G1收集器的收集思想和具体过程&#xff08;填上一篇文章留下的坑&#xff09; 本系列其他文章链接&#xff1a; JVM&#xff08;Java Virtual Machine&#xff09;内存模型篇 JVM&#xff08;Java Virtual Machi…

绿米Aqara S1【妙控开关 S1E】的破解方法---续篇

概述 上接第一篇《绿米Aqara S1【妙控开关 S1E】的破解方法》。 链接地址如下: 绿米Aqara S1【妙控开关 S1E】的破解方法-CSDN博客 上篇主要讲述了,绿米S1E这款硬件的基本组成及TTL线的线序,并获取到了对应的串口打印信息。 此篇重点讲解,如何进入其系统,并开启访问权…

【经验分享】在WSL中使用USB设备

具体步骤&#xff1a; 首先在windows中安装 USBIP 工具&#xff0c;在GitHub上下载安装包并根据README文档的说明进行操作&#xff1a; 下载链接&#xff1a;https://github.com/dorssel/usbipd-win/releases 同时在 WSL Linux 端也需要安装编译内核所需的库和工具&#xff0c…

【微信小程序】实现投票功能(附源码)

一、Vant Weapp介绍 Vant Weapp 是一个基于微信小程序的组件库&#xff0c;它提供了丰富的 UI 组件和交互功能&#xff0c;能够帮助开发者快速构建出现代化的小程序应用。Vant Weapp 的设计理念注重简洁、易用和高效&#xff0c;同时提供灵活的定制化选项&#xff0c;以满足开发…

电动汽车租赁平台【EV Mobility】申请875万美元纳斯达克IPO上市

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 猛兽财经获悉&#xff0c;总部位于美国的电动汽车租赁平台【EV Mobility】近期已向美国证券交易委员会&#xff08;SEC&#xff09;提交招股书&#xff0c;申请在纳斯达克IPO上市&#xff0c;股票代码为(EVMO) &#xff0c…

举个栗子!Quick BI 技巧(3):创建趋势分析表

上一期举个栗子为数据粉们分享了如何简单几步创建柱线图&#xff0c;有一些数据想了解如何在 Quick BI 中制作趋势分析表。 趋势分析表由趋势分析图和趋势分析明细表组成&#xff0c;可以通过趋势分析图查看指标的宏观趋势&#xff0c;通过趋势分析表查看指标详情&#xff0c;…

macOS Sonoma 14.1正式版(23B74)发布(可下载黑白苹果镜像)

系统介绍 黑果魏叔苹果今天为 macOS Sonoma 推出了 14.1 版本更新&#xff0c;魏叔发现&#xff0c;本更新主要改善了 Apple Music 界面&#xff0c;设置中新增保修状态&#xff0c;并修复了多项错误内容。 根据苹果的新说明&#xff0c;这次的 Mac 更新不仅提供了一系列的改善…

博睿数据亮相GOPS全球运维大会上海站!

10月26日&#xff0c;博睿数据携核心产品新一代一体化智能可观测平台 Bonree ONE 亮相第二十一届 GOPS 全球运维大会上海站&#xff0c;展示博睿数据在智能运维领域的科技实力与创新成果。同时&#xff0c;博睿数据AIOps首席专家兼产品总监贺安辉在AIOps最佳实践及解决方案专场…

User CSS 在性能优化方面的实践

目录 前言 1. 减少重绘和回流 1.1 用法 1.2 代码示例 1.3 理解 2. 使用CSS精灵 2.1 用法 2.2 代码示例 2.3 理解 3. 压缩CSS文件 3.1 用法 3.2 代码示例 3.3 理解 4. 使用媒体查询进行响应式设计 4.1 用法 4.2 代码示例 4.3 理解 5. 使用CSS预处理器和构建工…

物联网AI MicroPython传感器学习 之 MDL0025心率传感器

学物联网&#xff0c;来万物简单IoT物联网&#xff01;&#xff01; 一、产品简介 PulseSensor&#xff08;MDL0025&#xff09; 是一款用于脉搏心率测量的光电反射式模拟传感器。将其佩戴于手指或耳垂等处&#xff0c;通过导线连接可将采集到的模拟信号传输给HaaS开发版用来转…

[java进阶]——HashMap的底层实现原理和源码分析,另附几个高频面试题

&#x1f308;键盘敲烂&#xff0c;年薪30万&#x1f308; 目录 一、底层数据结构 二、底层原理及源码分析 2.1 继承关系 2.2 成员变量 2.3 构造方法 2.4 重要的成员方法 2.4.1 put()方法 三、高频面试题 一、底层数据结构 JDK8以后底层使用 数组链表红黑树的数据结构&am…

贝锐花生壳内网穿透推出全新功能,远程业务连接更安全

贝锐旗下内网穿透兼动态域名解析品牌花生壳目前推出了全新的“访问控制”功能&#xff0c;可精确设置访问权限&#xff0c;充分保障信息安全&#xff0c;满足更多用户安全远程访问内网服务的需求。 通过这一功能&#xff0c;可实现指定时间、IP、地区等条件下才能远程访问映射的…

【C++】继承 ⑬ ( 虚继承原理 | 虚继承解决继承二义性问题 | 二义性产生的原因分析 )

文章目录 一、虚继承原理1、虚继承解决继承二义性问题2、二义性产生的原因分析3、虚继承原理 二、代码示例 - 虚继承原理1、完整代码示例2、执行结果 一、虚继承原理 1、虚继承解决继承二义性问题 继承的二义性 : 如果 一个 子类 ( 派生类 ) 继承多个 父类 ( 基类 ) , 这些父类…

【【萌新的FPGA学习之FIFO的介绍】】

萌新的FPGA学习之FIFO的介绍 FIFO first in first out FIFO 的作用更多的是 缓冲与缓存 或者FIFO 也常被用来使用为 FIFO 本质上是由 RAM 加读写控制逻辑构成的一种先进先出的数据缓冲器&#xff0c;其与普通存储器 RAM 的 区别在于 FIFO 没有外部读写地址线&#xff0c;使用起…