linux线程互斥

线程

  • 1. 线程分离
    • 1.1 线程分离函数
    • 1.2 线程分离用例
  • 2.线程独立栈
  • 3. Linux线程互斥

1. 线程分离

概念:线程分离是指在一个线程结束时,能够自动释放该线程所占用的系统资源,而不需要其他线程对其进行回收。
线程分离的状态决定一个线程以什么样的方式来终止自己。非分离状态下,一个线程结束后,还有一部分资源没有被回收,所以创建线程者应该调用pthread_join来等待线程运行结束,并可得到线程的退出代码,回收其资源。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

1.1 线程分离函数

pthread_detach函数是一个用于设置线程分离状态的函数,它的原型是:int pthread_detach(pthread_t thread);

它的参数是一个线程标识符,它的返回值是0表示成功,非0表示失败。

线程分离状态决定了一个线程在终止时如何释放其占用的资源。一个可结合的(joinable)线程在终止时不会自动释放资源,需要其他线程调用pthread_join函数来获取其退出状态并回收资源。一个分离的(detached)线程在终止时会自动释放资源,无需其他线程回收。

pthread_detach函数可以将一个可结合的线程设置为分离的,这样就不需要再调用pthread_join函数来等待和回收该线程。如果一个线程已经是分离的,再调用pthread_detach函数会返回EINVAL错误。

一般来说,有两种方法可以创建一个分离的线程:
在调用pthread_create函数时,将线程属性设置为PTHREAD_CREATE_DETACHED,这样创建的线程就是分离的。
在线程开始运行后,调用pthread_detach函数将自己设置为分离的。
使用pthread_detach函数的好处是可以避免资源泄漏和死锁等问题,但也要注意不能对一个已经分离的线程进行回收或取消操作

1.2 线程分离用例

如下为一个简单的例子,程序运行,创建子线程,主线程和子线程同时运行,主线程遇到join等待子线程退出,子线程运行完退出。

#include 
#include 
#include 
#include 
#include 
using namespace std;

void *threadRoutine(void* args)
{
    string name = static_cast<const char*>(args); //static_cast<强转的类型>() 如同强转
    int cnt = 5;
    while (cnt)
    {
        cout << name << ":" << cnt-- << endl;
        sleep(1);
    }

    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1"); //创建线程
	
    int n = pthread_join(tid, nullptr); //等待线程结束
    if (n != 0) //为0表示等待成功,非0表示失败
    {
        cerr << "error" << n << ":" << strerror(n) << endl;
    }
    return 0;
}

linux线程互斥_第1张图片
接下来用pthread_detach函数将子线程分离,可以在主线程中分离,也可以在子线程中分离,其中在子线程中分离要注意要在主线程退出之前分离,不然还未分离,主线程退出,所有线程都会退出,而未将子线程join,也未将子线程detach,会造成资源泄漏。

#include 
#include 
#include 
#include 
#include 
using namespace std;

void *threadRoutine(void* args)
{
    string name = static_cast<const char*>(args); //static_cast<强转的类型>() 如同强转
    int cnt = 5;
    while (cnt)
    {
        cout << name << ":" << cnt-- << endl;
        sleep(1);
    }

    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
    pthread_detach(tid);
    sleep(1);
    // int n = pthread_join(tid, nullptr);
    // if (n != 0)
    // {
    //     cerr << "error" << n << ":" << strerror(n) << endl;
    // }
    return 0;
}

如果分离线程,还进行join,那么会打印如下错误error22:Invalid argument

2.线程独立栈

由以下代码看现象:主线程创建三个线程,主线程等待三个线程,三个线程访问同一个函数,函数中有一个变量,对其进行操作,然后观察其的变化。

#include 
#include 
#include 
#include 
#include 
using namespace std;

void *threadRoutine(void* args)
{
    string name = static_cast<const char*>(args);
    int cnt = 5;
    while (cnt)
    {
        cout << name << ":" << cnt-- << endl;
        sleep(1);
    }
    
    return nullptr;
}
int main()
{
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, threadRoutine, (void*)"thread 1");
    pthread_create(&t2, nullptr, threadRoutine, (void*)"thread 2");
    pthread_create(&t3, nullptr, threadRoutine, (void*)"thread 3");
    
    pthread_join(t1, nullptr); 
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    return 0;
}

由下图看出,三个线程谁先运行由调度器来决定;并且三个线程中的cnt变量是不同的,因为线程有自己独立的栈,每个线程在进程虚拟地址空间中会分配拥有相对独立的栈空间。
linux线程互斥_第2张图片

