线程控制学习
1、线程创建:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);
参数 thread:返回线程ID;attr:设置线程的属性,attr为nullptr表示使用默认属性(一般这个都是nullptr);start_routine:是个函数地址,线程启动后要执行的函数;arg:传给线程启动函数的参数 返回值:成功返回0;失败返回错误码。
注意上面的start_routine是返回值为void*,参数也是void*。
pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID。pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
在创建线程时要使用到线程库,所以在使用线程库时,会将对应的库文件加载到内存中,然后通过页表和虚拟地址完成映射。最终线程库会被映射到虚拟地址空间的mmap区域。下图所示:
当我们调用pthread_create创建线程后,在线程库中就会新建一个描述线程的结构体,来保存线程的相关数据,而这一部分也是实际的内存,有对应的地址,而pthread_t本质就是一个进程地址空间上的地址。
这就相当于线程的TCB。
通过上面对进程地址空间分布的认识也可以学习一个操作:_thread
对于全局变量,主线程和新线程是共享的,所以当某一个线程访问全局变量时,访问的都是同一个变量,下图可以看到新线程改变g_val,主线程读出的也跟着改变:
但是对全局变量添加_thread修饰后,就将一个内置类型设置为局部存储,这种情况下,虽然这个g_val仍然是全局变量,但是每个线程都有属于自己的一个,互不影响,当在自己的线程中对其改变时,不会影响其他的线程,下图可以看出,新线程改变g_val,但是主线程并没跟着变:
而且通过上面的两个图也可以发现,他们的地址变换很大,因为刚开始,只是一个全局表量,保存在已初始化数据段,而添加_thread修饰后,主线程的g_val就在主线程栈中存储,而新线程的g_val就在对应线程空间的线程栈上。
2、线程退出:
1)新线程函数return;
void *routine(void *args)
{
ThreadData* td = static_cast<ThreadData*>(args);
int cnt = 5;
while (cnt)
{
cout << "我是新线程!name: " << td->namebuffer << endl;
cnt--;
sleep(1);
// exit(0); //不能终止线程,是用来终止进程的,这里任何一个执行流调用exit,会让所有的线程和主线程都终止。
}
//一般终止线程return即可
//nullptr;
return (void*)100; //1. 线程函数结束,return的时候,线程就算终止了
}
2)线程可以调用pthread_ exit终止自己;
void pthread_exit(void *value_ptr); 参数 value_ptr:value_ptr不要指向一个局部变量。
void *routine(void *args)
{
ThreadData* td = static_cast<ThreadData*>(args);
int cnt = 5;
while (cnt)
{
cout << "我是新线程!name: " << td->namebuffer << endl;
cnt--;
sleep(1);
}
ThreadReturn* tr = new ThreadReturn();
tr->code_quit = td->number;
tr->code_result = 1;
//注意:返回值需要是void*
pthread_exit((void*)tr);
}
注意:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc/new分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
上面的ThreadReturn是自己定义的一个结构体,用来保存退出信息。这里的返回值是new出来的,在主线程的线程等待时记得delete。
3)一个线程可以调用pthread_ cancel终止同进程中的另一个线程。
int pthread_cancel(pthread_t thread); 参数 thread:线程ID;成功返回0;失败返回错误码。
线程取消要注意:只有运行的线程才能被取消:
#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>
using namespace std;
class ThreadData
{
public:
pthread_t tid;
char namebuffer[64];
int number;
};
class ThreadReturn
{
public:
int code_quit;
int code_result;
};
void *routine(void *args)
{
ThreadData* td = static_cast<ThreadData*>(args);
int cnt = 5;
while (cnt)
{
cout << "我是新线程!name: " << td->namebuffer << endl;
cnt--;
sleep(1);
// exit(0); //不能终止线程,是用来终止进程的,这里任何一个执行流调用exit,会让所有的线程和主线程都终止。
}
return nullptr;
}
int main()
{
vector<ThreadData*> threads;
#define NUM 10
for (int i = 0; i < NUM; ++i)
{
ThreadData* td = new ThreadData();
td->number = i+1;
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);
pthread_create(&(td->tid), nullptr, routine, td);
threads.push_back(td);
// sleep(1);
}
for(auto& iter : threads)
{
cout << "create thread" << iter->namebuffer << " : " << iter->tid << "success" << endl;
}
//线程取消:pthread_cancel,只有执行中的线程才能被取消
sleep(2);
for(int i = 0; i < threads.size()/2; ++i)
{
pthread_cancel(threads[i]->tid);
cout << "cancel: " << threads[i]->namebuffer << " success!" << endl;
}
/*
for(auto& iter : threads)
{
void* ret = nullptr;
//这里的第二个参数的类型需要是void**,因为外面的ret是void*,
//如果传ret进去,那么函数内部改变ret并不会影响外面的值,所以需要传地址进去,那么ret的地址就是void**的了。
pthread_join(iter->tid, &ret);
cout << "join: " << iter->namebuffer << " success! code_quit--> " << (long long)ret << endl;
//释放new的资源
delete ret;
delete iter;
}
*/
cout << "main thread quit!" << endl;
return 0;
}
3、线程等待:
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。 创建新的线程不会复用刚才退出线程的地址空间。如果不等待,会造成类似僵尸进程的问题:内存泄漏。
int pthread_join(pthread_t thread, void **value_ptr);
参数 thread:线程ID;value_ptr:它指向一个指针,后者指向线程的返回值。
等待成功返回0;失败返回错误码。
1)如果新线程通过return返回,value_ ptr所指向的单元里存放的是新线程函数的返回值。
2)如果新线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED。
3)如果新线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
4)如果对新线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
要注意:这里的value_ptr是输出型参数,用来获取新线程的返回值,因为新线程的返回值是void*的,返回的数据保存在pthread库中;想要同过该函数将返回值带出,就得在外面用个void* 的变量,然后再将该变量的地址(void**)传进去,最终就能拿到新线程的返回值信息。
4、线程分离:
1)默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
2)如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
所以对分离的线程,就不需要再join了,可以自动释放资源。
int pthread_detach(pthread_t thread);
线程分离函数,参数thread是线程ID。线程ID可以通过pthread_self()来获得。
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <cstring>
using namespace std;
void* start_routine(void* args)
{
string name = static_cast<const char*>(args);
//将自己设置为分离状态,pthread_self()是获取自己的ID
// pthread_detach(pthread_self());
int cnt = 5;
while(cnt--)
{
cout << name << " running..." << endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, start_routine, (void*)"thread 1");
//将创建的新线程分离,分离后该线程退出时自动回收
pthread_detach(tid);
//一个线程默认是joinable的,如果设置了分离状态,就不能够进行等待了。
int n = pthread_join(tid,nullptr);
cout << n << " : " << strerror(n) << endl;
return 0;
}
这里需要注意的是:进行线程分离时,可以在主线程创建后分离,也可以在新线程内部设置分离。
这里先看创建新线程后就分离,如果对分离的线程再等待就会出问题,最终导致程序崩溃:
而在新线程内部分离,可能有一些问题,因为创建新线程后,并不能确定是哪一个线程先运行,可能主线程先运行,已经开始等待新线程,而线程运行时虽然设置分离状态,主线程并不知道,但是主线程仍然在等待,并且等待成功,但是实际上对于已经分离的线程再等待就会出现问题: