Thread

Thread

C++11 提供了线程开发模块,此前都使用pthread,一种C语言的解决方案。但是pthread的使用极其繁琐,导致初初使用pthread在赛场上搞了几个小时还是传不对参数,结果也拿不出来。thread的出现大大的简化了多线程编程。

Thread 的基本用法 #


#include<iostream>
#include<thread> 

using namespace std;


void say_hi(string s) {
    cout << "Hello: " << s << endl;
    return ;
}

int main() {

    string str = "Paladnix";

    thread thr(say_hi, str); // 创建线程thr并启动。传入待执行的函数和参数。

    cout << "Others" << endl;

    thr.join(); // 阻塞主函数直至thr线程结束退出。
    return 0;
}

如果主函数没有调用join(), 则主函数可能在thr线程之前结束,会强制结束子线程。输出的结果如下:

Others
libc++abi: terminating
Hello: Paladnix
[1]    39257 abort      ./a.out

也可以使用detach() 函数,将thr函数放到后台运行,与主函数的进程解绑。缺点就是thr的输出结果不会再出现在主进程的终端上。

传入成员函数 #

除了可以使用普通函数作为线程的启动函数,也可以用类的成员函数来构造线程。


class Person {
    string name;
    int age;
public:

    Person() {}
    Person(string name, int age):name(name), age(age){}

    void say_hi(string hi) {
        cout << hi << ' ' << name << endl;
        cout << __LINE__ << ": " << this_thread::get_id() << endl;
    }
}; 


void test_thread_2() {
    Person a("Paladnix", 18);
    thread thr(&Person::say_hi, a, "Hi");

    thr.join();
    cout << __LINE__ << ": " << this_thread::get_id() << endl;
}

Mutex 锁 #

多个线程共享一个变量时,往往需要对资源进行加锁。Mutex是一种互斥锁。


#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex

volatile int counter(0); // non-atomic counter
std::mutex mtx;           // locks access to counter

void attempt_10k_increases() {
    for (int i=0; i<10000; ++i) {
        if (mtx.try_lock()) {   // only increase if currently not locked:
            ++counter;
            mtx.unlock();
        }
    }
}

int main (int argc, const char* argv[]) {
    std::thread threads[10];
    for (int i=0; i<10; ++i)
        threads[i] = std::thread(attempt_10k_increases);

    for (auto& th : threads) th.join();
    std::cout << counter << " successful increases of the counter.\n";

    return 0;
}

上面的代码展示了Mutex的一个示例用法,最后统计出++counter的操作一共进行了5321次,所以可以知道try_lock()函数是非阻塞的。

time_mutex锁 #

与mutex锁不同,提供了一个try_lock_for() 函数可以指定阻塞的时间,如果超时还未获得锁则返回false。 try_lock_until() 函数支持指定一个时间点,在指定时间点之前保持阻塞。

std::lock_guard #

A lock guard is an object that manages a mutex object by keeping it always locked. On construction, the mutex object is locked by the calling thread, and on destruction, the mutex is unlocked. It is the simplest lock, and is specially useful as an object with automatic duration that lasts until the end of its context. In this way, it guarantees the mutex object is properly unlocked in case an exception is thrown.

构造函数时加锁,析构/异常捕获时释放锁。这是C++的基础机制RAII的利用。


// lock_guard example
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock_guard
#include <stdexcept>      // std::logic_error

std::mutex mtx;

void print_even (int x) {
  if (x%2==0) std::cout << x << " is even\n";
  else throw (std::logic_error("not even"));
}

void print_thread_id (int id) {
  try {
    // using a local lock_guard to lock mtx guarantees unlocking on destruction / exception:
    std::lock_guard<std::mutex> lck (mtx);
    print_even(id);
  }
  catch (std::logic_error&) {
    std::cout << "[exception caught]\n";
  }
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(print_thread_id,i+1);

  for (auto& th : threads) th.join();

  return 0;
}

std::unique_lock #

一样使用了RAII机制,利用析构函数自动释放锁。

更方便的是,不需要try_catch 来保证析构函数的调用了。

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::unique_lock

std::mutex mtx;           // mutex for critical section

void print_block (int n, char c) {
    // critical section (exclusive access to std::cout signaled by lifetime of lck):
    std::unique_lock<std::mutex> lck (mtx);
    for (int i=0; i<n; ++i) {
        std::cout << c;
    }
    std::cout << '\n';
}

int main ()
{
    std::thread th1 (print_block,50,'*');
    std::thread th2 (print_block,50,'$');

    th1.join();
    th2.join();

    return 0;
}

条件变量:condition_variable #

多线程中,线程之间的同步可以借助condition_variable来实现, 在线程A中,使用condition_variable的wait方法将线程阻塞,直到线程B调用notify方法将线程A唤醒,线程A继续进行。

一个典型的生产-消费模型:


#include<iostream>
#include<thread>
#include<mutex>
#include<atomic>
#include<queue>
#include<chrono>
#include<condition_variable>

int main()
{
  std::queue<int> production;
  std::mutex mtx;
  std::condition_variable cv;
  bool ready = false;  // 是否有产品可供消费
  bool done = false;   // 生产结束

  std::thread producer(
    [&] () -> void {
      for (int i = 1; i < 10; ++i)
      {
        // 模拟实际生产过程
        std::this_thread ::sleep_for(std::chrono::milliseconds(10));
        std::cout << "producing " << i << std::endl;

        std::unique_lock<std::mutex> lock(mtx);
        production.push(i);

        // 有产品可以消费了
        ready = true;
        cv.notify_one();
      }
      // 生产结束了
      done = true;
    }
  );

  std::thread consumer(
    [&] () -> void {
      std::unique_lock<std::mutex> lock(mtx);
      // 如果生成没有结束或者队列中还有产品没有消费,则继续消费,否则结束消费
      while(!done || !production.empty())
      {
        // 防止误唤醒
        while(!ready)
        {
          cv.wait(lock);
        }

        while(!production.empty())
        {
          // 模拟消费过程
          std::cout << "consuming " << production.front() << std::endl;
          production.pop();
        }

        // 没有产品了
        ready = false;
      }
    }
  );

  producer.join();
  consumer.join();

  return 0;
}

其中在使用的时候尤其要注意wait函数需要配合一个标注变量,并用while循环。这是为了避免线程被虚假唤醒。例如存在线程B和C都在wait, 而线程A唤醒B线程的时候,C线程也被唤醒了,并且以更快的速度消耗了A产生的数据,那么B继续执行则会发现并没有数据。