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

目录

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(多线程)线程同步+条件变量+生产者消费模型。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue项目中内嵌iframe,打包上线时候iframe地址如何写?

vue项目中内嵌iframe&#xff0c;打包上线时候iframe地址如何写 一、项目结构1.内嵌的iframe文件位置2.打包后的iframe的位置 二、代码 前提描述&#xff0c;项目是用webpack打包的&#xff0c;内嵌一个完整的js小组件 一、项目结构 1.内嵌的iframe文件位置 2.打包后的iframe的…

Pytorch代码入门学习之分类任务(三):定义损失函数与优化器

一、定义损失函数 1.1 代码 criterion nn.CrossEntropyLoss() 1.2 损失函数简介 神经网络的学习通过某个指标表示目前的状态&#xff0c;然后以这个指标为基准&#xff0c;寻找最优的权重参数。神经网络以某个指标为线索寻找最优权重参数&#xff0c;该指标称为损失函数&am…

【开发篇】一、处理函数:定时器与定时服务

文章目录 1、基本处理函数2、定时器和定时服务3、KeyedProcessFunction下演示定时器4、process重获取当前watermark 前面API篇完结&#xff0c;对数据的转换、聚合、窗口等&#xff0c;都是基于DataStream的&#xff0c;称DataStreamAPI&#xff0c;如图&#xff1a; 在Flink…

宏电5G RedCap工业智能网关获首个中国移动5G物联网开放实验室5G及轻量化产品能力认证

10月21日&#xff0c;2023世界物联网博览会——中国移动物联网开发者大会暨物联网产业论坛在无锡圆满举行。宏电股份参与中国移动5G物联网开放实验室5G及轻量化产品能力认证成果授牌仪式&#xff0c;并获得认证证书。 此次认证主要对产品功能、产品性能、RedCap网络兼容性进行测…

DJYROS产品:基于DJYOS的国产自主割草机器人解决方案

基于都江堰泛计算操作系统的国产自主机器人操作系统即将发布…… 1、都江堰机器人操作系统命名&#xff1a;DJYROS 2、机器人算法&#xff1a;联合行业自主机器人厂家&#xff0c;构建机器人算法库。 3、机器人芯片&#xff1a;联合行业机器人AI芯片公司&#xff0c;构建专用…

Windows Server 2019 搭建FTP站点

目录 1.添加IIS及FTP服务角色 2.创建FTP账户&#xff08;用户名和密码&#xff09;和组 3.设置共享文件夹的权限 4.添加及设置FTP站点 5.配置FTP防火墙支持 6.配置安全组策略 7.客户端测试 踩过的坑说明&#xff1a; 1.添加IIS及FTP服务角色 a.选择【开始】→【服务器…

中文编程开发语言工具编程实际案例:台球棋牌混合计时计费软件使用的编程构件说明

中文编程开发语言工具编程实际案例&#xff1a;台球棋牌混合计时计费软件使用的编程构件说明 上图说明&#xff1a;该软件可以用于桌球和棋牌同时计时计费&#xff0c;在没有开台的时候&#xff0c;图片是处于等待状态&#xff0c;这使用编程工具中的固定图像构件&#xff0c;在…

leetcode做题笔记203. 移除链表元素

给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,6,3,4,5,6], val 6 输出&#xff1a;[1,2,3,4,5]示例 2&#xff1a; 输入&#xff1a…

css 两栏布局的实现

目录 前言 1. 浮动布局 用法 代码示例 理解 2. Flex布局 用法 代码示例 理解 3. Grid布局 用法 代码示例 理解 高质量的设计 前言 两栏布局是一种常见的网页设计模式&#xff0c;它将页面分为两个主要区域&#xff1a;主内容区域和侧边栏。这种布局方式不仅能够提…

睿趣科技:抖音小店申请流程

随着移动互联网的发展&#xff0c;越来越多的人开始尝试通过开设网店来创业。抖音作为国内最受欢迎的短视频平台之一&#xff0c;也推出了自己的电商功能——抖音小店。那么&#xff0c;如何申请抖音小店呢?下面就为大家详细介绍一下抖音小店的申请流程。 首先&#xff0c;打开…

