相信很多同学在做项目的时候都需要用到多线程的知识,我这里进行简单的总结,希望能给各位一些参考。
这里参考了优秀文章/资源并进行整理,放在前面,大家也可以去看看。
线程是轻量级的进程(LWP:light weight process
),在 Linux 环境下线程的本质仍是进程。在计算机上运行的程序是一组指令及指令参数的组合,指令按照既定的逻辑控制计算机运行。操作系统会以进程为单位,分配系统资源,可以这样理解,进程是资源分配的最小单位,线程是操作系统调度执行的最小单位。
先从概念上了解一下线程和进程之间的区别:
本人开发主要在Linux
系统中,最常用的便是pthread_creat
方式。
pthread_t
/* Thread identifiers. The structure of the attribute type is not
exposed on purpose. */
typedef unsigned long int pthread_t;
pthread_create
//#include
/* Create a new thread, starting with execution of START-ROUTINE
getting passed ARG. Creation attributed come from ATTR. The new
handle is stored in *NEWTHREAD. */
extern int pthread_create (pthread_t *__restrict __newthread,
const pthread_attr_t *__restrict __attr,
void *(*__start_routine) (void *),
void *__restrict __arg) __THROWNL __nonnull ((1, 3));
参数:
__newthread
: 传出参数,是无符号长整形数,线程创建成功,会将线程 ID
写入到这个指针指向的内存中__attr
: 线程的属性,一般情况下使用默认属性即可,一般写 NULL
__start_routine
: 函数指针,创建出的子线程的处理动作,也就是该函数在子线程中执行。__arg
: 作为实参传递到 __start_routine
指针指向的函数内部#include
#include
#include
#include
#include
// 子线程函数
void* func(void* arg)
{
printf("子线程ID: %ld \n", pthread_self());
printf("hello world!\n");
return NULL;
}
int main()
{
// 创建一个子线程
pthread_t id;
pthread_create(&id, NULL, func, NULL);
printf("子线程创建成功, 线程ID: %ld\n", id);
// 打印主线程
printf("主线程ID: %ld\n", pthread_self());
//等待子线程结束
pthread_join(id, NULL);
return 0;
}
执行结果
子线程创建成功, 线程ID: 139989099230976
主线程ID: 139989107406592
子线程ID: 139989099230976
hello world!
C++11
之前,C++
语言没有对并发编程提供语言级别的支持,这使得我们在编写可移植的并发程序时,存在诸多的不便。现在 C++11
中增加了线程以及线程相关的类,很方便地支持了并发编程,使得编写的多线程程序的可移植性得到了很大的提高。
C++11
中提供的线程类叫做 std::thread
,基于这个类创建一个新的线程非常的简单,只需要提供线程函数或者函数对象即可,并且可以同时指定线程函数的参数。我们首先来了解一下这个类提供的一些常用 API
:
// ①
thread() noexcept;
// ②
thread( thread&& other ) noexcept;
// ③
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
// ④
thread( const thread& ) = delete;
任务函数 f 的可选类型有很多,具体如下:
普通函数,类成员函数,匿名函数,仿函数(这些都是可调用对象类型)
可以是可调用对象包装器类型,也可以是使用绑定器绑定之后得到的类型(仿函数)
与pthread_self()
类似,get_id()
是获取线程 ID
的函数
inline thread::id get_id() noexcept
#include
#include
using namespace std;
void func(int num, string str)
{
for (int i = 0; i < 3; ++i)
{
cout << "子线程1: i = " << i << "num: " << num << ", str: " << str << endl;
}
}
void func1()
{
for (int i = 0; i < 3; ++i)
{
cout << "子线程2: i = " << i << endl;
}
}
int main()
{
cout << "主线程的线程ID: " << this_thread::get_id() << endl;
thread t(func, 520, "i love you");
thread t1(func1);
cout << "线程t 的线程ID: " << t.get_id() << endl;
cout << "线程t1的线程ID: " << t1.get_id() << endl;
//等待子线程结束
t.join();
t1.join();
}
假设有 4
个线程 A、B、C、D
,当前一个线程 A
对内存中的共享资源进行访问的时候,其他线程 B, C, D
都不可以对这块内存进行操作,直到线程 A
对这块内存访问完毕为止,B,C,D
中的一个才能访问这块内存,剩余的两个需要继续阻塞等待,以此类推,直至所有的线程都对这块内存操作完毕。 线程对内存的这种访问方式就称之为线程同步,通过对概念的介绍,我们可以了解到所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。
在研究线程同步之前,先来看一个两个线程交替数数(每个线程数 5
个数,交替数到 10
)的例子:
#include
#include
#include
#include
#include
#include
#include
#define MAX 10
// 全局变量
int number;
// 线程处理函数
void* funcA_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
int cur = number;
cur++;
usleep(10);
number = cur;
printf("Thread A, id = %lu, number = %d\n", pthread_self(), number);
}
return NULL;
}
void* funcB_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
int cur = number;
cur++;
number = cur;
printf("Thread B, id = %lu, number = %d\n", pthread_self(), number);
usleep(5);
}
return NULL;
}
int main(int argc, const char* argv[])
{
pthread_t p1, p2;
// 创建两个子线程
pthread_create(&p1, NULL, funcA_num, NULL);
pthread_create(&p2, NULL, funcB_num, NULL);
// 阻塞,资源回收
pthread_join(p1, NULL);
pthread_join(p2, NULL);
return 0;
}
执行结果
Thread B, id = 140685318096640, number = 1
Thread A, id = 140685326489344, number = 1
Thread B, id = 140685318096640, number = 2
Thread A, id = 140685326489344, number = 2
Thread B, id = 140685318096640, number = 3
Thread A, id = 140685326489344, number = 3
Thread A, id = 140685326489344, number = 4
Thread B, id = 140685318096640, number = 5
Thread A, id = 140685326489344, number = 5
Thread B, id = 140685318096640, number = 6
通过对上面例子的测试,可以看出虽然每个线程内部循环了 5
次每次数一个数,但是最终没有数到 10
,有些数字被重复数了多次,其原因就是没有对线程进行同步处理,造成了数据的混乱。具体导致这种现象的原因涉及到寄存器、一级缓存、二级缓存、三级缓存相关知识,这里不进行展开介绍。
对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步。常用的线程同步方式有四种:互斥锁、读写锁、条件变量、信号量
。所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也被称之为临界资源。
首先进行互斥锁的使用介绍,分别介绍c语言的pthread_mutex_lock
方式以及C++的mutex
类
互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块,被锁定的这个代码块,所有的线程只能顺序执行 (不能并行处理),这样多线程访问共享资源数据混乱的问题就可以被解决了,需要付出的代价就是执行效率的降低,因为默认临界区多个线程是可以并行处理的,现在只能串行处理。
pthread_mutex_lock
#include
#include
#include
#include
#include
#include
#include
#define MAX 5
// 全局变量
int number;
// 创建一把互斥锁
// 全局变量, 多个线程共享
pthread_mutex_t mutex;
// 线程处理函数
void* funcA_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
// 如果线程A加锁成功, 不阻塞
// 如果B加锁成功, 线程A阻塞
pthread_mutex_lock(&mutex);
int cur = number;
cur++;
usleep(10);
number = cur;
pthread_mutex_unlock(&mutex);
printf("Thread A, id = %lu, number = %d\n", pthread_self(), number);
}
return NULL;
}
void* funcB_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
// a加锁成功, b线程访问这把锁的时候是锁定的
// 线程B先阻塞, a线程解锁之后阻塞解除
// 线程B加锁成功了
pthread_mutex_lock(&mutex);
int cur = number;
cur++;
number = cur;
pthread_mutex_unlock(&mutex);
printf("Thread B, id = %lu, number = %d\n", pthread_self(), number);
usleep(5);
}
return NULL;
}
int main(int argc, const char* argv[])
{
pthread_t p1, p2;
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 创建两个子线程
pthread_create(&p1, NULL, funcA_num, NULL);
pthread_create(&p2, NULL, funcB_num, NULL);
// 阻塞,资源回收
pthread_join(p1, NULL);
pthread_join(p2, NULL);
// 销毁互斥锁
// 线程销毁之后, 再去释放互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
执行结果:
Thread A, id = 140496895964928, number = 1
Thread B, id = 140496887572224, number = 2
Thread A, id = 140496895964928, number = 3
Thread A, id = 140496895964928, number = 4
Thread A, id = 140496895964928, number = 5
Thread A, id = 140496895964928, number = 6
Thread B, id = 140496887572224, number = 7
Thread B, id = 140496887572224, number = 8
Thread B, id = 140496887572224, number = 9
Thread B, id = 140496887572224, number = 10
mutex
类mutex
类是真的方便,这里我将上面的程序进行了改动,使用mutex类进行上锁#include
#include
#include
#include
#include
#include
#include
#include
#define MAX 5
using namespace std;
// 全局变量
int number;
mutex g_num_mutex;
// 创建一把互斥锁
// 全局变量, 多个线程共享
// 线程处理函数
void funcA_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
// 如果线程A加锁成功, 不阻塞
// 如果B加锁成功, 线程A阻塞
g_num_mutex.lock();
int cur = number;
cur++;
usleep(10);
number = cur;
g_num_mutex.unlock();
printf("Thread A, id = %lu, number = %d\n", pthread_self(), number);
}
}
void funcB_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
// a加锁成功, b线程访问这把锁的时候是锁定的
// 线程B先阻塞, a线程解锁之后阻塞解除
// 线程B加锁成功了
g_num_mutex.lock();
int cur = number;
cur++;
number = cur;
g_num_mutex.unlock();
printf("Thread B, id = %lu, number = %d\n", pthread_self(), number);
usleep(5);
}
}
int main(int argc, const char* argv[])
{
// 创建两个子线程
thread t1(funcA_num, (void *)NULL);
thread t2(funcB_num, (void *)NULL);
// 阻塞,资源回收
t1.join();
t2.join();
return 0;
}
执行结果:
Thread A, id = 140232361907968, number = 1
Thread A, id = 140232361907968, number = 2
Thread A, id = 140232361907968, number = 3
Thread A, id = 140232361907968, number = 4
Thread A, id = 140232361907968, number = 5
Thread B, id = 140232353515264, number = 6
Thread B, id = 140232353515264, number = 7
Thread B, id = 140232353515264, number = 8
Thread B, id = 140232353515264, number = 9
Thread B, id = 140232353515264, number = 10
std::lock_guard
lock_guard
是 C++11 新增的一个模板类,使用这个类,可以简化互斥锁 lock()
和 unlock()
的写法,同时也更安全。这个模板类的定义和常用的构造函数原型如下:// 类的定义,定义于头文件
template< class Mutex >
class lock_guard;
// 常用构造函数
explicit lock_guard( mutex_type& m );
lock_guard
在使用上面提供的这个构造函数构造对象时,会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记 unlock()
操作而导致线程死锁。lock_guard
使用了 RAII
技术,就是在类构造函数中分配资源,在析构函数中释放资源,保证资源出了作用域就释放。
使用 lock_guard
对上面的例子进行修改,代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX 5
using namespace std;
// 全局变量
int number;
mutex g_num_mutex;
// 创建一把互斥锁
// 全局变量, 多个线程共享
// 线程处理函数
void funcA_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
// 如果线程A加锁成功, 不阻塞
// 如果B加锁成功, 线程A阻塞
lock_guard<mutex> lock(g_num_mutex);
int cur = number;
cur++;
// usleep(10);
number = cur;
printf("Thread A, id = %lu, number = %d\n", pthread_self(), number);
}
}
void funcB_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
// a加锁成功, b线程访问这把锁的时候是锁定的
// 线程B先阻塞, a线程解锁之后阻塞解除
// 线程B加锁成功了
lock_guard<mutex> lock(g_num_mutex);
int cur = number;
cur++;
number = cur;
printf("Thread B, id = %lu, number = %d\n", pthread_self(), number);
// usleep(5);
}
}
int main(int argc, const char* argv[])
{
// 创建两个子线程
thread t1(funcA_num, (void *)NULL);
thread t2(funcB_num, (void *)NULL);
// 阻塞,资源回收
t1.join();
t2.join();
return 0;
}
执行结果:
Thread A, id = 140307365684992, number = 1
Thread A, id = 140307365684992, number = 2
Thread A, id = 140307365684992, number = 3
Thread A, id = 140307365684992, number = 4
Thread B, id = 140307357292288, number = 5
Thread B, id = 140307357292288, number = 6
Thread B, id = 140307357292288, number = 7
Thread B, id = 140307357292288, number = 8
Thread B, id = 140307357292288, number = 9
Thread A, id = 140307365684992, number = 10
通过修改发现代码被精简了,而且不用担心因为忘记解锁而造成程序的死锁,但是这种方式也有弊端,在上面的示例程序中整个for循环的体都被当做了临界区,多个线程是线性的执行临界区代码的,因此临界区越大程序效率越低,还是需要根据实际情况选择最优的解决方案。
待更新内容