【面試】如果把線程當作一個人來對待,所有問題都瞬間明白了

多線程的問題都曾經困擾過每個開發人員,今天將從全新視角來解說,希望讀者都能明白。

強烈建議去運行下文章中的示例代碼,自己體會下。



問題究竟出在哪裡?

一個線程執行,固然是安全的,但是有時太慢了,怎麼辦?

老祖宗告訴我們,“一方有難,八方支援”,那不就是多叫幾個線程來幫忙嘛,好辦呀,多new幾個不就行了,又不要錢。這樣能管用嗎?繼續往下看。

俗話說,“在家靠父母,出門靠朋友”。有了朋友的幫助,就會事半功倍。是這樣的嗎?

不一定,如果朋友“不靠譜”,結果竟是在“添亂”。於是就演變為,“不怕神一樣的對手,就怕豬一樣的隊友”。可見“人多力量大”縱然是對的,但也要配合好才能成事。

人和人是朋友,那線程和線程也是“朋友”,如果多線程之間不能配合好的話,最終也會變為“豬一樣的隊友”。事實證明,這也不是一件易事。且容我慢慢道來。

開發是一門技術,管理是一門藝術。也許你正想帶着兄弟們大幹一場,可偏偏就有人要辭職。或者你付出了這麼多,但別人從來沒有感動過。為什麼會這樣呢?

因為你面對的是人。每個人都是獨立的個體,有思想,有靈魂,有情感,有三觀。能夠接受外界的“輸入”,經過“處理”后,能夠產生“輸出”。

說白了就是會自主的分析問題,並做出決定。這叫什麼呢?答案就是,主觀能動性。

擁有主觀能動性的物體(比如人),你需要和它協商着或配合著來共同完成一件事情,而不能“強迫”它去做什麼,因為這樣往往不會有好的結果。

費了這麼多口舌,就是希望把問題盡量的簡單化。終於可以回到程序了,那線程的情況是不是類似的呢?答案是肯定的。

一個線程準備好后,經過CPU的調度,就可以自主的運行了。此時它儼然成了一個獨立的個體,且具有主觀能動性。

這本是一件好事,但卻也有不好的一面,那就是你對它的“掌控”能力變弱了,頗有一種“將在外,君命有所不受”的感覺。

可能你不同意這種看法,說我可以“強迫”它停止運行,調用Thread類的stop()方法來直接把它“掐死”,不好意思,該方法已廢棄。

因為線程可能在運行一些“關鍵”代碼(比如轉賬),此刻不能被終止。Thread類還有一些其它的方法也都廢棄了,大抵原因其實都差不多。

講了這麼多,相信你已經明白了,簡單總結一下:

事情起因:線程可以獨立自主的運行,可以認為它具有主觀能動性。

造成結果:對它的掌控能力變弱了,而且又不能直接把它“幹掉”。

解決方案:凡事商量着來,互相配合著把事情完成。

作者觀點:其實就是把線程當作人來對待。



小試牛刀一下

一旦把線程當成人,就來到了人類的世界,這我們太熟悉了,所以很多問題都會變得非常簡單明了。一起來看看吧。

場景一,停止

“大胖,大胖,12點了,該去吃飯了,別寫了”

“好的,好的,稍等片刻,把這幾行代碼寫完就走”

要點:把停止的信號傳達給別人,別人處理完手頭的事情就自己主動停止了。

 static void stopByFlag() {
    ARunnable ar = new ARunnable();
    new Thread(ar).start();
    ar.tellToStop();
  }

  static class ARunnable implements Runnable {

    volatile boolean stop;

    void tellToStop() {
      stop = true;
    }

    @Override
    public void run() {
      println("進入不可停止區域 1。。。");
      doingLongTime(5);
      println("退出不可停止區域 1。。。");
      println("檢測標誌stop = %s", String.valueOf(stop));
      if (stop) {
        println("停止執行");
        return;
      }
      println("進入不可停止區域 2。。。");
      doingLongTime(5);
      println("退出不可停止區域 2。。。");
    }

  }

 

解說:線程在預設的地點檢測flag,來決定是否停止。


場景二,暫停/恢復

“大胖,大胖,先別發請求了,對方服務器快掛了”

“好的,好的,等這個執行完就不發了”

過了一會

“大胖,大胖,可以重新發請求了”

“好的,好的”

要點:把暫停的信號傳達給別人,別人處理完手頭的事情就自己主動暫停了。但是恢復是無法自主進行的,只能由操作系統來恢複線程的執行。

 