Vue3:将表格数据下载为excel文件

需求 将表格数据或者其他形式的数据下载为excel文件 技术栈 Vue3、ElementPlus、 实现 1、安装相关的库 下载xlsx 和 file-saver 库 npm install -S file-saver npm install -S xlsx引入XLSX库和FileSaver库 import XLSX from xlsx; import FileSaver from file-saver;…

linux音频-IIS音频接口

IIS 总线 IIS(Integrate Interface of Sound)即集成音频接口&#xff0c;在上个世纪 80 年代首先被 Philips 公司用于消费产品的音频设备&#xff0c; I2S规范 I2S总线只能用来处理audio data&#xff0c;而别的信号比如控制信号&#xff0c;编码信号则交给别的模块处理。为了…

iOS调试技巧——使用Python 自定义LLDB

一、类介绍 在使用Python 自定义LLDB之前&#xff0c;先了解一下LLDB的一些类型 SBTarget 正在被调试的程序SBProcess 和程序关联的具体的进程SBThread 执行的线程SBFrame 和线程关联的一个栈帧SBVariable 变量&#xff0c;寄存器或是一个表达式 一般情况下&#xff0c;我们…

【从0到1设计一个网关】自研网关的架构搭建

文章目录 项目骨架搭建领域模型与DDD核心上下文模型封装静态配置的加载组件生命周期源码地址: 源码地址 项目骨架搭建 这里我使用的IDE工具是IDEA。 从上文中我们了解到,我们的项目大概有五个模块,Client,Common,Register Center,Config Center,Core这五个模块。 下面…

Linux进程终止

文章目录 进程退出场景进程退出码strerrorerrno浅谈进程异常exit && _exit 进程退出场景 代码运行完毕&#xff0c;结果正确代码运行完毕&#xff0c;结果不正确代码异常 进程退出码 我们写的C/C的代码&#xff0c;main函数每次都需要返回0&#xff0c;而这个return…

比Nginx测试桩更方便,ShenYu网关的Mock插件

有时候为了方便测试&#xff0c;我们需要模拟 HTTP 外部接口的返回结果。通常情况下&#xff0c;我们可以使用 Nginx 测试桩来实现这个目的。然而&#xff0c;Nginx 的使用门槛较高&#xff0c;可能对一些初级开发和测试人员来说有一定的难度。相比之下&#xff0c;Apache Shen…

深度学习_6_实战_点集最优直线解_代码解析

问题描述&#xff1a; 上述题目的意思为&#xff0c;人工造出一些数据点&#xff0c;对我们的模型y Xw b ∈进行训练&#xff0c;其中标准模型如下&#xff1a; 其中W和X都为张量&#xff0c;我们训练的模型越接近题目给出的标准模型越好 训练过程如下&#xff1a; 人造数…

SCSS动态生成类

前言 在项目开发中&#xff0c;为了方便样式的复用和规范化&#xff0c;通常都会统一一些公共的样式类&#xff0c;如果用传统的css来写就会显得很臃肿。 最近看了看接手的项目的公共css文件&#xff0c;发现很多重复的样式声明&#xff0c;还有全局的样式使用不统一问题。 例…

80.每日一练:移除元素(力扣)

问题描述 代码解决以及思想 解法一 class Solution { public:int removeElement(vector<int>& nums, int val) {int len 0; // 初始化一个用于记录非目标值个数的变量// 创建一个迭代器 it&#xff0c;指向 nums 的开头vector<int>::iterator it nums.beg…

Ubuntu22.04 交叉编译阿里oss c-sdk

一、交叉编译openssl Ubuntu20.04 交叉编译openssl 1.0.1f_编译前去除 makefile 中所有的"-m64"字段_qq76211822的博客-CSDN博客文章浏览阅读319次。Ubuntu20.04 交叉编译openssl_编译前去除 makefile 中所有的"-m64"字段https://blog.csdn.net/sz7621182…