但如果访问全局变量,可以看到访问的是同一个全局变量(g_val)。

#include 
#include 
#include 
#include 
#include 
using namespace std;

int g_val = 100;
void *threadRoutine(void* args)
{
    string name = static_cast<const char*>(args);

    while (g_val)
    {
        cout << name << ":" << g_val++ << "  &g_val:" << &g_val << endl;
        sleep(1);
    }

    return nullptr;
}
int main()
{
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, threadRoutine, (void*)"thread 1");
    pthread_create(&t2, nullptr, threadRoutine, (void*)"thread 2");
    pthread_create(&t3, nullptr, threadRoutine, (void*)"thread 3");
    
    pthread_join(t1, nullptr); 
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    return 0;
}

linux线程互斥_第3张图片

3. Linux线程互斥

进程线程间的互斥相关背景概念

  1. 临界资源:多线程执行流共享的资源就叫做临界资源
  2. 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
  3. 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  4. 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

互斥量mutex
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。

// 操作共享变量会有问题,如下售票系统代码
#include 
#include 
#include 
#include 
#include 
using namespace std;

int ticket = 10000;
void *threadRoutine(void* args)
{
    string name = static_cast<const char*>(args);

    while (true)
    {
        usleep(1000);
        if (ticket > 0)
            cout << name << " get a ticket: " << ticket-- << endl;
        else 
            break;
    }

    return nullptr;
}
int main()
{
    pthread_t t[4];
    int n = sizeof(t)/sizeof(t[0]);
    for (int i = 0; i < n; ++i)
    {
        char* name = new char[64];
        snprintf(name, 64, "thread-%d", i + 1);
        pthread_create(t + i, nullptr, threadRoutine, name);
    }
    for (int i = 0; i < n; ++i)
    {
        pthread_join(t[i], nullptr);
    }

    return 0;
}

多线程并发访问临界资源,可能会造成越界的原因是多个线程同时访问同一份资源,这个资源对应的值有可能会出现值不准确的情况。这是因为在多个线程访问同一份资源的时候,如果一个线程在取值的过程中线程被切换到另一个线程,那么另一个线程也会取到这个值,然后对这个值进行操作,就会出现越界的情况。
linux线程互斥_第4张图片

为了避免这种情况,通常会对临界资源(共享资源)进行加锁(Linux上提供的这把锁叫互斥量),让并发访问临界资源变成串型访问临界资源,以此来保证临界资源安全问题。

互斥锁
互斥锁是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全域变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区域(critical section)达成。互斥锁有两种状态:上锁和解锁。在某一时刻只能有一个线程掌握着互斥。掌握着互斥的线程可以对共享资源进行操作。若其他线程想要上锁一个已经被上锁的互斥锁,该线程就会被挂起,等到已上锁的线程释放掉互斥锁为止。互斥锁保证了每个线程按顺序对共享资源进行操作。

以下是互斥锁相关操作:
定义互斥锁pthread_mutex_t数据类型来表示。
linux线程互斥_第5张图片

初始化互斥锁和锁毁互斥锁函数
初始化互斥锁函数:pthread_mutex_init函数是一个初始化互斥锁的函数,它的作用是初始化一个互斥锁,以便后续使用。它有两个参数:mutex和attr。mutex是指向要初始化的互斥锁的指针,attr是指向属性对象的指针,定义了初始化互斥锁的属性,比如锁类型、优先级等。如果attr为NULL,则使用默认的互斥锁属性,默认属性为快速互斥锁。函数成功返回0,失败返回错误码
锁毁互斥锁函数:pthread_mutex_destroy函数是一个销毁互斥锁的函数,它的作用是销毁一个已经初始化的互斥锁,使其变为无效的状态。一个已经销毁的互斥锁可以重新使用pthread_mutex_init函数进行初始化,否则对其进行其他操作的结果是未定义的。销毁互斥锁的函数只能由占有该互斥锁的线程完成,如果试图销毁一个被其他线程锁定或引用的互斥锁,会导致未定义的行为。当这两个函数成功完成时返回0,否则返回错误编号指明错误。
申请互斥锁
pthread_mutex_lock函数是一个线程同步函数,用于对互斥锁进行加锁操作。互斥锁是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。互斥锁有两种状态:上锁和解锁。在某一时刻只能有一个线程掌握着互斥。掌握着互斥的线程可以对共享资源进行操作。若其他线程想要上锁一个已经被上锁的互斥锁,该线程就会被挂起,等到已上锁的线程释放掉互斥锁为止。互斥锁保证了每个线程按顺序对共享资源进行操作。加锁的本质就是将锁的共享数据交换到自己的私有上下文当中,如果加锁失败,线程则会被挂起等待。
函数原型:int pthread_mutex_lock(pthread_mutex_t *mutex);
函数参数:mutex是指向要加锁的互斥锁的指针。
函数返回值:如果成功,返回0;如果失败,返回错误码。
函数功能:以阻塞的方式申请互斥锁。如果该互斥锁已经被其他线程占用,那么调用该函数的线程会进入阻塞状态,直到该互斥锁被释放为止。如果该互斥锁没有被占用,那么调用该函数的线程会立即获取该互斥锁,并将其状态设置为上锁。

