概念:线程分离是指在一个线程结束时,能够自动释放该线程所占用的系统资源,而不需要其他线程对其进行回收。
线程分离的状态决定一个线程以什么样的方式来终止自己。非分离状态下,一个线程结束后,还有一部分资源没有被回收,所以创建线程者应该调用pthread_join来等待线程运行结束,并可得到线程的退出代码,回收其资源。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
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函数的好处是可以避免资源泄漏和死锁等问题,但也要注意不能对一个已经分离的线程进行回收或取消操作。
如下为一个简单的例子,程序运行,创建子线程,主线程和子线程同时运行,主线程遇到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;
}
接下来用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
。
由以下代码看现象:主线程创建三个线程,主线程等待三个线程,三个线程访问同一个函数,函数中有一个变量,对其进行操作,然后观察其的变化。
#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变量是不同的,因为线程有自己独立的栈,每个线程在进程虚拟地址空间中会分配拥有相对独立的栈空间。
但如果访问全局变量,可以看到访问的是同一个全局变量(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;
}
进程线程间的互斥相关背景概念
互斥量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上提供的这把锁叫互斥量),让并发访问临界资源变成串型访问临界资源,以此来保证临界资源安全问题。
互斥锁
互斥锁是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全域变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区域(critical section)达成。互斥锁有两种状态:上锁和解锁。在某一时刻只能有一个线程掌握着互斥。掌握着互斥的线程可以对共享资源进行操作。若其他线程想要上锁一个已经被上锁的互斥锁,该线程就会被挂起,等到已上锁的线程释放掉互斥锁为止。互斥锁保证了每个线程按顺序对共享资源进行操作。
以下是互斥锁相关操作:
定义互斥锁pthread_mutex_t
数据类型来表示。
初始化互斥锁和锁毁互斥锁函数
初始化互斥锁函数: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;
}
在使用锁时,有一些注意事项: