【并发编程八】线程和线程同步
- 简介:
本文介绍了windows系统、linux系统、c++标准库提供的多线程实现的方法,以及线程同步的相关知识。
一、线程
进程和线程,我们在【并发编程】系列的第一篇介绍过,我们再简单介绍下。
- 进程是资源分配的基本单位,进程间的切换依赖时间中断,系统消耗较大。
- 而线程切换的成本较低。
- 线程间并发执行。
- 线程共享地址空间。
二、线程的实现
c++标准库从c++11开始引入了线程,在c++11之前需要调用系统提供的方法,在c++11后我们就可以使用c++标准库了。
1、linux系统
#include<pthread.h>
2、windows系统
#include<windows.h>
3、c++11
3.1、线程的操作
#include<thread>
操作 | 解释 |
---|---|
join | 等待线程完成其执行(公开成员函数) |
detach | 容许线程从线程句柄独立开来执行(公开成员函数) |
swap | 交换二个 thread 对象(公开成员函数) |
- join demo
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
int g_num = 0;
void foo()
{
/*cout << g_num << endl;
g_num++;*/
// simulate expensive operation
std::this_thread::sleep_for(std::chrono::seconds(1));
cout << "i am foo" << endl;
}
int main()
{
std::cout << "starting thread t1 t2...\n";
std::thread t1(foo);
std::thread t2(foo);
std::cout << "waiting for t1 to finish..." << std::endl;
t1.join();
t2.join();
std::cout << "done!\n";
}
- detch demo
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
int g_num = 0;
void foo()
{
// simulate expensive operation
std::this_thread::sleep_for(std::chrono::seconds(1));
cout << "i am foo" << endl;
}
int main()
{
std::cout << "starting thread t1 t2...\n";
std::thread t1(foo);
std::thread t2(foo);
std::cout << "not waiting for t1 to finish..." << std::endl;
t1.detach();
t2.detach();
std::cout << "thread t1 t2 done!\n";
// wait for t1 t2,不在此等待的话,主线程执行完毕整个进程就结束了,就不会等待线程t1 t2运行了
std::this_thread::sleep_for(std::chrono::seconds(2));
}
说明
- join会等待线程执行完毕,
- detch不会等待线程执行完毕,
- 红色框中的是量endl,线程是共享io的,在一个线程t1的cout输出文字i am foo后 另外一个线程cout就会立刻输出文字 i am foo后 。不会等待endl的执行后再执行另外一个线程。
3.2、管理当前线程的函数
定义于命名空间 this_thread
操作 | 解释 |
---|---|
yield(C++11) | 建议实现重新调度各执行线程(函数) |
get_id(C++11) | 返回当前线程的线程 id(函数) |
sleep_for (C++11) | 使当前线程的执行停止指定的时间段(函数) |
sleep_until(C++11) | 使当前线程的执行停止直到指定的时间点(函数) |
我重点说下yield这个函数。
比如说你的线程需要等待某个操作完成,如果你直接用一个循环不断判断这个操作是否完成就会使得这个线程占满CPU时间,这会造成资源浪费。这时候你可以判断一次操作是否完成,如果没有完成就调用yield交出时间片,过一会儿再来判断是否完成,这样这个线程占用CPU时间会大大减少。
从以下demo可用看到,如果线程t1交出了时间片,则线程t2就可用多输出“我爱中国”,反之,如果线程t1不交出时间片的话,线程t2就输出的我爱中国就会减少。
我们知道,进程的时间片一定,线程分的是进程的时间片,有的线程占用的多了,其他线程占用的相应的就会减少。反之亦然。
- not use yield demo
#include <iostream>
#include <chrono>
#include <thread>
using namespace std;
// "busy sleep" while suggesting that other threads run
// for a small amount of time
void little_sleep(std::chrono::microseconds us)
{
int i = 1;
auto start = std::chrono::high_resolution_clock::now();
auto end = start + us;
do {
//std::this_thread::yield();
cout << i<< endl;
i++;
} while (std::chrono::high_resolution_clock::now() < end);
}
void busy()
{
auto start = std::chrono::high_resolution_clock::now();
auto end = start + std::chrono::microseconds(1000);
do {
std::this_thread::yield();
cout << "我爱中国" << endl;
} while (std::chrono::high_resolution_clock::now() < end);
}
int main()
{
auto start = std::chrono::high_resolution_clock::now();
//little_sleep(std::chrono::microseconds(100));
std::thread t1(little_sleep, std::chrono::microseconds(1000));
std::thread t2(busy);
auto elapsed = std::chrono::high_resolution_clock::now() - start;
std::cout << "waited for "
<< std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count()
<< " microseconds\n";
t1.join();
t2.join();
}
- use yield demo
#include <iostream>
#include <chrono>
#include <thread>
using namespace std;
// "busy sleep" while suggesting that other threads run
// for a small amount of time
void little_sleep(std::chrono::microseconds us)
{
int i = 1;
auto start = std::chrono::high_resolution_clock::now();
auto end = start + us;
do {
std::this_thread::yield();
cout << i<< endl;
i++;
} while (std::chrono::high_resolution_clock::now() < end);
}
void busy()
{
auto start = std::chrono::high_resolution_clock::now();
auto end = start + std::chrono::microseconds(1000);
do {
std::this_thread::yield();
cout << "我爱中国" << endl;
} while (std::chrono::high_resolution_clock::now() < end);
}
int main()
{
auto start = std::chrono::high_resolution_clock::now();
//little_sleep(std::chrono::microseconds(100));
std::thread t1(little_sleep, std::chrono::microseconds(1000));
std::thread t2(busy);
auto elapsed = std::chrono::high_resolution_clock::now() - start;
std::cout << "waited for "
<< std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count()
<< " microseconds\n";
t1.join();
t2.join();
}
三、线程同步
1、linux系统
1.1、互斥体(mutual exclusive、Mutex)
- linux下的互斥体和windows下的临界区的用法很类似,一般也是通过限制多个线程同时执行某段代码来保护资源的。
#include<pthread.h>
int main()
{
pthread_mutex_t mymutex;//定义互斥体
pthread_mutex_init(&mymutex, NULL);// 初始化
int ret = pthread_mutex_lock(&mymutex);// 锁定,
ret = pthread_mutex_unlock(&mymutex);// 解锁
ret = pthread_mutex_destroy(&mymutex);// 销毁
}
1.2、信号量(semapphore)
- 我们在进程通信中介绍过了。这里简单介绍下,具体实现可以查看之前的文章。(linux和windows原理一样的,区别是函数名称不同)【并发编程六】c++进程通信——信号量(semaphore)
- 资源有多分,可以同时被多个线程访问。
1.3、条件变量(condition variable)
- 原理参见【并发编程十】c++线程同步——条件变量(condition_variable)。
- 相关接口
pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t * attr); //初始化一个条件变量
pthread_cond_destroy(pthread_cond_t *cond); // 销毁一个条件变量
pthread_cond_signal(pthread_cond_t *cond); // 唤醒至少一个阻塞在条件变量上的线程
pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒全部阻塞在条件变量上的线程
1.4、读写锁
- 如果使用互斥体完全阻止读请求并发,则会造成性能损失。
- 读请求之前无须同步,他们之前的并发访问是安全的。
- 写请求必须锁住读请求和其他写请求。
- 初始化读写锁
pthread_rwlock_t myRWLock = PTHREAD_RWLOCK_INITIALIZER;
//还可以借助 pthread_rwlock_init() 函数初始化读写锁,此函数的语法格式为:
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
- 线程发出“读锁”请求
//通过以下两个函数,线程可以向读写锁发出“读锁”请求:
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
//其中,rwlock 参数指的是初始化好的读写锁。
//当读写锁处于“无锁”或者“读锁”状态时,以上两个函数都能成功获得读锁;当读写锁处于“写锁”状态时:
//pthread_rwlock_rdlock() 函数会阻塞当前线程,直至读写锁被释放;
//pthread_rwlock_tryrdlock() 函数不会阻塞当前线程,直接返回 EBUSY。
- 线程发出“写锁”请求
//通过以下两个函数,线程可以向读写锁发出“写锁”请求:
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
- 释放读写锁
//无论是处于“无锁”、“读锁”还是“写锁”的读写锁,都可以使用如下函数释放读写锁:
int pthread_rwlock_unlock (pthread_rwlock_t* rwlock);
- 销毁读写锁
//当读写锁不再使用时,我们可以借助如下函数将它销毁:
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);
2、windows系统
2.1、临界区(critical section)
- 同linux下的互斥体。
//创建:
CRITICAL_SECTION my_winsec;//创建windows中的临界区,类似与互斥量,使用前必须初始化
//初始化:(通常在类构造函数中初始化)
InitializeCriticalSection(&my_winsec);//初始化临界区
//临界区使用:
EnterCriticalSection(&my_winsec);//进入临界区(加锁)
myQueue.push_back(i);
LeaveCriticalSection(&my_winsec);//离开临界区(解锁)
2.2、信号量(semapphore)
- 同linux
2.3、条件变量(condition variable)
- 同linux
- 使用条件变量,需要配合临界区或者读写锁。
//等待
BOOL SleepConditionVariableCS(
[in, out] PCONDITION_VARIABLE ConditionVariable,
[in, out] PCRITICAL_SECTION CriticalSection,
[in] DWORD dwMilliseconds
);
BOOL SleepConditionVariableSRW(
[in, out] PCONDITION_VARIABLE ConditionVariable,
[in, out] PSRWLOCK SRWLock,
[in] DWORD dwMilliseconds,
[in] ULONG Flags
);
//唤醒
WakeAllConditionVariable();
WakeConditionVariable();
2.4、读写锁
- 同linux
//定义读写锁
RTL_SRWLOCK rdlock;
//初始化读写锁
VOID WINAPI InitializeSRWLock( _Out_ PSRWLOCK SRWLock );
//共享模式
//申请读锁
VOID WINAPI AcquireSRWLockShared(_Inout_ PSRWLOCK SRWLock);
//释放读锁
VOID WINAPI ReleaseSRWLockShared(_Inout_ PSRWLOCK SRWLock);
//排他模式
//申请写锁
VOID WINAPI AcquireSRWLockExclusive(_Inout_ PSRWLOCK SRWLock);
//释放写锁
VOID WINAPI ReleaseSRWLockExclusive(_Inout_ PSRWLOCK SRWLock);
//
2.5、互斥体(mutual exclusive、Mutex)
- 同linux下的互斥体,区别,对象在同一时刻最多只能属于1。
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全属性结构指针
BOOL bInitialOwner, //是否占有该互斥量
LPCTSTR lpName //设置互斥对象的名字
);
2.6、Event对象
本节讨论的event对象,不是windows ui事件驱动机制中的事件,而是多线程同步中的event对象,也是windows的内核对象之一。
//CreateEvent是创建windows事件的意思,作用主要用在判断线程退出,线程锁定方面.
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性 BOOL bManualReset, // 复位方式
BOOL bInitialState, // 初始状态
LPCTSTR lpName // 对象名称
);
//将指定的事件对象设置为信号状态。
BOOL SetEvent(
[in] HANDLE hEvent
);
//将指定的事件对象设置为非对齐状态。
BOOL ResetEvent(
[in] HANDLE hEvent
);
3、进程同步和线程同步
我们有篇文章介绍过windows和linux进程通信的区别,【并发编程四】windows进程通信和Linux进程通信,在此我们继续做个补充
- linux:
信号量(进程和线程同步都可以) - windows:
信号量、事件、互斥体(mutex对象),只要是创建的有名字的,不单单可以线程通信,还可以进程同步。
4、c++11/c++14/c++17/c++20
在c++中直接使用操作系统提供的多线程api系统函数,虽然限制少,但是同样的代码不可以同时兼容windows和linux两个平台,对跨平台开发还是有很多的不方便。
在c++11开始,c++标准库就提供了很多线程同步的对象,接下来我准备单独分出一篇文章来,一个个来详细的介绍。
4.1、互斥
4.2、条件变量
4.3、future
4.3、信号量
参考:
1、https://www.apiref.com/cpp-zh/cpp/thread.html
2、https://en.cppreference.com/w/cpp/thread
3、书籍《c++服务器开发精髓》——张远龙