static void pauseByFlag() {
    BRunnable br = new BRunnable();
    new Thread(br).start();
    br.tellToPause();
    sleep(8);
    br.tellToResume();
  }

  static class BRunnable implements Runnable {

    volatile boolean pause;

    void tellToPause() {
      pause = true;
    }

    void tellToResume() {
      synchronized (this) {
        this.notify();
      }
    }

    @Override
    public void run() {
      println("進入不可暫停區域 1。。。");
      doingLongTime(5);
      println("退出不可暫停區域 1。。。");
      println("檢測標誌pause = %s", String.valueOf(pause));
      if (pause) {
        println("暫停執行");
        try {
          synchronized (this) {
            this.wait();
          }
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        println("恢復執行");
      }
      println("進入不可暫停區域 2。。。");
      doingLongTime(5);
      println("退出不可暫停區域 2。。。");
    }

  }

解說:還是在預設的地點檢測flag。然後就是wait/notify配合使用。


場景三,插隊

“大胖,大胖,讓我站到你前面,不想排隊了”

“好吧”

要點:別人插隊到你前面,必須等他完事後才輪到你。

static void jqByJoin() {
    CRunnable cr = new CRunnable();
    Thread t = new Thread(cr);
    t.start();
    sleep(1);
    try {
      t.join();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    println("終於輪到我了");
  }

  static class CRunnable implements Runnable {

    @Override
    public void run() {
      println("進入不可暫停區域 1。。。");
      doingLongTime(5);
      println("退出不可暫停區域 1。。。");
    }

  }

 

解說:join方法可以讓某個線程插到自己前面,等它執行完,自己才會繼續執行。


場景四,叫醒

“大胖,大胖,醒醒,醒醒,看誰來了”

“誰啊,我去”

要點:要把別人從睡夢中叫醒,一定要採取稍微暴力一點的手段。

static void stopByInterrupt() {
    DRunnable dr = new DRunnable();
    Thread t = new Thread(dr);
    t.start();
    sleep(2);
    t.interrupt();
  }

  static class DRunnable implements Runnable {

    @Override
    public void run() {
      println("進入暫停。。。");
      try {
        sleep2(5);
      } catch (InterruptedException e) {
        println("收到中斷異常。。。");
        println("做一些相關處理。。。");
      }
      println("繼續執行或選擇退出。。。");
    }

  }

 

解說:線程在sleep或wait時,是處於無法交互的狀態的,此時只能使用interrupt方法中斷它,線程會被激活並收到中斷異常。



常見的協作配合

上面那些場景,其實都是對一個線程的操作,下面來看多線程間的一些配合。

事件一,考試

假設今天考試,20個學生,1個監考老師。規定學生可以提前交卷,即把卷子留下,直接走人就行了。

但老師必須等到所有的學生都走後,才可以收卷子,然後裝訂打包。

如果把學生和老師都看作線程,就是1個線程和20個線程的配合問題,即等20個線程都結束了,這1個線程才開始。

比如20個線程分別在計算數據,等它們都結束后得到20个中間結果,最後這1個線程再進行後續匯總、處理等。

  static final int COUNT = 20;
  static CountDownLatch cdl = new CountDownLatch(COUNT);

  public static void main(String[] args) throws Exception {
    new Thread(new Teacher(cdl)).start();
    sleep(1);
    for (int i = 0; i < COUNT; i++) {
      new Thread(new Student(i, cdl)).start();
    }
    synchronized (ThreadCo1.class) {
      ThreadCo1.class.wait();
    }
  }

  static class Teacher implements Runnable {

    CountDownLatch cdl;

    Teacher(CountDownLatch cdl) {
      this.cdl = cdl;
    }

    @Override
    public void run() {
      println("老師髮捲子。。。");
      try {
        cdl.await();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      println("老師收卷子。。。");
    }

  }

  static class Student implements Runnable {

    CountDownLatch cdl;
    int num;

    Student(int num, CountDownLatch cdl) {
      this.num = num;
      this.cdl = cdl;
    }

    @Override
    public void run() {
      println("學生(%d)寫卷子。。。"num);
      doingLongTime();
      println("學生(%d)交卷子。。。"num);
      cdl.countDown();
    }

  }

 

解說:每完成一個線程,計數器減1,當減到0時,被阻塞的線程自動執行。


事件二,旅遊

最近景色宜人,公司組織去登山,大夥都來到了山腳下,登山過程自由進行。

但為了在特定的地點拍集體照,規定1個小時后在半山腰集合,誰最後到的,要給大家表演一個節目。

然後繼續登山,在2個小時后,在山頂集合拍照,還是誰最後到的表演節目。

接着開始下山了,在2個小時后在山腳下集合,點名回家,最後到的照例表演節目。

  static final int COUNT = 5;
  static CyclicBarrier cb = new CyclicBarrier(COUNT, new Singer());

  public static void main(String[] args) throws Exception {
    for (int i = 0; i < COUNT; i++) {
      new Thread(new Staff(i, cb)).start();
    }
    synchronized (ThreadCo2.class) {
      ThreadCo2.class.wait();
    }
  }

  static class Singer implements Runnable {

    @Override
    public void run() {
      println("為大家唱歌。。。");
    }

  }

  static class Staff implements Runnable {

    CyclicBarrier cb;
    int num

    Staff(int num, CyclicBarrier cb) {
      this.num = num;
      this.cb = cb;
    }

    @Override
    public void run() {
      println("員工(%d)出發。。。"num);
      doingLongTime();
      println("員工(%d)到達地點一。。。"num);
      try {
        cb.await();
      } catch (Exception e) {
        e.printStackTrace();
      }
      println("員工(%d)再出發。。。"num);
      doingLongTime();
      println("員工(%d)到達地點二。。。"num);
      try {
        cb.await();
      } catch (Exception e) {
        e.printStackTrace();
      }
      println("員工(%d)再出發。。。"num);
      doingLongTime();
      println("員工(%d)到達地點三。。。"num);
      try {
        cb.await();
      } catch (Exception e) {
        e.printStackTrace();
      }
      println("員工(%d)結束。。。"num);
    }

  }

 

解說:某個線程到達預設點時就在此等待,等所有的線程都到達時,大家再一起向下個預設點出發。如此循環反覆下去。


事件三,勞動

大胖和小白去了創業公司,公司為了節約開支,沒有請專門的保潔人員。讓員工自己掃地和擦桌。

大胖覺得擦桌輕鬆,就讓小白去掃地。可小白覺得掃地太累,也想擦桌。

為了公平起見,於是決定,每人先干一半,然後交換工具,再接着干對方剩下的那一個半。

  static Exchanger<Tool> ex = new Exchanger<>();

  public static void main(String[] args) throws Exception {
    new Thread(new Staff("大胖"new Tool("笤帚""掃地"), ex)).start();
    new Thread(new Staff("小白"new Tool("抹布""擦桌"), ex)).start();
    synchronized (ThreadCo3.class) {
      ThreadCo3.class.wait();
    }
  }

  static class Staff implements Runnable {

    String name;
    Tool tool;
    Exchanger<Tool> ex;

    Staff(String name, Tool tool, Exchanger<Tool> ex) {
      this.name = name;
      this.tool = tool;
      this.ex = ex;
    }

    @Override
    public void run() {
      println("%s拿的工具是[%s],他開始[%s]。。。", name, tool.name, tool.work);
      doingLongTime();
      println("%s開始交換工具。。。", name);
      try {
        tool = ex.exchange(tool);
      } catch (Exception e) {
        e.printStackTrace();
      }

      println("%s的工具變為[%s],他開始[%s]。。。", name, tool.name, tool.work);
    }

  }

  static class Tool {

    String name;
    String work;

    Tool(String name, String work) {
      this.name = name;
      this.work = work;
    }

  }

 

解說:兩個線程在預設點交換變量,先到達的等待對方。


事件四,魔性遊戲

這是一個充滿魔性的小遊戲,由一個團隊一起參加。所有人每隔5秒鐘抽一次簽,每個人有50%的概率留下來或被淘汰。

留下來的人下次抽籤時同樣有50%的概率被淘汰。被淘汰的人下次抽籤時同樣有50%的概率復活。

團隊所有成員都被淘汰完,為挑戰失敗,團隊所有成員都回到遊戲中(除剛開始外),為挑戰成功。

比如一開始10人參與遊戲,第一輪抽籤后,6人留下,4人淘汰。

第二輪抽籤后,留下的6人中4人被淘汰,淘汰的4人中2人復活,那麼目前是4人在遊戲中,6人被淘汰。

一直如此繼續下去,直到10人全部被淘汰,或全部回到遊戲中。

可見,人數越多,全部被淘汰的概率越小,但全部回到遊戲中的概率也越小。

反之,人數越少,全部回到遊戲中的概率越大,但全部被淘汰的概率也越大。

是不是很有魔性啊。哈哈。

  static final int COUNT = 6;
  static Phaser ph = new Phaser() {
    protected boolean onAdvance(int phase, int registeredParties) {
      println2("第(%d)局,剩餘[%d]人", phase, registeredParties);
      return registeredParties == 0 ||
          (phase != 0 && registeredParties == COUNT);
    };
  };

  public static void main(String[] args) throws Exception {
    new Thread(new Challenger("張三")).start();
    new Thread(new Challenger("李四")).start();
    new Thread(new Challenger("王五")).start();
    new Thread(new Challenger("趙六")).start();
    new Thread(new Challenger("大胖")).start();
    new Thread(new Challenger("小白")).start();
    synchronized (ThreadCo4.class) {
      ThreadCo4.class.wait();
    }
  }

  static class Challenger implements Runnable {

    String name;
    int state;

    Challenger(String name) {
      this.name = name;
      this.state = 0;
    }

    @Override
    public void run() {
      println("[%s]開始挑戰。。。", name);
      ph.register();
      int phase = 0;
      int h;
      while (!ph.isTerminated() && phase < 100) {
        doingLongTime(5);
        if (state == 0) {
          if (Decide.goon()) {
            h = ph.arriveAndAwaitAdvance();
            if (h < 0)
              println("No%d.[%s]繼續,但已勝利。。。", phase, name);
            else
              println("No%d.[%s]繼續at(%d)。。。", phase, name, h);
          } else {
            state = -1;
            h = ph.arriveAndDeregister();
            println("No%d.[%s]退出at(%d)。。。", phase, name, h);
          }
        } else {
          if (Decide.revive()) {
            state = 0;
            h = ph.register();
            if (h < 0)
              println("No%d.[%s]復活,但已失敗。。。", phase, name);
            else
              println("No%d.[%s]復活at(%d)。。。", phase, name, h);
          } else {
            println("No%d.[%s]沒有復活。。。", phase, name);
          }
        }
        phase++;
      }
      if (state == 0) {
        ph.arriveAndDeregister();
      }
      println("[%s]結束。。。", name);
    }

  }

  static class Decide {

    static boolean goon() {
      return random(9) > 4;
    }

    static boolean revive() {
      return random(9) < 5;
    }
  }

 

解說:某個線程到達預設點后,可以選擇等待同伴或自己退出,等大家都到達后,再一起向下一個預設點出發,隨時都可以有新的線程加入,退出的也可以再次加入。



生產與銷售的問題

在現實中,工廠生產出來的產品會先放到倉庫存儲,銷售人員簽了單子后,會從倉庫把產品發給客戶。

如果生產的過快,倉庫里產品越堆越多,直到把倉庫堆滿,那就必須停止生產,因為沒地方放了。

此時只能讓銷售人員趕緊出去簽單子,把產品發出去,倉庫就有了空間,可以恢復生產了。

如果銷售的過快,倉庫里產品越來越少,直到把倉庫清空,那就必須停止銷售,因為沒產品了。

此時只能讓生產人員趕緊生產產品,把產品放到倉庫里,倉庫里就有了產品,可以恢復銷售了。

可能會有人問,為什麼不讓生產和銷售直接掛鈎呢,把倉庫這個環節去掉?

這樣會造成兩種不好的情況:

一是突然來了很多單子,生產人員累成死Dog也生產不出來。

二是很長時間沒有單子,生產人員閑成廢Dog也無事可做。

用稍微“專業”點的術語就是此時的生產和銷售是一種強耦合的關係,銷售的波動對生產影響太大。

倉庫就是一個緩衝區,能有效的吸收波動,很大程度上減少波動的傳遞,起到一種解耦作用,由強耦合變成一種鬆散耦合。

這其實就對應計算機里經典的生產者和消費者問題。

經典的生產者和消費者

一到多個線程充當生產者,生產元素。一到多個線程充當消費者,消費元素。

在兩者之間插入一個隊列(Queue)充當緩衝區,建立起生產者和消費者的鬆散耦合。

正常情況下,即生產元素的速度和消費元素的速度差不多時,生產者和消費者其實是不需要去關注對方的。

生產者可以一直生產,因為隊列里總是有空間。消費者可以一直消費,因為隊列里總是有元素。即達到一個動態的平衡。

但在特殊情況下,比如生產元素的速度很快,隊列里沒有了空間,此時生產者必須自我“ba工”,開始“睡大覺”。

一旦消費者消費了元素之後,隊列里才會有空間,生產者才可以重啟生產,所以,消費者在消費完元素後有義務去叫醒生產者復工。

更準確的說法應該是,只有在生產者“睡大覺”時,消費者消費完元素后才需要去叫醒生產者。否則,其實可以不用叫醒,因為人家本來就沒睡。

反之,如果消費元素的速度很快,隊列里沒有了元素,只需把上述情況顛倒過來即可。

但這樣的話就會引入一個新的問題,就是要能夠準備的判斷出對方有沒有在睡大覺,為此就必須定義一個狀態變量,在自己即將開始睡大覺時,自己設置下這個變量。

對方通過檢測這個變量,來決定是否進行叫醒操作。當自己被叫醒后,首先要做的就是清除一下這個變量,表明我已經醒來複工了。

這樣就需要多維護一個變量和多了一部分判斷邏輯。可能有些人會覺得可以通過判斷隊列的“空”或“滿”(即隊列中的元素數目)來決定是否進行叫醒操作。

在高併發下,可能剛剛判斷隊列不為空,瞬間之後隊列可能已經變為空的了,這樣會導致邏輯出錯。線程可能永遠無法被叫醒。

因此,綜合所有,生產者每生產一個元素后,都會通知消費者,“現在有元素的,你可以消費”。

同樣,消費者每消費一個元素后,也會通知生產者,“現在有空間的,你可以生產”。

很明顯,這些通知很多時候(即對方沒有睡大覺時)是沒有真正意義的,不過無所謂,只要忽略它們就行了。

就是“寧可錯殺一千,也不放過一個”。首先要保證是正確的,然後才有資格去BB別的。

  public static void main(String[] args) {
    Queue queue = new Queue();
    new Thread(new Producer(queue)).start();
    new Thread(new Producer(queue)).start();
    new Thread(new Consumer(queue)).start();
  }

  static class Producer implements Runnable {

    Queue queue;

    Producer(Queue queue) {
      this.queue = queue;
    }

    @Override
    public void run() 
{
      try {
        for (int i = 0; i < 10000; i++) {
          doingLongTime();
          queue.putEle(random(10000));
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
    }

  }

  static class Consumer implements Runnable {

    Queue queue;

    Consumer(Queue queue) {
      this.queue = queue;
    }

    @Override
    public void run() 
{
      try {
        for (int i = 0; i < 10000; i++) {
          doingLongTime();
          queue.takeEle();
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
    }

  }

  static class Queue {
    Lock lock = new ReentrantLock();
    Condition prodCond  = lock.newCondition();
    Condition consCond = lock.newCondition();

    final int CAPACITY = 10;
    Object[] container = new Object[CAPACITY];
    int count = 0;
    int putIndex = 0;
    int takeIndex = 0;

    public void putEle(Object ele) throws InterruptedException {
      try {
        lock.lock();
        while (count == CAPACITY) {
          println("隊列已滿:%d,生產者開始睡大覺。。。", count);
          prodCond.await();
        }
        container[putIndex] = ele;
        println("生產元素:%d", ele);
        putIndex++;
        if (putIndex >= CAPACITY) {
          putIndex = 0;
        }
        count++;
        println("通知消費者去消費。。。");
        consCond.signalAll();
      } finally {
        lock.unlock();
      }
    }

    public Object takeEle() throws InterruptedException {
      try {
        lock.lock();
        while (count == 0) {
          println("隊列已空:%d,消費者開始睡大覺。。。", count);
          consCond.await();
        }
        Object ele = container[takeIndex];
        println("消費元素:%d", ele);
        takeIndex++;
        if (takeIndex >= CAPACITY) {
          takeIndex = 0;
        }
        count--;
        println("通知生產者去生產。。。");
        prodCond.signalAll();
        return ele;
      } finally {
        lock.unlock();
      }
    }
  }

 

解說:其實就是對await/signalAll的應用,幾乎面試必問。


源代碼

https://github.com/coding-new-talking/java-code-demo.git

 

 

(END)

 

作者是工作超過10年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。下面是公眾號和知識星球的二維碼,歡迎關注!

       

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

超級簡單的跨平台高性能音視頻播放框架QtAv編譯指南

目錄

  • 一、了解QtAv
  • 二、相關文章
  • 三、下載QtAv源碼
  • 四、下載QtAv依賴庫
  • 五、設置環境變量
    • 1、gcc設置方式
    • 2、msvc(cl)設置方式
  • 六、編譯
  • 七、測試

一、了解QtAv

這幾天抱着試一試的心態,嘗試着了解了下QtAv這個庫,感覺確實挺不錯的,因此就打算學習下這個庫。

斷斷續續的看了不少文章,大多數都是通過百度搜索出來的文章。說實話百度上大多數文章內容都差不多,而且很少有文章說清楚了編譯時的環境配置和編譯器上的區別,導致我自己也一度認為這個庫很難編譯。其實真的不難

網上的資源真的很多,但是有點兒雜亂,新手上來一看可能很容易就懵逼了。可是我這裏要告訴大家,真的不需要害怕,這個庫的編譯真的炒雞簡單,不信看我第三小節開始的編譯步驟,簡單到不敢相信。

因為我看到了windows編譯qtav這篇文章,文章中清楚的說明了環境變量配置是需要根據編譯器進行選擇設置的,這時自己的思路也一下子就開闊了。

我這裏使用的是QtCreator編輯器,編譯器使用的是是MSVC,是vs2013的編譯器。所以頭文件需要配置到Include上,庫文件需要配置到Lib目錄上。

如果是gcc的編譯器,配置才可能像下邊這樣。這個我沒有測試,因為我自己是msvc環境,不過網上這麼多人說了,估計應該也沒啥問題。這也是為啥我開頭說網上資源亂,因為我看的大多數是Mingw集成環境下的文章。

CPATH : C:\Users\Administrator\Desktop\QtAV-depends-windows-x86+x64\QtAV-depends-windows-x86+x64\include
LIBRARY_PATH : C:\Users\Administrator\Desktop\QtAV-depends-windows-x86+x64\QtAV-depends-windows-x86+x64\lib

首先說明我的編譯環境:

  • Qt版本:Qt5.7.1
  • 編譯器:vs2013上的MSVC編譯器
  • 編輯工具:QtCreator 4.2,其實跟這個關係不大,只是一個ide而已,我們使用的編譯器仍然是微軟的msvc編譯器。
  • 系統:Win10 64位

重點強調下,windows編譯qtav這篇文章一定要看,內容真的很實用。主要是告訴你在編譯前期,msvc和gcc兩種編譯器下,怎麼去配置環境變量。

二、相關文章

編譯步驟:Qt5.5.0編譯QtAV

不同編譯器下環境變量配置:windows編譯qtav

我自己是看着Qt5.5.0編譯QtAV這篇文章進行編譯的,最起碼資源都是在文章中的連接里下載的,包括QtAv源碼和依賴庫QtAV-depends-windows-x86+x64

但是參考這篇文章中配置環境變量時,一定要注意,這篇文章中的作者是GCC編譯器。而我們自己去要根據自己的編譯環境來配置環境變量,如果你是MINGW集成環境,也就是說你是GCC編譯器,那麼恭喜你,直接按原文配置即可。

但是,如果你不是GCC編譯器,那麼你就需要看windows編譯qtav
這篇文章,他會告訴你,其他編譯器怎麼配置環境變量

MSVC編譯器,配置方法如下。把頭文件和庫文件分別配置在Include和LIB目錄上。
如果是gcc的編譯器,需要把頭文件和庫文件分別配置在CPATH和LIBTRARY_PATH環境變量上。

三、下載QtAv源碼

源碼下載時直接上官方的github即可,QtAv

四、下載QtAv依賴庫

由於QtAv是基於ffmpeg開發的,因此我們需要下載相關依賴庫。QtAV-depends-windows-x86+x64

五、設置環境變量

根據不同編譯器設置方法不一樣,具體參看windows編譯qtav這篇文章

1、gcc設置方式

CPATH : C:\Users\Administrator\Desktop\QtAV-depends-windows-x86+x64\QtAV-depends-windows-x86+x64\include
LIBRARY_PATH : C:\Users\Administrator\Desktop\QtAV-depends-windows-x86+x64\QtAV-depends-windows-x86+x64\lib

2、msvc(cl)設置方式

圖中環境變量列表中加粗的字即是我添加的環境變量,msvc編譯器下INCLUDE和LIB這個兩個變量本身就是存在的,所以我們只需要在值那一列把include和lib目錄添加上即可。

注意:需要添加自己的QtAV-depends-windows-x86+x64依賴庫路徑

六、編譯

環境變量配置好之後,直接點擊構建即可,編譯成功后,效果如下

七、測試

編譯完成之後,我們會發現bin目錄下會有很多可執行文件,這個時候我們可以執行其中某一個可執行文件對我們編譯的庫進行測試。

首先拷貝QtAv的依賴庫ffmpeg,找到之前解壓的QtAV-depends-windows-x86+x64文件夾,把裡邊的bin目錄下的資源文件都拷貝到我們剛才編譯出來的QtAv目錄下。

找到我們剛才編譯生成的bin目錄,打開裡邊的simpleplayer.exe可執行程序。選擇一個本地的資源文件進行播放,效果圖可能如下圖所示,這裡是只放了一張圖,主要作為示意。

到這裏,我們的QtAv就編譯完成了。

後續有時間我會嘗試使用這個庫,然後做更進一步的分析

很重要–轉載聲明

  1. 本站文章無特別說明,皆為原創,版權所有,轉載時請用鏈接的方式,給出原文出處。同時寫上原作者:朝十晚八 or Twowords

  2. 如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

談談實現瀑布流布局的幾種思路

最近遇到這麼一個需求,需要在手機上做一個兩列的瀑布流布局,後來就把這個問題研究了一下,做個記錄。

一般來講,這種布局可以分為兩種情況:

  1. 圖片的數量是一定的,不需要頁面滾動到底部時,再動態加載圖片,只需要將圖片排成若干列

  2. 圖片的數量的不定的,頁面觸底時,需要從遠程加載圖片。

前者使用css的方法即可解決,後者則需要js來幫忙。

css解法

一、CSS多列布局

當我們展示的圖片數量一定時,可以優先採用css解法。其中一種方法是藉助css的多欄布局:


.photos{
  
  column-count: 3;
  column-gap: 10px;
}

得到的效果如下:

&lt;p&gt;See the Pen &lt;a href=’https://codepen.io/imgss/pen/Kjmjmq/’&gt;Kjmjmq&lt;/a&gt; by imgss&lt;br /&gt; (&lt;a href=’https://codepen.io/imgss’&gt;@imgss&lt;/a&gt;) on &lt;a href=’https://codepen.io’&gt;CodePen&lt;/a&gt;.&lt;br /&gt;

二、flex布局

flex布局同樣可以做到這一點,訣竅在於將flex-direction設為column;但是相對於多列布局,需要根據瀑布流的列數,計算一個合適的容器高度,不然可能會導致多出一行。如果你在下面的demo 中,看到了4列,不要懷疑,就是我計算的容器高度不合適導致的。。。

&lt;p&gt;See the Pen &lt;a href=’https://codepen.io/imgss/pen/XLRvmQ/’&gt;waterflow-flex&lt;/a&gt; by imgss&lt;br /&gt; (&lt;a href=’https://codepen.io/imgss’&gt;@imgss&lt;/a&gt;) on &lt;a href=’https://codepen.io’&gt;CodePen&lt;/a&gt;.&lt;br /&gt;

js解法

當圖片需要動態插入時,上面的兩種方法就不合適了,因為他們本質上是將圖片按照縱向進行排列的。圖片動態插入時通常我們希望圖片是按橫向插入到容器中的。這時候就需要js來幫忙了。首先,我們看看瀑布流和背包問題的關係。

瀑布流的基本思路是將一堆圖片放到若干列中,列與列之間的高度比較均勻,而不會相差太大。假如我們要分成兩列,那麼,問題就變成了,從 n 張圖片中挑出 m 張,使這 m 張圖片的總高度盡量接近 n 張圖片總高度的 1 / 2。於是這就變成了一個背包問題

01背包問題

背包問題是啥這裏不做展開,說白了是將一個複雜的問題分解為幾個簡單的問題,大佬們講的都比我好,網上也有各個語言版本的實現,不太了解的同學可以查看上面的鏈接。這裏直接放一個函數


function dp(ws, vs, limit) {
    let len = ws.length;
    let tables = new Array(len).fill().map(x => [])
    tables[-1] = new Array(limit + 1).fill(0);

    for(let i = 0; i < len; i++) {
        for (let w = 0; w <= limit; w++) {
            if (ws[i] > w) {
                tables[i][w] = tables[i-1][w]
            } else {
                tables[i][w] = Math.max(tables[i-1][w], tables[i-1][w-ws[i]] + vs[i])
            }
        }
    }
    // 回溯得到應該選哪些
    let max = limit;
    let selected = [];
    for (let idx = len - 1; idx >= 0; idx--) {
        if (ws[idx] <= max) {
            let isSelected = tables[idx-1][max] < tables[idx-1][max-ws[idx]] + vs[idx]
            if(isSelected) {
                selected.push(idx);
                max = max - ws[idx];
            }
        }
    }
    return selected;
}

有了這個解法之後,我們也就不難寫出一個瀑布流布局。具體思路是:假設我們要做一個3列的瀑布流布局,那麼可以不斷從圖片數組中選出一組圖片,使圖片的高度接近總高度的1/3,最終得到3組圖片。下面是一個代碼片段

      // colCount 表示要生成幾列
      while(colCount--) {
          // 獲取被選出的照片索引
          let idxs = dp(photoHeights, photoHeights, aver)
          // 得到被選出的一組圖片
          let photoCol = photos.filter((p,idx) => idxs.includes(idx)) 
          this.cols.push(photoCol)
          photoHeights.forEach((v,i) => {
          if (idxs.includes(i)) {
            photoHeights[i] = null
          }
        })
      }

下面這個demo就是按上面的思路實現的,可以拖動下面的滑塊來改變列數,觀察底部的間隙。在使用背包算法解決瀑布流問題時,一個需要我們注意的地方是,要將圖片高度轉化成整數。

&lt;p&gt;See the Pen &lt;a href=’https://codepen.io/imgss/pen/agwbgx/’&gt;waterflow-js&lt;/a&gt; by imgss&lt;br /&gt; (&lt;a href=’https://codepen.io/imgss’&gt;@imgss&lt;/a&gt;) on &lt;a href=’https://codepen.io’&gt;CodePen&lt;/a&gt;.&lt;br /&gt;

參考文章:https://www.cnblogs.com/ainyi/p/8766281.html
https://www.cnblogs.com/AndyJee/p/4543424.html
https://segmentfault.com/a/1190000012829866

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

Python深入淺出property特性屬性

導語

在Java中,通常在類中定義的成員變量為私有變量,在類的實例中不能直接通過對象.屬性直接操作,而是要通過getter和setter來操作私有變量。
而在Python中,因為有property這個概念,所以不需要寫getter和setter一堆重複的代碼來操作私有變量。Python“私有變量”通常在變量前加上“_”或者“__”,例如_attr或者__attr,這是約定俗成的規範。

把私有屬性變成只讀特性

class MyClass:

    def __init__(self, x):
        self._x = x

這裏定義了一個MyClass類,它有一個實例變量_x,綁定了用戶傳來的x值。_x是私有變量,通過obj._x獲取私有變量不符合語言規範,進而我們要使_x變成property(特性),通過obj.x直接訪問。

改造后的代碼如下:

class MyClass:

    def __init__(self, x):
        self._x = x

    @property
    def x(self):
        return self._x
    
>>> obj = MyClass(10)
>>> obj.x
10

我們把_x變成了property特性,以只讀的方式獲取x的值。

我們現在想為x賦值該怎樣做呢?

>>> obj.x = 999
Traceback (most recent call last):
  File "xxx", line 14, in <module>
    obj.x = 23
AttributeError: can't set attribute

可以看到,拋出了AttributeError: can’t set attribute。顯然,只讀方法不支持賦值。

把私有變量變成可賦值的特性

我們只需要在上述代碼改造成:

class MyClass:

    def __init__(self, x):
        self._x = x

    @property
    def x(self):
        return self._x
    
    @x.setter
    def x(self, value):
        self._x = value

>>> obj = MyClass(10)
>>> obj.x = 999
>>> obj.x
999

可以看到,我們為x添加了setter,可以直接為obj.x賦值操作

property屬性能夠遮蓋實例屬性

繼續上面的代碼,我們看看以下的操作:

>>> obj = MyClass(10)
>>> obj.__dict__
{'_x': 999}  #此時實例變量中有_x的值
>>> obj.__dict__['x'] = 99999  #設置obj的實例變量有x值,跟property屬性重名!
>>> obj.__dict__
{'_x': 999, 'x': 99999}  #此時實例變量中有_x和x的值

>>> obj.x     #結果是obj的實例變量還是property屬性?
10

如上代碼所示,obj對象有一個_x實例變量和一個x的property屬性,我們又強行為obj增加了一個x實例變量,這個實例變量x和property屬性x同名!
通過obj.x我們得知,返回的是property屬性,說明property屬性會遮蓋實例屬性!也可以理解為property屬性的優先級更大!

property類解析

我們通常使用內置的@property裝飾器。但其實property是一個類,python中類和函數的調用方式都差不多,他們都是可調用對象
property的構造方法如下:

class property(object):
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        """"""

它最大接受4個參數,都可以為空。
第一個為getter,第二個為setter,第三個為delete函數,第四個為文檔。

上述代碼的另一種寫法

class MyClass:

    def __init__(self, x):
        self._x = x

    def get_x(self):
        return self._x

    def set_x(self, value):
        self._x = value

    x = property(get_x, set_x)

>>> obj = MyClass(10)
>>> obj.x
10

如上,x是property的實例,設置了getter和setter,作為類變量放在MyClass類中。

以上就是property屬性的解析。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

機器學習實記(一)簡介機器學習

一.寫在前面

  本系列中的內容來自對李宏毅老師的機器學習教程的理解和一些個人理解的筆記,主要用於之後方便查看複習。李宏毅老師的機器學習教程地址:https://www.bilibili.com/video/av35932863?from=search&seid=3902369523652897681。

二.機器學習概念

2.1人工智能、機器學習、深度學習這幾個概念的關係

  要想理解清楚機器學習的基本概念,首先要明白機器學習在人工智能領域中所處的位置,再簡單明了些首先要明白人工智能、機器學習、深度學習這幾個概念的關係。三者關係如下圖2-1

                      圖2-1 人工智能、機器學習、深度學習三者關係

從圖可以直觀的看出,人工智能的範圍>機器學習的範圍>深度學習的範圍,而平時常說的神經網絡的這些有關概念則是屬於深度學習下的分支。再具體而言這三者的關係,其實人工智能是作為我們最終所要達到的目標,所謂的人工智能概念我覺的用之前某科學家提過的一種說法簡單理解一下就行,即當用一塊黑幕將計算機遮蓋起來,黑幕外的人無法區分所交互的對象是人還是計算機就算是達到了人工智能標準,再簡單點說就是使計算機做到過去只有人才能做到的事,其他更為深入的解釋歡迎查看百度詞條https://baike.baidu.com/item/人工智能/9180?fr=aladdin。而機器學習則是實現人工智能這一目標的手段,這說明還有其他手段有興趣的同學可以自行去了解一下,不過個人認為其餘的手段不是現在的主流研究方向吧。而深度學習則是作為機器學習下的一個重要分支。

2.2 理解機器學習

  所謂機器學習概念,簡單的理解就是教會計算機學習,舉個具體的例子,當你給它看完貓的某一張圖片后,你告訴機器這是貓,當你給它看完一張狗的圖片后,你告訴它這是狗,不斷重複這一過程讓機器學習大量不同的貓狗照片,學習結束后給予機器一張全新的貓或狗的照片,機器能夠成功的識別出貓和狗。再進一步抽象的理解,我們可以發現這一過程十分類似於數學上求解函數表達式,即這個過程其實是在尋找一個函數f,當輸入某張貓的照片函數f的輸出為貓,當輸入某張狗的照片函數f的輸出為狗。再進一步具體而言整個機器學習框架可以理解為如圖2-2

                                                                            圖2-2 機器學習框架描述

整個機器學習框架的步驟大體上分為三步:第一步定義一個函數池,其中有大量的備選函數f1、f2、f3…..fn;第二步對各個函數進行評價,這裏的話其實我們可以將每個函數理解為一個模型,所謂的評價的標準可以理解為對每個輸入照片各個模型所能識別出來的精確度,優秀的模型識別照片的準確度更高;第三部選出最優的函數fbest,即選出最優的模型。

2.3 理解機器學習的學習圖

  機器學習的學習圖如圖2-3,這張圖看起來很複雜,其實結構很清晰,主要分為三中顏色藍、橙、綠三種顏色,分別代表運用情景,所要解決問題的目標,以及解決問題用到的方法。

                           圖2-3 機器學習學習圖

  首先是藍色部分,即運用情景從圖中可以看出有supervise learning、semi-supervise learning、transer learning、unsupervised learning、reinforcement learning,看到這麼多運用情景不經想問一個問題,為什麼會有這麼多運用場景的劃分?原因其實很簡單,從前面的對機器學習的介紹中我們可以發現,機器學習的過程中是需要大量的帶標籤的數據,所謂帶標籤的數據就是指給出了輸入數據的正確結果,比如說輸入一張貓的圖片,這個貓就是這張圖片的標籤,但其實當數據量巨大的時候,給每個數據標上標籤是需要消耗大量時間的,所以由輸入的數據是否帶有標籤就產生了不同的運用場景。

  supervise learning 即監督學習,訓練所使用的數據均帶標籤,即告訴機器輸入數據的正確答案。semi-supervise learning 即半監督學習,訓練中的數據有一部分帶標籤,其餘數據不帶標籤交給機器自己學習。transer learning 即遷移學習 訓練數據中有一部分帶標籤,剩下的數據來自其他模型帶標籤或不帶標籤的數據,舉個例子我們做貓和狗的分類,我們有少量的已經有標籤的貓和狗的數據,還有大量的可能是帶標籤或不帶標籤的其他動物的數據,將這些數據也交由機器學習。unsupervised learning 即無監督學習,訓練中的數據均不帶標籤。reinforcement learning 即強化學習,這種學習方式與前面的學習方式均有所不同,前面的學習方式中均為告訴機器輸入數據的正確答案或者直接將數據交由機器,在強化學習中數據均不帶標籤,它是通評價告訴機器這次學習的結果,舉個例子阿爾法狗,當它完成一次棋局對戰最終取得勝利,則給與機器較高的評價,讓機器自身進行調整,雖然機器本身並不知道自己到底哪下的好,但知道這麼下贏了。

  其次是橙色部分,即所要解決問題的目標,分為regression和classification以及structured learning。regression即所要解決的問題的解是個數值,比方說預測某日pm2.5的值;classification即所要解決的問題為分類問題,包括二分類即回答是或者否,比方說判斷郵件是否為垃圾郵件,多分類對輸入的數據進行多個類別分類,比方說判斷輸入的文章屬於哪個板塊是娛樂版塊還是體育板塊還是金融板塊等等;structured learning即所要解決的問題是結構化的,比方說輸入一段語言判斷語言的內容。

  最後是綠色部分,這一塊的話還記的我在2.2中說過的函數池嗎,所謂的函數其實就是模型,我們可以使用不同的模型來解決不同的問題,這些模型有linear model即線性模型、non-linear model 即非線性模型,非線性模型中就包括之前提到的深度學習還有一些非線性模型。

三.寫在最後

  這一部分主要是對機器學習要有個整體的大概理解,包括理解機器學習的整體框架的大致模樣,尤其是要明白框架中所說的函數其實就是模型的概念,再由就是要對為何要這樣劃分學習圖有自己的理解,至於是否要記住學習圖的內容倒是其次。

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

【工具篇】在.Net中實現HTML生成圖片或PDF的幾種方式

前段時間由於項目上的需求,要在.Net平台下實現把HTML內容生成圖片或PDF文件的功能,特意在網上研究了幾種方案,這裏記錄一下以備日後再次使用。當時想着找一種開發部署都比較清爽並且運行穩定的方案,但實際上兩者同時滿足基本不可能,只能做一個自己覺得合適的取捨,下面從兩個維度(清爽指數和功能指數)逐一對比。


1.   WebBrowser

這種方案在開發時不依賴任務外部程序集和nuget包,部署時也不需要安裝額外的工具和服務,可以說是非常清爽了。它藉助了WinForm下的WebBrowser控件實現HTML內容渲染,並把渲染結果繪製在Bitmap中,進而保存成圖片或PDF文件。這種方案簡單粗暴,是C#中最基礎的實現方式,也是網上搜索結果最多的一種,下面看它的核心代碼(從網上拼湊得來):

 1     class WebBrowserPage2Image
 2     {
 3         Bitmap m_Bitmap;
 4 
 5         string m_Url;               
 6 
 7         public void Convert(string pageUrl, string fileName)
 8         {
 9             m_Url = pageUrl;
10             Thread m_thread = new Thread(new ThreadStart(HtmlDrawToBitmap));
11             m_thread.SetApartmentState(ApartmentState.STA);
12             m_thread.IsBackground = true;
13             m_thread.Start();
14             m_thread.Join();
15             MemoryStream stream = new MemoryStream();
16             m_Bitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
17             byte[] buff = stream.ToArray();
18             FileStream fs = new FileStream(fileName, FileMode.Create);
19             stream.WriteTo(fs);
20             stream.Dispose();
21             stream.Close();
22             fs.Close();
23         }
24 
25         private void HtmlDrawToBitmap()
26         {
27             WebBrowser browser = new WebBrowser();
28             browser.ScrollBarsEnabled = false;
29             browser.Navigate(m_Url);
30             browser.DocumentCompleted += new WebBrowserDocumentCompletedEventHandler(delegate (object sender, WebBrowserDocumentCompletedEventArgs bdce)
31             {
32                 if (browser.ReadyState == WebBrowserReadyState.Complete)
33                 {
34                     //myWebBrowser.Document.Body.Style = "zoom:180%";
35                     Rectangle r = browser.Document.Body.ScrollRectangle;
36                     browser.Height = r.Height;
37                     browser.Width = r.Width;
38                     m_Bitmap = new Bitmap(browser.Width, browser.Height);
39                     browser.BringToFront();
40                     browser.DrawToBitmap(m_Bitmap, new Rectangle() { Width = browser.Width, Height = browser.Height });
41                 }
42             });
43             while (browser.ReadyState != WebBrowserReadyState.Complete)
44             {
45                 Application.DoEvents();
46             }
47             browser.Dispose();
48         }
49     }

View Code

雖然開發起來非常簡潔,但是問題也很明顯。WebBrowser是Winform下的一個組件,在非Winform項目中運行會出現不可知的異常,即使在Winform項目中,數據量比較大的時候依然會出現卡死的情況。我做過500次循環的測試,在執行到100多次的時候程序出現假死不動也無異常拋出。除此之外,生成的圖片失真也比較嚴重,特殊字體和部分CSS樣式無法渲染。總的來說,基本無法達到生成環境需求。

清爽指數:★★★★★    功能指數:★


2.         Wkhtmltox

這也是網上廣泛流傳的一個方案,wkhtmltox是一套開源的命令行工具,提供了圖片和PDF的轉換能力,它採用C++編寫,使用Webkit作為渲染引擎,開源地址是https://github.com/wkhtmltopdf/wkhtmltopdf。使用方法就是在命令行工具中執行命令,例如:

wkhtmltopdf --grayscale  https://www.baidu.com  baidu.pdf

如果要在.Net項目中使用的話,核心問題就是用程序喚起命令行,同時指定參數執行即可,類似於下面的代碼:

       System.Diagnostics.ProcessStartInfo Info = new System.Diagnostics.ProcessStartInfo();
       Info.FileName = @"D:\dev\wkhtmltox\bin\wkhtmltopdf.exe";
       Info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
       Info.CreateNoWindow = true;
       Info.Arguments = @"-q --orientation Landscape https://www.baidu.com D:\\baidu.pdf";
       System.Diagnostics.Process proc = System.Diagnostics.Process.Start(Info);
       proc.WaitForExit();
       proc.Close();

更多強大的功能例如加水印、分頁、改樣式等可以參考這篇文章:https://www.cnblogs.com/82xb/p/7837597.html

詳細的參數說明可以查看文檔:https://wkhtmltopdf.org/usage/wkhtmltopdf.txt

GitHub上有很多針對各個開發語言的封裝,使用起來比較方便,唯一不爽的是部署項目前要先安裝好這個工具。

清爽指數:★★★★    功能指數:★★★★


3.         PuppeteerSharp

這個就更厲害了,說到這個就不得不先介紹下Puppeteer,因為PuppeteerSharp正是從Puppeteer衍生而來。

Puppeteer是由谷歌開源的一個Node項目,它提供了和Chrome DevTools的通信能力,基本上我們能在Chrome實現的操作通過它的API都可以實現,強大到讓你不敢相信。主要的應用有:

  • 生成頁面快照(圖片、PDF)
  • 爬蟲,網站內容抓取
  • 自動化測試(模擬鍵盤鼠標輸入,表單提交,UI測試等)
  • 網站性能分析(追蹤,時間線捕獲等)

開源地址是https://github.com/GoogleChrome/puppeteer

在Node項目中使用Puppeteer非常簡單,先安裝npm包:

npm i puppeteer

安裝過程可能會有點慢,因為在安裝的時候會下載一個最近版本的Chromium(Mac下大概170M,Linux下大概282M,Windows下大概280M)。當然,如果你本地已經有一個Chromium,可以設置npm的全局配置PUPPETEER_SKIP_CHROMIUM_DOWNLOAD 跳過下載,然後在程序中手動指定Chromium的位置。

生成圖片和PDF文件例子:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://www.baidu.com');
  await page.screenshot({path: 'baidu.png'});
  await page.pdf({path: 'baidu.pdf', format: 'A4'});
  await browser.close();
})(); 

Puppeteer默認使用無界面模式(headless:true),如果想看到完整的瀏覽器界面,可以通過下面的設置開啟:

  const browser = await puppeteer.launch({headless: false});

Puppeteer提供了豐富的選擇器接口,可以輕鬆實現模擬輸入和鼠標點擊,例如:

  await page.type('#index-kw', 'cnblogs');
  await page.click('#index-bn');

      還支持指定使用設備:

  const devices = require('puppeteer/DeviceDescriptors');
  await page.emulate(devices['iPhone 8']);

      詳細的API文檔可以參考:https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md

Puppeteer確實非常強大,但由於它是一個Node包無法直接在C#項目中使用,那怎麼辦呢?好在有國外的大神把Puppeteer移植到了.Net平台,也就是PuppeteerSharp。

注意:PuppeteerSharp是基於NetStandard 2.0開發的,所以項目的平台最低版本要是.NET Framework 4.6.1和.NET Core 2.0。

首先通過nuget安裝:

PM > Install-Package PuppeteerSharp

導入命名空間:

  using PuppeteerSharp;

下面是我在ASP.NET Core 2.1下封裝的測試方法:

        [HttpPost, Route("page2img")]
        public async Task<string> PageToImage(string url, int? width, int? height)
        {
            await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);
            var browser = await Puppeteer.LaunchAsync(new LaunchOptions
            {
                Headless = true,
                //ExecutablePath="",
                Args = new string[] { "--no-sandbox" }
            });
            var page = await browser.NewPageAsync();
            bool fullPage = true;
            if (width.HasValue && height.HasValue)
            {
                await page.SetViewportAsync(new ViewPortOptions
                {
                    Width = width.Value,
                    Height = height.Value
                });
                fullPage = false;
            }
            await page.GoToAsync(System.Web.HttpUtility.UrlDecode(url));
            string fileName = $"/Files/{Guid.NewGuid().ToString()}.png";
            await page.ScreenshotAsync($"{AppDomain.CurrentDomain.BaseDirectory}{fileName}", new ScreenshotOptions { FullPage = fullPage });
            return $"{Request.Host.ToString()}{fileName}";
        }

上面方法的第一行:

  await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);

程序會判斷本地環境有沒有可用的Chromium,如果沒有的話會自動下載一個默認版本的Chromium,這個過程可能會有點長,下載成功後會在項目根目錄多一個這樣的文件夾:

和前面說的一樣,如果本地已經下載過Chromium,可以通過LaunchOptionsExecutablePath字段指定一個目錄。目前PuppeteerSharp在網上的資料還不是很多,但是得益於它與Puppeteer高度完整和相似的API,Puppeteer的文檔對它基本都能適用。

總體來說,這個工具功能強大並且比較穩定(我在Windows和Linux下都測試通過),是一個不錯的選擇,但是由於它必須依賴於Chromium來運行,打包部署並不是很方便,我建議把它作為一個獨立的web服務。

清爽指數:★★★    功能指數:★★★★★


4.         IronPdf

    除了一些開源的項目和工具能提供HTML轉圖片或PDF的功能,很多商業軟件公司也提供了這樣的產品,IronPdf算是裏面比較有代表性的一個。和其他收費軟件不同的是,IronPdf有一個對開發者免費試用的license:

    IronPdf的主要特性包括:

  • 任何類型的HTML文件、代碼片段、URL生成PDF
  • PDF編輯
  • 圖片與PDF互轉
  • 支持HTML5和CSS3,支持響應式布局,支持JS腳本,豐富的配置選項
  • 支持C#、VB、Webform、ASP.NET MVC、.NET CORE

    我們可以在官網下載DLL文件直接引用到項目,也可以通過nuget來安裝:

PM > Install-Package IronPdf

    導入命名空間:

  using IronPdf;

    一個最簡單的例子:

// Create a PDF from any existing web page
var Renderer = new IronPdf.HtmlToPdf();
Renderer.PrintOptions.EnableJavaScript = true;
Renderer.PrintOptions.PaperOrientation = IronPdf.PdfPrintOptions.PdfPaperOrientation.Landscape;
var PDF = Renderer.RenderUrlAsPdf("https://www.baidu.com");
PDF.SaveAs("baidu.pdf");

// This neat trick opens our PDF file so we can see the result
System.Diagnostics.Process.Start("baidu.pdf");

    添加水印:

  pdf.WatermarkAllPages("<h2 style='color:red'>SAMPLE</h2>", PdfDocument.WaterMarkLocation.MiddleCenter, 50, -45, "https://www.baidu.com");

    用圖片生成PDF文檔:

// Select one or more images.  This example selects all JPEG images in a specific folder.
var ImageFiles = Directory.EnumerateFiles(@"C:\project\assets").Where(f => f.EndsWith(".jpg") || f.EndsWith(".jpeg"));

// Convert the images to a PDF and save it.
ImageToPdfConverter.ImageToPdf(ImageFiles).SaveAs(@"C:\project\composite.pdf");

    更多高級功能和配置可以參考官網例子:https://ironpdf.com/examples/image-to-pdf/

 清爽指數:★★★★    功能指數:★★★★

    

寫在最後

    以上幾種方式,都是我在本次實踐中總結出來的,可能不是很全面,歡迎大家不吝補充。

    遺憾的是,最終項目沒有用上面的任何一種方式,而是抓取到HTML內容後用正則解析,然後用Bitmap一點一點重新畫圖生成圖片文件保存。因為我要截取的頁面內容很少,就是一個簡單的电子處方箋,需求上也沒有要求必須完全和原網頁100%一致,繪圖也算是一個不錯的方案,但是缺點是一旦HTML結構或樣式發生變化,那這套東西就失效了,好在這個不會輕易變更,也算是一個折中方案。

 

 

 

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

docker (2)—存儲、網絡(利用docker容器上線靜態網站)

一、docker底層依賴的核心技術

1、命名空間 (Namespaces)

2、控制組 (Control Groups)

3、聯合文件系統 (Union File System)

4Linux 虛擬網絡支持:本地和容器內創建虛擬接口

(1) 命名空間(Namespaces):

實現了容器間資源的隔離,每個容器擁有自己獨立的命名空間 , 運行其中的應用就像是運行在獨立的操作系統中一樣 , 我們都可以看到文件系統,網卡等資源保證了容器之間互不影響,namesaces管理進程號 , 每個進程命名空間有一套自己的進程號管理方法 ,

程命名空間是一個父子關係的結構 , 子空間中的進程對於父空間是可見的。

(2) 控制組 (Control Groups) :

  控制組 (Control groups)–CGroups Linux 內核的一個特性 ,主要用來對共享資源進行隔離、限制、審計等 。cgroups 允許對於進程或進程組公平( 不公平 ) 的分配 CPU 時間、內存分配和 I/O 帶寬。

容器通過 cgroups 來得到所能夠管理資源的分配和使用。因此容器所獲得資源僅為所有系統資源的一個部分

1、資源限制 : 內存子系統為進程組設置內存使用上限,內存達到上限后再申請內存,就會發出 Out of Memory

2、 優先級 : 通過優先級讓一些組得到更多 CPU 等資源

3、 資源審計 : 用來統計系統上實際把多少資源用到適合的目的上 , 可以使用 cpuacct 子系統記錄某個進程組使用的 CPU 時間

4、 隔離 : 為組隔離名字空間 , 這樣一個組不會看到其他組的進程 .網絡連接和文件系統

5、 控制 : 掛起 . 恢復和啟動等操作

(3)聯合文件系統 (Union File System) :

  docker 中使用AUFS(another Union File System v2 版本以後的Advanced multi-layered Unification File System) 控製為每一個成員目錄設定只讀 / 讀寫 / 寫出權限 , 同時 AUFS 有一個類似分層的概念 , 對只讀權限的分支可以邏輯上進行增量的修改.

二、docker的存儲

docker兩種存儲資源類型:

1Data Volume (數據卷)

2Data Volume Dontainers — 數據卷容器

(1) Data Volume (數據卷):

Data Volume 本質上是 Docker Host 文件系統中的目錄或文件,使用類似與 Linux 下對目錄或者文件進行 mount 操作。數據卷可以在容器之間共享和重用,對數據卷的更改會立馬生效,對數據卷的更新不會影響鏡像,卷會一直存在,直到沒有容器使用。

Data Volume(數據卷)的特點:

1Data Volume 是目錄或文件,而非沒有格式化的磁盤(塊設備)。

2、容器可以讀寫 volume 中的數據。

3volume 數據可以被永久的保存,即使使用它的容器已經銷毀。

 Data Volume 的使用:

1、在宿主機根目錄下創建一個目錄(數據卷)

 

2、啟動一個容器並將數據卷掛載到容器的目錄下

 

3、驗證  ( 持久化的需要映射目錄)

 

#

 

(2)Data Volume Dontainers — 數據卷容器

數據卷容器就是一個普通的容器,只不過是專門用它提供數據卷供其他容器掛載使用

Data Volume Dontainers使用:

1、創建一個名為 dbdata 的數據卷,並在其中創建一個數據卷掛載到 /dbdata

docker run -dti -v /dbdata --name dbser centos:latest

2、再啟動兩個容器,並使用數據卷容器

docker run -dti --volumes-from dbser --name db1 centos:latest

#

3、驗證

 

#

容器 db1 db2 同時掛載了同一個數據卷到本地相同 /dbdata目錄。三個容器任何一個目錄下的寫入,都可以時時同步到另外兩個

三、docker 三種網絡

  docker 網絡從覆蓋範圍可分為單個 host 上的容器網絡和跨多個 host 的網絡,docker 目前提供了映射容器端口到宿主主機和容器互聯機制來為容器提供網絡服務,在啟動容器的時候,如果不指定參數,在容器外部是沒有辦法通過網絡來訪問容器內部的網絡應用和服務的

docker 安裝時會自動在host上創建三個網絡

docker   network  ls   (查看docker  網絡)

(1) docker–none網絡

none 網絡就是什麼都沒有的網絡。掛在這個網絡下的容器除了 lo,沒有其他 任何網卡。容器創建時,可以通過 –network=none 指定使用 none 網絡

 

none網絡的應用

封閉的網絡意味着隔離,一些對安全性要求高並且不需要聯網的應用可以使用 none 網絡。

(2)docker–host網絡

連接到 host 網絡的容器,共享 docker host 的網絡棧,容器的網絡配置與host 完全一樣。可以通過 –network=host 指定使用 host 網絡

 

host 網絡的應用

  直接使用 Docker host 的網絡最大的好處就是性能,如果容器對網絡傳輸效率有較高要求,就可以選擇 host 網絡。當然不便之處就是犧牲一些靈活性,比如要考慮端口衝突問題,Docker host上已經使用的端口就不能再用了。

Docker host 的另一個用途是讓容器可以直接配置 host 網路。比如某些跨host 的網絡解決方案,其本身也是以容器方式運行的,這些方案需要對網絡進行配置,比如管理 iptables

(3) docker–bridge 網絡

docker 安裝時會創建一個 命名為 docker0 linux bridge。如果不指定–network,創建的容器默認都會掛到 docker0

 

#

 

eth0@if29      veth04c5851 是一對 veth pair

#

  veth pair 是一種成對出現的特殊網絡設備,可以把它們想象成由一根虛擬網線連接起來的一對網卡,網卡的一頭(eth0@if29)在容器中,另一頭( veth04c5851)掛在網橋 docker0 上,其效果就是將 eth0@if29也掛在了docker0 上。

# 查看網絡配置信息 ( 設置容器ip 網段、網關)

docker network inspect bridge

 

#

注:容器創建時,docker 會自動從 172.17.0.0/16 中分配一個 IP,這裏 16 位的掩碼保證有足夠多的 IP 可以供容器。

四、創建 user-defined網絡 (自定義網絡)

通過 bridge 驅動創建類似前面默認的 bridge 網絡

1、利用bridge驅動創建名為my-net2網橋(docker會自動分配網段)

docker network create --driver bridge my-net2

# 查看網絡配置信息

 

# 查看網橋

 

2、利用bridge驅動創建名為my-net3網橋(user-defined (自定義)網段及網關)

docker network create --driver bridge --subnet 172.33.1.0/24 --gateway 172.33.1.1 my-net3

# 查看網絡配置信息

 

# 查看網橋

 

3、啟動容器使用新建的my-net3網絡

docker run -it  --network=my-net3  busybox:latest

4、啟動容器使用my-net3網絡並指定ip(只有使用 –subnet 創建的網絡才能指定靜態 IP,如果是docker自動分配的網段不可以指定ip

docker run -it --network=my-net3  --ip 172.33.1.100  busybox:latest

 

5、讓已啟動不同vlanbusybox容器,可以連接到my-net2(其實在busybox中新建了my-net2的網卡)(添加網卡。訪問不同的網段)

 

# #docker network connect my-net3  08493ae30117   ( 連接)

6、使用–name指定啟動容器名字,可以使用docker自帶DNS通信,但只能工作在user-defined 網絡,默認的 bridge 網絡是無法使用 DNS 的。

#docker run -it --network=my-net3 --name=bbox1 busybox:latest

#docker run -it --network=my-net3 --name=bbox2 busybox:latest

7、容器之間的網絡互聯

&1、創建一個 db 容器

docker run -dti --name db centos:latest

&2、創建一個 web 容器,並使其連接到 容器db

docker run -dti --name web --link db:dblink centos:latest /bin/bash

–link db:dblink 實際是連接對端的名字和這個鏈接的名字,也就是和 db 容器建立一個叫做 dblink 的鏈接

 

# 測試  

 

注:此鏈接通信是單向的

8、容器端口映射

在啟動容器的時候,如果不指定參數,在容器外部是沒有辦法通過網絡來訪問容器內部的網絡應用和服務的,當容器需要通信時,我們可以使用 -P (大) &&-p (小)來指定端口映射

(1)   -P Docker 會隨機映射一個 49000 49900 的端口到容器內部開放的網絡端口

(2)   -p :則可以指定要映射的端口,並且在一個指定的端口上只可以綁定一個容器。

支持的格式

 IP HostPort ContainerPort

 IP : : ContainerPort

 IP HostPort

&1、 查看映射

docker port   CONTAINER ID/NAMES

&2、映射所有接口地址,此時綁定本地所有接口上的 5200 到容器的 5200 接口,訪問任何一個本地接口的 5000 ,都會直接訪問到容器內部

docker run -dti -p 5200:5200 centos:latest  /bin/bash

&3、多次使用可以實現多個接口的映射

docker run -dti -p 5400:5400  -p 5300:5300 centos:latest  /bin/bash

&4、映射到指定地址的指定接口

此時會綁定本地 192.168.226.147 接口上的 5100 到容器的 5100 接口

docker run -dti -p 192.168.226.147:5100:5100 centos:latest /bin/bash

 

&5、映射到指定地址的任意接口

此時會綁定本地 192.168.226.147 接口上的任意一個接口到容器的 5500 接口

docker run -dti -p 192.168.226.147::5500 centos:latest /bin/bash

實驗、通過端口映射實現訪問本地的 IP:PORT 可以訪問到容器內的 web

1、將容器80端口映射到主機8090端口

docker run -itd -p 192.168.226.147:8090:80 --name http-test httpd:latest

2、查看剛運行docker容器

docker  ps

 

3、進入 容器

 

4、容器內部編輯網頁文件 index.html

 

5、到宿主機上打開瀏覽器輸入 IP:PORT 訪問驗證

http://192.168.226.147:8090/

 

6、宿主機上傳靜態網站測試文件

 

7、解壓

 

8、把解壓的目錄上傳至容器下的網站根目錄

docker cp jd 67b3daf15a40:/usr/local/apache2/htdocs

9、進入容器,刪除原來的index.html 文件

 

10、展開目錄

 

11web 訪問

http://192.168.226.147:8090/

 

 

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

【劍指Offer】調整數組順序使奇數位於偶數前面

題目描述

輸入一個整數數組,實現一個函數來調整該數組中数字的順序,使得所有的奇數位於數組的前半部分,所有的偶數位於數組的後半部分,並保證奇數和奇數,偶數和偶數之間的相對位置不變。

解法1

最直接的思路是再構建一個新數組,先遍歷一遍原數組,把其中的奇數依次添加到新數組中,再遍歷一遍原數組把其中的偶數依次添加到新數組中,時間複雜度為O(2n)。實現代碼如下

實現代碼

public int[] reOrderArray2(int[] array)
{
    int[] arr = new int[array.Length];
    int index = 0;
    for (int i = 0; i < array.Length; i++)
    {
        // 奇數
        if ((array[i] % 2) != 0)
        {
            arr[index] = array[i];
            index++;
        }
    }
    for (int i = 0; i < array.Length; i++)
    {
        // 偶數
        if ((array[i] % 2) == 0)
        {
            arr[index] = array[i];
            index++;
        }
    }
    return arr;
}

解法2

C#的數組是不支持動態添加元素的,我們可以使用泛型List,來實現在指定位置插入元素。基本思路是遍歷原數組,依次將元素插入到List中,如果是偶數元素,默認插入到List的末尾。如果是奇數元素,則插入到所有的偶數元素之前(已插入的所有奇數元素之後),因此需要記錄最後插入的奇數元素的索引。實現代碼如下,算法的時間複雜度是O(n)

實現代碼

public int[] reOrderArray(int[] array)
{
    List<int> list = new List<int>();
    // 最後插入奇數元素的索引
    int index = 0;
    foreach (int i in array)
    {
        if ((i % 2) == 0)
            list.Add(i);
        else
        {
            list.Insert(index, i);
            index++;
        }
    }
    return list.ToArray();
}

解法3

上面的兩種解法都用到臨時數組或List,空間複雜度是O(n),某些情況下可能希望空間複雜度越低越好。下面這種解法雖然時間複雜度提高了,但降低了空間複雜度,不再需要額外的空間。基本思路是遍歷原數組,如果遇到了奇數元素,就將該元素向前移動,該元素前面的偶數元素都依次向後移動。
舉個例子:比如數組{1, 2, 4, 3, 5}
遍曆數組,得到第一個元素是奇數1,其前面沒有元素所以不做移動
第二個,第三個是偶數,不做處理。
第四個元素是奇數3,所以將3往前移動,3前面的偶數元素{2, 4}都向後移動。移動后的數組為{1, 3, 2, 4, 5}
接着第五個元素是奇數5,所以將5往前移動,5前面的偶數元素{2, 4}都向後移動。移動后的數組為{1, 3, 5, 2, 4}
可以這樣理解,每發現一個奇數時,就將這個奇數移動到了它最終應該在的位置上。

實現代碼

public int[] reOrderArray(int[] array)
{
    for(int i = 0; i < array.Length; i++)
    {
        if((array[i] % 2) != 0)
        {
            int temp = array[i];
            int j = i - 1;
            for(; j >= 0; j--)
            {
                if ((array[j] % 2) != 0)
                    break;
                array[j + 1] = array[j]; 
            }
            array[j + 1] = temp;
        }
    }
    return array;
}

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

多線程與高併發(一)多線程入門

一、基礎概念

多線程的學習從一些概念開始,進程和線程,併發與并行,同步與異步,高併發。

1.1 進程與線程

幾乎所有的操作系統都支持同時運行期多個任務,所有運行中的任務通常就是一個進程,進程是處於運行過程中的程序,進程是操作系統進行資源分配和調度的一個獨立單位。

進程有三個如下特徵:

  • 獨立性:進程是系統中獨立存在的實體,它可以擁有自己獨立的資源,每一個進程都擁有自己私有的地址空間。在沒有經過進程本身允許的情況下,一個用戶進程不可以直接訪問其他進程的地址空間。

  • 動態性:進程與程序的區別在於,程序只是一個靜態的指令集合,而進程是一個正在系統中活動的指令集合。在進程中加入了時間的概念,進程具有自己的生命周期和各種不同的狀態,這些概念在程序中部是不具備的。

  • 併發性:多個進程可以在單個處理器上併發執行,多個進程之間不會互相影響。

線程是進程的組成部分,一個進程可以擁有多個線程,而線程必須有一個父進程,線程可以有自己的堆棧、自己的程序計數器和自己的局部變量,但不擁有系統資源。比如使用QQ時,我們可以同事傳文件,發送圖片,聊天,這就是多個線程在進行。

線程可以完成一定的任務,線程能夠獨立運行的,它不知道有其他線程的存在,線程的執行是搶佔式的,當前線程隨時可能被掛起。

總之:一個程序運行后至少有一個進程,一個進程里可以有多個線程,但至少要有一個線程。

1.2 併發和并行

併發和并行是比較容易混淆的概念,他們都表示兩個或者多個任務一起執行,但併發側重多個任務交替執行,同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,使得在宏觀上具有多個進程同時執行的效果。而并行確實真正的同時執行,有多條指令在多個處理器上同時執行,并行的前提條件就是多核CPU。

1.3 同步和異步

同步和異步通常用來形容一次方法調用。同步方法調用一旦開始,調用者必須等到方法調用返回后,才能繼續後續的行為。異步方法調用更像一個消息傳遞,一旦開始,方法調用就會立即返回,調用者可以繼續後續的操作。

1.4 高併發

高併發一般是指在短時間內遇到大量操作請求,非常具有代表性的場景是秒殺活動與搶票,高併發是互聯網分佈式系統架構設計中必須考慮的因素之一,高併發相關常用的一些指標有響應時間(Response Time),吞吐量(Throughput),每秒查詢率QPS(Query Per Second),併發用戶數等。

多線程在這裏只是在同/異步角度上解決高併發問題的其中的一個方法手段,是在同一時刻利用計算機閑置資源的一種方式

1.5 多線程的好處

線程在程序中是獨立的、併發的執行流,擁有獨立的內存單元,多個線程共享父進程里的全部資源,線程共享的環境有進程的代碼段,進程的公有數據等,利用這些共享數據,線程很容易實現相互之間的通信,可以提高程序的運行效率。

多線程的好處主要有:

  • 進程之間不能共享內存,但線程之間共享內存非常容易。

  • 系統創建進程時需要給進程重新分配系統資源,但創建線程代價小得多,所以使用多線程實現多任務併發比多進程效率高

  • Java語言內置了多線程功能支持。

二、使用多線程

上面講了多線程的一些概念,都有些抽象,下面將學習如何使用多線程,創建多線程的方式有三種。

2.1 繼承Thread類創建

繼承Thread創建並啟動多線程有三個步驟:

  1. 定義類並繼承Thread,重寫run()方法,run()方法中為需要多線程執行的任務。

  2. 創建該類的實例,即創建了線程對象。

  3. 調用實例的start()方法啟動線程。

public class FirstThread extends Thread {

    private int i=0;
    public void run() {
        for (; i < 100; i++) {
            //獲取當前線程名稱
            System.out.println(this.getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            //Thread的靜態方法currentThread,獲取當前線程
            System.out.println(Thread.currentThread().getName());
            if (i == 20) {
                //創建線程並啟動
                new FirstThread().start();
                new FirstThread().start();
            }

        }
    }
}

運行結果可以看到兩個線程的i並不是連續的,說明他們並不共享數據。

2.2 實現Runnable接口

實現Runnable接口創建並啟動多線程也有以下步驟:

  1. 定義類並繼承Runnable接口,重寫run()方法,run()方法中為需要多線程執行的任務。

  2. 創建該類的實例,並以此實例作為target為參數來創建Thread對象,這個Thread對象才是真正的多線程對象。

public class SecondThread implements Runnable {
    private int i = 0;
    
    @Override
    public void run() {
        for (; i < 100; i++) {
            //此時想要獲取到多線程對象,只能使用Thread.currentThread()方法
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            //Thread的靜態方法currentThread,獲取當前線程
            System.out.println(Thread.currentThread().getName());
            if (i == 20) {
                //創建線程並啟動
                SecondThread secondThread=new SecondThread();
                new Thread(secondThread,"線程一").start();
                new Thread(secondThread,"線程二").start();
            }

        }
    }
}

2.3 使用Callable和Future

Callable是Runnable的增加版,主要是接口中的call()方法可以有返回值,並且可以申明拋出異常,使用Callable創建的步驟如下:

  1. 定義類並繼承Callable接口,重寫call()方法,run()方法中為需要多線程執行的任務。

  2. 創建類實例,使用FutureTask來包裝對象實例,

  3. 使用FutureTask對象作為Thread的target來創建多線程,並啟動線程。

  4. 調用FutureTask對象的get()方法來獲取子線程結束后的返回值。

public class ThirdThread {

    public static void main(String[] args) {
        //使用lambda表達式
        FutureTask<Integer> task = new FutureTask<>((Callable<Integer>) () -> {
            int i = 0;
            for (; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + "的循環變量i的值:" + i);
            }
            return i;
        });
        for (int i = 0; i < 100; i++) {
            //Thread的靜態方法currentThread,獲取當前線程
            System.out.println(Thread.currentThread().getName());
            if (i == 20) {
                //創建線程並啟動
                new Thread(task, "有返回值的線程").start();
            }
        }
        try {
            System.out.println("線程的返回值:" + task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

這裏使用了lambda表達式,不使用表達式的方式也很簡單,可以去源碼中查看。Callable與Runnable方式基本相同,只不過增加了返回值且可允許聲明拋出異常。

使用三種方式都可以創建線程,且方式也相對簡單,大體分為實現接口和實現Thread類兩種,這兩種都各有優缺點。

繼承接口實現:

  • 優點:除了繼承接口之外,還可以繼承其他類。這種方式多個線程共享一個target對象,可以處理用於共同資源的情況。
  • 缺點:編程稍微複雜一些,並且沒有直接獲取當前線程對象的方式,必須使用Thread.currentThread()方式。

基礎Thread類:

  • 優點:編程簡單

  • 缺點:不能繼承其他類

三、多線程的生命周期

線程狀態是線程中非常重要的一個概念,然而我看過很多資料,線程的狀態理解有很多種方式,很多人將其分為五個基本狀態:新建、就緒、運行、阻塞、死亡,但在狀態枚舉中並不是這五個狀態,我不知道是什麼原因(有大神可以解答更好),只能按照枚舉中的狀態根據自己的理解。

  1. 初始(NEW):新創建了一個線程對象,但還沒有調用start()方法,而且就算調用了改方法也不代表狀態立即改變。

  2. 運行(RUNNABLE):在運行的狀態肯定就處於RUNNABLE狀態。

  3. 阻塞(BLOCKED):表示線程阻塞,或者說線程已經被掛起了。

  4. 等待(WAITING):進入該狀態的線程需要等待其他線程做出一些特定動作(通知或中斷)。

  5. 超時等待(TIMED_WAITING):該狀態不同於WAITING,它可以在指定的時間后自行返回。

  6. 終止(TERMINATED):表示該線程已經執行完畢。

狀態流程圖如下:

理解:初始狀態很好理解,這個時候其實還不能被稱為一個線程,因為他還沒被啟動,當調用start()方法后,線程正式啟動,但是也不代表立即就改變了狀態。

運行狀態中其實包含兩種狀態,運行中(RUNING)就緒(READY)

就緒狀態表示你有資格運行,只要CPU還未調度到你,就處於就緒狀態,有幾個狀態會是線程狀態編程就緒狀態

  • 調用線程的start()方法。

  • 當前線程sleep()方法結束,其他線程join()結束,等待用戶輸入完畢,某個線程拿到對象鎖。

  • 當前線程時間片用完了,調用當前線程的yield()方法。

  • 鎖池裡的線程拿到對象鎖后。

運行中(RUNING)狀態比較好理解,線程調度程序選擇了當前線程作。

阻塞狀態是線程阻塞在進入synchronized關鍵字修飾的方法或代碼塊(獲取鎖)時的狀態。

等待狀態是指線程沒有被CPU分配執行時間,需要等待,這種等待是需要被显示的喚醒,否則會無限等待下去。

超時等待狀態是這現在沒有被CPU分配執行時間,需要等待,不過這種等待不需要被显示的喚醒,會設置一定的時間后zi懂喚醒。

死亡狀態也很好理解,說明線程方法被執行完成,或者出錯了,線程一旦進入這個狀態就代表徹底的結束

 

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

Git的使用 — 用git玩翻github,結尾有驚喜!有驚喜!有驚喜!林妙妙看了說:牛呲呼啦帶閃電 (三)(超詳解)

簡介

      上一篇主要講解的是Git安裝及配置,這一篇就詳細的從無到有的來用Git玩翻github。

一、什麼是Github

Github是全球最大的社交編程及代碼託管網站(https://github.com/)。

Github可以託管各種git庫,並提供一個web界面(用戶名.github.io/倉庫名)

二、Github和Git是什麼關係 

Git是版本控制軟件

Github是項目代碼託管的平台,藉助git來管理項目代碼

1、 使用Github

目的:藉助github託管項目代碼

2、基本概念

a、倉庫(Repository)

 倉庫的意思,即你的項目,你想在 GitHub 上開源一個項目,那就必須要新建一個 Repository ,如果你開源的項目多了,你就擁有了多個 Repositories 。

 倉庫用來存放項目代碼,每個項目對應一個倉庫,多個開源項目則有多個倉庫。

b、收藏(Star)

倉庫主頁star按鈕,意思為收藏項目的人數,收藏項目,方便下次查看,在 GitHub 上如果你有一個項目獲得100個star都算很不容易了!

【如何收藏】

 操作:打開對應項目主頁,點擊右上角  star 按鈕即可收藏

 情景:張三無意訪問到李四的開源項目感覺不錯並進行收藏

 

【如何查看自己得收藏】

 

c、複製項目(Fork)派生

這個不好翻譯,如果實在要翻譯我把他翻譯成分叉,什麼意思呢?你開源了一個項目,別人想在你這個項目的基礎上做些改進,然後應用到自己的項目中,這個時候他就可以 Fork 你的項目(打開項目主頁點擊右上角的fork按鈕即可),然後他的 GitHub 主頁上就多了一個項目,只不過這個項目是基於你的項目基礎(本質上是在原有項目的基礎上新建了一個分支),他就可以隨心所欲的去改進,但是絲毫不會影響原有項目的代碼與結構。

注意:該fork的項目時獨立存在的

比如:張三fork了李四的項目,相當於張三複制了李四的項目,所以自己也單獨有了一個一樣名稱的倉庫(注:該倉庫會聲明來自於李四,但是獨立存在)

d、發起請求(Pull Request)

發起請求,這個其實是基於 Fork 的,還是上面那個例子,如果別人在你基礎上做了改進,後來覺得改進的很不錯,應該要把這些改進讓更多的人收益,於是就想把自己的改進合併到原有項目里,這個時候他就可以發起一個 Pull Request(簡稱PR) ,原有項目創建人,也就是你,就可以收到這個請求,這個時候你會仔細review他的代碼,並且測試覺得OK了,就會接受他的PR,這個時候他做的改進原有項目就會擁有了。

e、關注(Watch)

這個也好理解就是觀察,如果你 Watch 了某個項目,那麼以後只要這個項目有任何更新,你都會第一時間收到關於這個項目的通知提醒。

f、問題(Issue)

發現代碼BUG,但是目前沒有成型代碼,需要討論時用; 問題的意思,舉個例子,就是你開源了一個項目,別人發現你的項目中有bug,或者哪些地方做的不夠好,他就可以給你提個 Issue ,即問題,提的問題多了,也就是 Issues ,然後你看到了這些問題就可以去逐個修復,修復ok了就可以一個個的 Close 掉。

g、Github主頁

賬號創建成功或點擊網址導航欄github圖標都可進入github主頁:該頁左側主要显示用戶動態以及關注用戶或關注倉庫的動態;右側显示所有的git庫

h、倉庫主頁

倉庫主頁主要显示項目的信息,如:項目代碼,版本,收藏/關注/fork情況等

i、個人主頁

個人信息:頭像,個人簡介,關注我的人,我關注的人,我關注的git庫,我的開源項目,我貢獻的開源項目等信息

3、註冊github賬號

官方網址:https://github.com

注意:

a、因為github在國外服務器所以訪問較慢或者無法訪問,需要FQ(***)

b、私有倉庫只能自己或者指定的朋友才有權限操作(私有倉庫是收費的)

c、新註冊的用戶必須驗證郵箱后才可以創建git庫倉庫

4、創建倉庫/創建新項目

說明:一個git庫(倉庫)對應一個開源項目。通過git管理git庫

a、創建倉庫

1)點擊【Start a project】創建一個倉庫

2)問題:點擊【Start a project】創建一個倉庫,后出現該頁面

2)原因:未驗證郵箱,點擊下圖框框中的鏈接進行驗證

 

3)點擊【resend】發送郵件驗證郵箱

 

 4)點擊【verify email address】驗證郵箱

   說明:驗證成功後會自動跳轉github主頁,重新點擊【Start a project】即可創建倉庫

 

5) 驗證郵箱后,點擊【Start a project】進入下圖界面

b、倉庫主頁說明

 

 

 注意:qq郵箱需要設置白名單才可以收到郵件

設置QQ郵箱白名單

1、打開QQ郵箱、點擊【設置】

2、點擊【反垃圾】

3、點擊【設置域名白名單】

4、在新頁面的input框中輸入【github.com】添加即可

5、倉庫管理

a、新建文件:倉庫主頁,點擊【create new file】創建倉庫文件

 

 

 

   

b、編輯文件:倉庫主頁,點擊【需要修改的文件】進入文件詳情頁

 

    

c、刪除文件

 

d、被刪除文件如何查看信息

答案:點擊commits按鈕查看

e、上傳文件

 

f、搜索倉庫文件:快捷鍵(t)

6、下載/檢出項目

7、Github Issues

作用:發現代碼BUG,但是目前沒有成型代碼,需要討論時用;或者使用開源項目出現問題時使用

情景:張三發現李四開源git庫,則發提交了一個issue;李四隔天登錄在github主頁看到通知並和張三交流,最後關閉issue

三、基本概念(實戰操作)

1、Github主頁

 

2、個人主頁

 

四、開源項目貢獻流程

1、新建Issue

提交使用問題或者建議或者想法

2、Pull Request

步驟:

a、 fork項目

b、 修改自己倉庫的項目代碼

c、 新建 pull request

d、 等待作者操作審核

五、下面就是驚喜:Github  Pages搭建網站

1、個人站點

訪問:https://用戶名.github.io

搭建步驟:

a、創建個人站點-》新建倉庫(倉庫名必須是【用戶名.github.io】)

 

b、在倉庫下新建index.html的文件即可

注意:a、Github Page僅支持靜態頁面

   b、倉庫裏面只能是.html文件

   c、個人主頁也可以設置主題

2、Project Pages 項目站點

訪問:https://用戶名.github.io/倉庫名

原理:gh-pages 用於構建和發布

搭建步驟

a、進入項目主頁,點擊settings

b、在settings頁面,點擊【Choose a theme 】來自動生成主題頁面

c、新建站點基礎信息設置

d、選擇主題

e、發布網頁(publish page)

六、小結

Clone和Fork的區別:

fork(派生):將別人的倉庫複製一份到自己的倉庫。
clone(克隆):將倉庫克隆到自己本地電腦中。

Fork的主要應用場景:
1.在A的倉庫中fork項目B (此時我們自己的github就有一個一模一樣的倉庫B,但是URL不同)
2.將我們修改的代碼push到自己github中的倉庫B中
3.pull request ,主人就會收到請求,並決定要不要接受你的代碼

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!