一、 创建线程
在Linux下,采用pthread_create函数来创建一个新的线程
语法:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
thread:指向线程标识符的地址。
attr:用于设置线程属性,一般为null,表示使用默认属性。
start_routine:是线程运行函数的地址,填函数名就可以了。
arg: 作为实参传递到 start_routine 指针指向的函数内部。
返回值:
线程创建成功返回 0,创建失败返回对应的错误号
编译:
在编译时注意加上-lpthread参数,以调用静态链接库,否则会报错。因为pthread并非Linux系统的默认库。
例子:
#include <stdio.h>
#include <iostream>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
using namespace std;
void *working(void *)
{
for (int i = 0; i < 3; i++)
{
cout << "我是子线程:" << i << endl;
}
cout << "子线程结束" << endl<<endl;
return NULL;
}
int main()
{
pthread_t t;
pthread_create(&t, NULL, working, NULL);
for (int i = 0; i < 3; i++)
{
cout << "我是主线程:" << i << endl;
}
sleep(1);
return 0;
}
若不加sleep()函数输出的结果:
为什么没有子线程没有执行呢?
子线程被创建出来之后需要抢cpu时间片, 抢不到就不能运行,如果主线程退出了, 虚拟地址空间就被释放了, 子线程就一并被销毁了。但是如果某一个子线程退出了, 主线程仍在运行, 虚拟地址空间依旧存在。可以在主线程中添加挂起函数 sleep(),等时间片空白的时候,就可以被子线程用了。在没有人为干预的情况下,虚拟地址空间的生命周期和主线程是一样的,与子线程无关。
加sleep()函数输出的结果:
2. 线程的终止
如果进程中的任一线程调用了exit,则整个进程会终止,所以,在线程的start_routine函数中,不能采用exit。
线程的终止有三种方式:
1)线程的start_routine函数代码结束,自然消亡。
2)线程的start_routine函数调用pthread_exit结束。只要调用该函数当前线程就马上退出了,并且不会影响到其他线程的正常运行,不管是在子线程或者主线程中都可以使用。
3)被主进程或其它线程中止。
语法:
#include <pthread.h>
void pthread_exit(void *retval);
retval:线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为 NULL。
例子:
#include <stdio.h>
#include <iostream>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
using namespace std;
void *working(void *)
{
for (int i = 0; i < 3; i++)
{
cout << "我是子线程:" << i << endl;
}
cout << "子线程结束" << endl<<endl;
return NULL;
}
int main()
{
pthread_t t;
pthread_create(&t, NULL, working, NULL);
pthread_exit(NULL);//主线程退出,子线程不会退出
for (int i = 0; i < 3; i++)
{
cout << "我是主线程:" << i << endl;
}
sleep(1);
return 0;
}
3. 线程回收
3.1线程回收函数
线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函叫做 pthread_join(),这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。另外通过线程回收函数还可以获取到子线程退出时传递出来的数据。
语法:
#include <pthread.h>
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞
// 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
int pthread_join(pthread_t thread, void **retval);
thread: 要被回收的子线程的线程 ID。
retval: 二级指针,指向一级指针的地址,是一个传出参数,这个地址中存储了 pthread_exit () 传递出的数据,如果不需要这个参数,可以指定为 NULL。
返回值:
线程回收成功返回 0,回收失败返回错误号。
3.2 回收子线程数据
- 在子线程退出的时候可以使用 pthread_exit() 的参数将数据传出
- 在回收这个子线程的时候可以通过 phread_join() 的第二个参数来接收子线程传递出的数据。
接收数据有很多种处理方式,下面来列举几种:
3.2.1 使用子线程的栈
通过函数 pthread_exit(void *retval); 可以得知,子线程退出的时候,需要将数据记录到一块内存中,通过参数传出的是存储数据的内存的地址,而不是具体数据,由因为参数是 void* 类型,所有这个万能指针可以指向任意类型的内存地址。
代码:
#include <stdio.h>
#include <iostream>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
using namespace std;
struct student
{
string name;
int age;
};
void *working(void *)
{
cout << "我是子线程:" << endl;
struct student stu;
stu.name="小明";
stu.age=18;
pthread_exit(&stu);
return NULL;
}
int main()
{
pthread_t t;
pthread_create(&t, NULL, working, NULL);
for (int i = 0; i < 3; i++)
{
cout << "我是主线程:" << i << endl;
}
void *ptr = NULL;
pthread_join(t, &ptr);
struct student* pp = (struct student*)ptr;//void*万能指针需要强制类型转换使用
cout<<"名字:"<<pp->name<<"年龄:"<<pp->age<<endl;
cout<<"子线程回收成功"<<endl;
return 0;
}
输出结果:
分析:
主线程中为什么没有没有得到子线程返回的数据信息年龄和名字呢?
具体原因是这样的:
如果多个线程共用同一个虚拟地址空间,每个线程在栈区都有一块属于自己的内存,相当于栈区被这几个线程平分了,当线程退出,线程在栈区的内存也就被回收了,因此随着子线程的退出,写入到栈区的数据也就被释放了,也就意味着数据的地址被释放了。
3.2.2 使用全局变量
位于同一虚拟地址空间中的线程,虽然不能共享栈区数据,但是可以共享全局数据区和堆区数据,因此在子线程退出的时候可以将传出数据存储到全局变量、静态变量或者堆内存中。在下面的例子中将数据存储到了全局变量中
代码:
#include <stdio.h>
#include <iostream>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
using namespace std;
struct student
{
string name;
int age;
};
struct student stu;//全局变量
void *working(void *)
{
cout << "我是子线程:" << endl;
stu.name="小明";
stu.age=18;
pthread_exit(&stu);
return NULL;
}
int main()
{
pthread_t t;
pthread_create(&t, NULL, working, NULL);
for (int i = 0; i < 3; i++)
{
cout << "我是主线程:" << i << endl;
}
void *ptr = NULL;
pthread_join(t, &ptr);
struct student* pp = (struct student*)ptr;//void*万能指针需要强制类型转换使用
cout<<"名字:"<<pp->name<<"年龄:"<<pp->age<<endl;
cout<<"子线程回收成功"<<endl;
return 0;
}
输出结果:
4.2.3 使用主线程栈
虽然每个线程都有属于自己的栈区空间,但是位于同一个地址空间的多个线程是可以相互访问对方的栈空间上的数据的。由于很多情况下还需要在主线程中回收子线程资源,所以主线程一般都是最后退出,基于这个原因在下面的程序中将子线程返回的数据保存到了主线程的栈区内存中:
代码:
#include <stdio.h>
#include <iostream>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
using namespace std;
struct student
{
string name;
int age;
};
void *working(void *arg)
{
struct student *p = (struct student *)arg;//强制类型转换
cout << "我是子线程:" << endl;
p->name = "小明";
p->age = 18;
pthread_exit(p);
return NULL;
}
int main()
{
pthread_t t;
struct student stu;
pthread_create(&t, NULL, working, &stu);
for (int i = 0; i < 3; i++)
{
cout << "我是主线程:" << i << endl;
}
void *ptr = NULL;
pthread_join(t, &ptr);
struct student *pp = (struct student *)ptr; // void*万能指针需要强制类型转换使用
cout << "名字:" << pp->name << "年龄:" << pp->age << endl;
cout << "子线程回收成功" << endl;
return 0;
}
结果:
4. 线程分离
主线程退出了,虚拟地址空间就结束了。
在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用 pthread_join() 只要子线程不退出主线程就会一直被阻塞,主要线程的任务也就不能被执行了。
在线程库函数中为我们提供了线程分离函数 pthread_detach(),调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用 pthread_join() 就回收不到子线程资源了。
语法:
#include <pthread.h>
// 参数就子线程的线程ID, 主线程就可以和这个子线程分离了
int pthread_detach(pthread_t thread);
代码:
#include <stdio.h>
#include <iostream>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
using namespace std;
struct student
{
string name;
int age;
};
void *working(void *arg)
{
cout << "我是子线程:" << endl;
for (int i = 0; i < 5; i++)
{
cout<<"我是线程"<<i<<endl;
}
return NULL;
}
int main()
{
pthread_t t;
pthread_create(&t, NULL, working, NULL);
for (int i = 0; i < 3; i++)
{
cout << "我是主线程:" << i << endl;
}
pthread_detach(t);
pthread_exit(NULL);
return 0;
}
结果:
可以看出主线程没有并调用sleep方法让出时间片,但是主线程退退出后,子线程还在执行。
5. 其他线程函数
5.1 线程取消
含义:
线程取消的意思就是在某些特定情况下在一个线程中杀死另一个线程。使用这个函数杀死一个线程需要分两步:
- 在线程 A 中调用线程取消函数 pthread_cancel。
- 在线程 B 中进程一次系统调用(从用户区切换到内核区),才会被取消掉,否则线程 B 可以一直运行。
语法:
线程A杀死另一个线程B,需要在线程A调用此函数。
#include <pthread.h>
// 参数是子线程的线程ID,在线程A中调用
int pthread_cancel(pthread_t thread);
- 参数:要杀死的线程的线程 ID
- 返回值:函数调用成功返回 0,调用失败返回非 0 错误号。
代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// 子线程的处理代码
void* working(void* arg)
{
int j=0;
for(int i=0; i<9; ++i)
{
j++;
}
// 这个函数会调用系统函数, 因此这是个间接的系统调用
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf(" child i: %d\n", i);
}
return NULL;
}
int main()
{
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("i = %d\n", i);
}
// 杀死子线程, 如果子线程中做系统调用, 子线程就结束了
pthread_cancel(tid);
// 让主线程自己退出即可
pthread_exit(NULL);
return 0;
}
6.2 线程 ID 比较
在 Linux 中线程 ID 本质就是一个无符号长整形,因此可以直接使用比较操作符比较两个线程的 ID,但是线程库是可以跨平台使用的,在某些平台上 pthread_t 可能不是一个单纯的整形,这中情况下比较两个线程的 ID 必须要使用比较函数,函数原型如下:
语法:
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
- 参数:t1 和 t2 是要比较的线程的线程 ID
- 返回值:如果两个线程 ID 相等返回非 0 值,如果不相等返回 0
6.其他
以上线程函数也可以用于 C++ 编程,但是 C++11 中提供了线程类。
C++11 之前,C++ 语言没有对并发编程提供语言级别的支持,这使得我们在编写可移植的并发程序时,存在诸多的不便。现在 C++11 中增加了线程以及线程相关的类,很方便地支持了并发编程,使得编写的多线程程序的可移植性得到了很大的提高。
C++11 中提供的线程类叫做 std::thread。