int pthread_mutex_trylock(pthread_mutex_t *mutex);以非阻塞的方式申请互斥锁。

pthread_mutex_unlock函数是一个线程同步函数,用于对互斥锁进行解锁操作。若其他线程想要上锁一个已经被上锁的互斥锁,该线程就会被挂起,等到已上锁的线程释放掉互斥锁为止。互斥锁保证了每个线程按顺序对共享资源进行操作。
以下是pthread_mutex_unlock函数的使用方法:

函数原型:int pthread_mutex_unlock(pthread_mutex_t *mutex);
函数参数:mutex是指向要解锁的互斥锁的指针。
函数返回值:如果成功,返回0;如果失败,返回错误码。
函数功能:释放一个已经被当前线程占用的互斥锁。如果有其他线程正在等待该互斥锁,pthread_mutex_unlock函数会唤醒其中一个线程,并让它从pthread_mutex_lock函数返回,同时获取该互斥锁。如果没有其他线程等待该互斥锁,那么该互斥锁就变为解锁状态,没有当前拥有者。

实例
给上述售票系统进行加锁,代码如下:

#include 
#include 
#include 
#include 
#include 
using namespace std;

class TData
{
public:
    TData(const string name, pthread_mutex_t *mutex)
    :_name(name), _pmutex(mutex)
    {}
public:
    string _name;
    pthread_mutex_t *_pmutex;
};

int ticket = 10000;
void *threadRoutine(void* args)
{
    TData* td = static_cast<TData*>(args);

    //pthread_mutex_lock(td->_pmutex); //加锁应该保证粒度够细,所以不再此处加锁
    while (true)
    {
        pthread_mutex_lock(td->_pmutex); //加锁应该保证粒度够细
        if (ticket > 0)
        {
            cout << td->_name << " get a ticket: " << ticket-- << endl; //临界区
            pthread_mutex_unlock(td->_pmutex);
        }
        else 
        {
            pthread_mutex_unlock(td->_pmutex);
            break;
        }
        // usleep(200);
    }

    return nullptr;
}
int main()
{
    //多个线程需要加同一把锁
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, nullptr);

    pthread_t t[4];
    int n = sizeof(t)/sizeof(t[0]);
    for (int i = 0; i < n; ++i)
    {
        char name[64];
        snprintf(name, 64, "thread-%d", i + 1);
        TData* td = new TData(name, &mutex);
        pthread_create(t + i, nullptr, threadRoutine, (void*)td);
    }
    for (int i = 0; i < n; ++i)
    {
        pthread_join(t[i], nullptr);
    }

    pthread_mutex_destroy(&mutex);//使用完后销毁锁

    return 0;
}

在使用锁时,有一些注意事项:

  1. 对象之间的依赖顺序是单向的,保障对象A的锁和对象B的锁是按顺序的。
  2. 加锁尽可能要考虑粒度和场景,锁保护的代码意味着无法进行多线程操作。对于Web类型的天然多线程项目,对方法进行大范围加锁会显著降级并发能力,要考虑尽可能地只为必要的代码块加锁,降低锁的粒度;而对于要求超高性能的业务,还要细化考虑锁的读写场景,以及悲观优先还是乐观优先,尽可能针对明确场景精细化加锁方案。
  3. 业务逻辑中有多把锁时要考虑死锁问题,通常的规避方案是,避免无限等待和循环等待。
  4. 加锁和释放没有配对的问题。加群解锁没有配对可以用一些代码质量工具协助排插,如Sonar,集成到ide和代码仓库,在编码阶段发现,加上超时自动释放,避免长期占有锁。
  5. 锁自动超时释放了,而业务逻辑却还在进行的情况下,如果别的线线程或进程拿到了相同的锁,可能会导致重复执行。

你可能感兴趣的:(linux)